Spread in parent and requires FlatEntity.__universal (#17753)
# Introduction Requiring the spreaded `__universal` record that aggregates all the universal identifier ( relations fk and aggregators ) of an entity to its root It's blockin for https://github.com/twentyhq/twenty/pull/17687 to be finalized because if we don't we would have to migrated all related entities at once in order for them to always have the universal properties ## `resolveEntityRelationUniversalIdentifiers` Introduced `resolveEntityRelationUniversalIdentifiers` a centralized utility that resolves foreign key IDs to universal identifiers using ALL_METADATA_RELATIONS metadata. It provides strict typing for both input (foreign keys) and output (universal identifiers), with nullability dynamically inferred from entity relation types. Strictly and dynamically typed for both output and input To do so added a new type and const/runtime grain to ALL_METADATA_RELATIONS `isNullable`to many-to-one entries, derived from the entity relation property types. And fixed incorrectly typed typeorm entities ### Usage ```ts const { availabilityObjectMetadataUniversalIdentifier, frontComponentUniversalIdentifier, } = resolveEntityRelationUniversalIdentifiers({ metadataName: 'commandMenuItem', foreignKeyValues: { availabilityObjectMetadataId: createCommandMenuItemInput.availabilityObjectMetadataId, frontComponentId: createCommandMenuItemInput.frontComponentId, }, flatEntityMaps: { flatObjectMetadataMaps, flatFrontComponentMaps }, }); ```
This commit is contained in:
+39
-1
@@ -10,7 +10,7 @@ import { type FromMetadataEntityToMetadataName } from 'src/engine/metadata-modul
|
||||
import { type MetadataEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-entity.type';
|
||||
import { type SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
|
||||
|
||||
type ManyToOneRelationValue<
|
||||
export type ManyToOneRelationValue<
|
||||
TSourceMetadataName extends AllMetadataName,
|
||||
TRelationProperty extends ExtractEntityManyToOneEntityRelationProperties<
|
||||
MetadataEntity<TSourceMetadataName>
|
||||
@@ -29,6 +29,9 @@ type ManyToOneRelationValue<
|
||||
MetadataEntity<TSourceMetadataName>,
|
||||
'id' | 'workspaceId'
|
||||
>;
|
||||
isNullable: null extends MetadataEntity<TSourceMetadataName>[TRelationProperty]
|
||||
? true
|
||||
: false;
|
||||
}
|
||||
: null;
|
||||
|
||||
@@ -84,11 +87,13 @@ export const ALL_METADATA_RELATIONS = {
|
||||
metadataName: 'objectMetadata',
|
||||
flatEntityForeignKeyAggregator: null,
|
||||
foreignKey: 'availabilityObjectMetadataId',
|
||||
isNullable: true,
|
||||
},
|
||||
frontComponent: {
|
||||
metadataName: 'frontComponent',
|
||||
flatEntityForeignKeyAggregator: null,
|
||||
foreignKey: 'frontComponentId',
|
||||
isNullable: true,
|
||||
},
|
||||
},
|
||||
oneToMany: {},
|
||||
@@ -102,16 +107,19 @@ export const ALL_METADATA_RELATIONS = {
|
||||
metadataName: 'objectMetadata',
|
||||
flatEntityForeignKeyAggregator: null,
|
||||
foreignKey: 'targetObjectMetadataId',
|
||||
isNullable: true,
|
||||
},
|
||||
folder: {
|
||||
metadataName: 'navigationMenuItem',
|
||||
flatEntityForeignKeyAggregator: null,
|
||||
foreignKey: 'folderId',
|
||||
isNullable: true,
|
||||
},
|
||||
view: {
|
||||
metadataName: 'view',
|
||||
flatEntityForeignKeyAggregator: null,
|
||||
foreignKey: 'viewId',
|
||||
isNullable: true,
|
||||
},
|
||||
},
|
||||
oneToMany: {},
|
||||
@@ -122,6 +130,7 @@ export const ALL_METADATA_RELATIONS = {
|
||||
metadataName: 'objectMetadata',
|
||||
flatEntityForeignKeyAggregator: 'fieldIds',
|
||||
foreignKey: 'objectMetadataId',
|
||||
isNullable: false,
|
||||
},
|
||||
workspace: null,
|
||||
application: null,
|
||||
@@ -129,11 +138,13 @@ export const ALL_METADATA_RELATIONS = {
|
||||
metadataName: 'fieldMetadata',
|
||||
flatEntityForeignKeyAggregator: null,
|
||||
foreignKey: 'relationTargetFieldMetadataId',
|
||||
isNullable: true,
|
||||
},
|
||||
relationTargetObjectMetadata: {
|
||||
metadataName: 'objectMetadata',
|
||||
flatEntityForeignKeyAggregator: null,
|
||||
foreignKey: 'relationTargetObjectMetadataId',
|
||||
isNullable: true,
|
||||
},
|
||||
},
|
||||
oneToMany: {
|
||||
@@ -167,6 +178,7 @@ export const ALL_METADATA_RELATIONS = {
|
||||
foreignKey: 'objectMetadataId',
|
||||
metadataName: 'objectMetadata',
|
||||
flatEntityForeignKeyAggregator: 'viewIds',
|
||||
isNullable: false,
|
||||
},
|
||||
workspace: null,
|
||||
createdBy: null,
|
||||
@@ -175,16 +187,19 @@ export const ALL_METADATA_RELATIONS = {
|
||||
foreignKey: 'calendarFieldMetadataId',
|
||||
metadataName: 'fieldMetadata',
|
||||
flatEntityForeignKeyAggregator: 'calendarViewIds',
|
||||
isNullable: true,
|
||||
},
|
||||
kanbanAggregateOperationFieldMetadata: {
|
||||
foreignKey: 'kanbanAggregateOperationFieldMetadataId',
|
||||
metadataName: 'fieldMetadata',
|
||||
flatEntityForeignKeyAggregator: 'kanbanAggregateOperationViewIds',
|
||||
isNullable: true,
|
||||
},
|
||||
mainGroupByFieldMetadata: {
|
||||
foreignKey: 'mainGroupByFieldMetadataId',
|
||||
metadataName: 'fieldMetadata',
|
||||
flatEntityForeignKeyAggregator: 'mainGroupByFieldMetadataViewIds',
|
||||
isNullable: true,
|
||||
},
|
||||
},
|
||||
oneToMany: {
|
||||
@@ -204,11 +219,13 @@ export const ALL_METADATA_RELATIONS = {
|
||||
metadataName: 'fieldMetadata',
|
||||
flatEntityForeignKeyAggregator: 'viewFieldIds',
|
||||
foreignKey: 'fieldMetadataId',
|
||||
isNullable: false,
|
||||
},
|
||||
view: {
|
||||
metadataName: 'view',
|
||||
flatEntityForeignKeyAggregator: 'viewFieldIds',
|
||||
foreignKey: 'viewId',
|
||||
isNullable: false,
|
||||
},
|
||||
workspace: null,
|
||||
application: null,
|
||||
@@ -221,16 +238,19 @@ export const ALL_METADATA_RELATIONS = {
|
||||
metadataName: 'fieldMetadata',
|
||||
flatEntityForeignKeyAggregator: 'viewFilterIds',
|
||||
foreignKey: 'fieldMetadataId',
|
||||
isNullable: false,
|
||||
},
|
||||
view: {
|
||||
metadataName: 'view',
|
||||
flatEntityForeignKeyAggregator: 'viewFilterIds',
|
||||
foreignKey: 'viewId',
|
||||
isNullable: false,
|
||||
},
|
||||
viewFilterGroup: {
|
||||
flatEntityForeignKeyAggregator: 'viewFilterIds',
|
||||
foreignKey: 'viewFilterGroupId',
|
||||
metadataName: 'viewFilterGroup',
|
||||
isNullable: true,
|
||||
},
|
||||
workspace: null,
|
||||
application: null,
|
||||
@@ -243,6 +263,7 @@ export const ALL_METADATA_RELATIONS = {
|
||||
metadataName: 'view',
|
||||
flatEntityForeignKeyAggregator: 'viewGroupIds',
|
||||
foreignKey: 'viewId',
|
||||
isNullable: false,
|
||||
},
|
||||
workspace: null,
|
||||
application: null,
|
||||
@@ -255,6 +276,7 @@ export const ALL_METADATA_RELATIONS = {
|
||||
metadataName: 'objectMetadata',
|
||||
flatEntityForeignKeyAggregator: 'indexMetadataIds',
|
||||
foreignKey: 'objectMetadataId',
|
||||
isNullable: false,
|
||||
},
|
||||
workspace: null,
|
||||
application: null,
|
||||
@@ -294,6 +316,7 @@ export const ALL_METADATA_RELATIONS = {
|
||||
metadataName: 'role',
|
||||
flatEntityForeignKeyAggregator: 'roleTargetIds',
|
||||
foreignKey: 'roleId',
|
||||
isNullable: false,
|
||||
},
|
||||
apiKey: null,
|
||||
workspace: null,
|
||||
@@ -308,12 +331,14 @@ export const ALL_METADATA_RELATIONS = {
|
||||
metadataName: 'objectMetadata',
|
||||
flatEntityForeignKeyAggregator: null,
|
||||
foreignKey: 'objectMetadataId',
|
||||
isNullable: true,
|
||||
},
|
||||
application: null,
|
||||
defaultTabToFocusOnMobileAndSidePanel: {
|
||||
metadataName: 'pageLayoutTab',
|
||||
flatEntityForeignKeyAggregator: null,
|
||||
foreignKey: 'defaultTabToFocusOnMobileAndSidePanelId',
|
||||
isNullable: true,
|
||||
},
|
||||
},
|
||||
oneToMany: {
|
||||
@@ -327,6 +352,7 @@ export const ALL_METADATA_RELATIONS = {
|
||||
metadataName: 'pageLayout',
|
||||
flatEntityForeignKeyAggregator: 'tabIds',
|
||||
foreignKey: 'pageLayoutId',
|
||||
isNullable: false,
|
||||
},
|
||||
application: null,
|
||||
},
|
||||
@@ -341,11 +367,13 @@ export const ALL_METADATA_RELATIONS = {
|
||||
metadataName: 'pageLayoutTab',
|
||||
flatEntityForeignKeyAggregator: 'widgetIds',
|
||||
foreignKey: 'pageLayoutTabId',
|
||||
isNullable: false,
|
||||
},
|
||||
objectMetadata: {
|
||||
metadataName: 'objectMetadata',
|
||||
flatEntityForeignKeyAggregator: null,
|
||||
foreignKey: 'objectMetadataId',
|
||||
isNullable: true,
|
||||
},
|
||||
application: null,
|
||||
},
|
||||
@@ -358,26 +386,31 @@ export const ALL_METADATA_RELATIONS = {
|
||||
metadataName: 'role',
|
||||
flatEntityForeignKeyAggregator: null,
|
||||
foreignKey: 'roleId',
|
||||
isNullable: false,
|
||||
},
|
||||
fieldMetadata: {
|
||||
metadataName: 'fieldMetadata',
|
||||
flatEntityForeignKeyAggregator: null,
|
||||
foreignKey: 'fieldMetadataId',
|
||||
isNullable: false,
|
||||
},
|
||||
workspaceMemberFieldMetadata: {
|
||||
metadataName: 'fieldMetadata',
|
||||
flatEntityForeignKeyAggregator: null,
|
||||
foreignKey: 'workspaceMemberFieldMetadataId',
|
||||
isNullable: true,
|
||||
},
|
||||
objectMetadata: {
|
||||
metadataName: 'objectMetadata',
|
||||
flatEntityForeignKeyAggregator: null,
|
||||
foreignKey: 'objectMetadataId',
|
||||
isNullable: false,
|
||||
},
|
||||
rowLevelPermissionPredicateGroup: {
|
||||
metadataName: 'rowLevelPermissionPredicateGroup',
|
||||
flatEntityForeignKeyAggregator: null,
|
||||
foreignKey: 'rowLevelPermissionPredicateGroupId',
|
||||
isNullable: true,
|
||||
},
|
||||
application: null,
|
||||
},
|
||||
@@ -389,17 +422,20 @@ export const ALL_METADATA_RELATIONS = {
|
||||
metadataName: 'objectMetadata',
|
||||
flatEntityForeignKeyAggregator: null,
|
||||
foreignKey: 'objectMetadataId',
|
||||
isNullable: false,
|
||||
},
|
||||
role: {
|
||||
metadataName: 'role',
|
||||
foreignKey: 'roleId',
|
||||
flatEntityForeignKeyAggregator: null,
|
||||
isNullable: false,
|
||||
},
|
||||
parentRowLevelPermissionPredicateGroup: {
|
||||
metadataName: 'rowLevelPermissionPredicateGroup',
|
||||
foreignKey: 'parentRowLevelPermissionPredicateGroupId',
|
||||
flatEntityForeignKeyAggregator:
|
||||
'childRowLevelPermissionPredicateGroupIds',
|
||||
isNullable: true,
|
||||
},
|
||||
workspace: null,
|
||||
application: null,
|
||||
@@ -420,11 +456,13 @@ export const ALL_METADATA_RELATIONS = {
|
||||
flatEntityForeignKeyAggregator: 'childViewFilterGroupIds',
|
||||
foreignKey: 'parentViewFilterGroupId',
|
||||
metadataName: 'viewFilterGroup',
|
||||
isNullable: true,
|
||||
},
|
||||
view: {
|
||||
metadataName: 'view',
|
||||
flatEntityForeignKeyAggregator: 'viewFilterGroupIds',
|
||||
foreignKey: 'viewId',
|
||||
isNullable: false,
|
||||
},
|
||||
workspace: null,
|
||||
},
|
||||
|
||||
+3
@@ -8,6 +8,8 @@ import {
|
||||
} from 'src/utils/custom-exception';
|
||||
|
||||
export const FlatEntityMapsExceptionCode = appendCommonExceptionCode({
|
||||
RELATION_UNIVERSAL_IDENTIFIER_NOT_FOUND:
|
||||
'RELATION_UNIVERSAL_IDENTIFIER_NOT_FOUND',
|
||||
ENTITY_ALREADY_EXISTS: 'ENTITY_ALREADY_EXISTS',
|
||||
ENTITY_NOT_FOUND: 'ENTITY_NOT_FOUND',
|
||||
ENTITY_MALFORMED: 'ENTITY_MALFORMED',
|
||||
@@ -17,6 +19,7 @@ const getFlatEntityMapsExceptionUserFriendlyMessage = (
|
||||
code: keyof typeof FlatEntityMapsExceptionCode,
|
||||
) => {
|
||||
switch (code) {
|
||||
case FlatEntityMapsExceptionCode.RELATION_UNIVERSAL_IDENTIFIER_NOT_FOUND:
|
||||
case FlatEntityMapsExceptionCode.ENTITY_ALREADY_EXISTS:
|
||||
case FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND:
|
||||
case FlatEntityMapsExceptionCode.ENTITY_MALFORMED:
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { Catch, type ExceptionFilter, Injectable } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
InternalServerError,
|
||||
NotFoundError,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import {
|
||||
FlatEntityMapsException,
|
||||
FlatEntityMapsExceptionCode,
|
||||
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
|
||||
|
||||
@Catch(FlatEntityMapsException)
|
||||
@Injectable()
|
||||
export class FlatEntityMapsGraphqlApiExceptionFilter
|
||||
implements ExceptionFilter
|
||||
{
|
||||
catch(exception: FlatEntityMapsException) {
|
||||
switch (exception.code) {
|
||||
case FlatEntityMapsExceptionCode.RELATION_UNIVERSAL_IDENTIFIER_NOT_FOUND:
|
||||
throw new NotFoundError(exception);
|
||||
case FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND:
|
||||
case FlatEntityMapsExceptionCode.ENTITY_ALREADY_EXISTS:
|
||||
case FlatEntityMapsExceptionCode.ENTITY_MALFORMED:
|
||||
case FlatEntityMapsExceptionCode.INTERNAL_SERVER_ERROR:
|
||||
throw new InternalServerError(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
import { type AllMetadataName } from 'twenty-shared/metadata';
|
||||
|
||||
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
|
||||
|
||||
/**
|
||||
* Currently under migration but aims to replace FlatEntity afterwards
|
||||
*/
|
||||
export type FlatEntityFromV2<
|
||||
TEntity,
|
||||
TMetadataName extends AllMetadataName | undefined = undefined,
|
||||
TInnerFlatEntity extends { __universal?: unknown } = FlatEntityFrom<
|
||||
TEntity,
|
||||
TMetadataName
|
||||
>,
|
||||
> = Omit<TInnerFlatEntity, '__universal'> & TInnerFlatEntity['__universal'];
|
||||
+6
-13
@@ -28,18 +28,11 @@ export type FlatEntityFrom<
|
||||
CastRecordTypeOrmDatePropertiesToString<TEntity> &
|
||||
AddSuffixToEntityOneToManyProperties<TEntity, 'ids'> &
|
||||
(TEntity extends SyncableEntity
|
||||
? {
|
||||
/**
|
||||
* /!\ Under migration the idea is at some point to replace FlatEntity by UniversalFlatEntity /!\
|
||||
* Please avoid any usage or contact me ( prastoin ) before doing so
|
||||
* TODO remove with FlatEntity once it has been fully migrated
|
||||
*/
|
||||
__universal?: UniversalFlatEntityExtraProperties<
|
||||
TEntity,
|
||||
TMetadataName extends undefined
|
||||
? FromMetadataEntityToMetadataName<TEntity>
|
||||
: TMetadataName
|
||||
> & { universalIdentifier: string };
|
||||
}
|
||||
? UniversalFlatEntityExtraProperties<
|
||||
TEntity,
|
||||
TMetadataName extends undefined
|
||||
? FromMetadataEntityToMetadataName<TEntity>
|
||||
: TMetadataName
|
||||
> & { universalIdentifier: string }
|
||||
: // eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
{});
|
||||
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
import { FlatEntityMapsExceptionCode } from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { resolveEntityRelationUniversalIdentifiers } from 'src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util';
|
||||
|
||||
const buildFlatEntityMaps = (
|
||||
entries: { id: string; universalIdentifier: string }[],
|
||||
): FlatEntityMaps<any> => ({
|
||||
byUniversalIdentifier: Object.fromEntries(
|
||||
entries.map((entry) => [
|
||||
entry.universalIdentifier,
|
||||
{ universalIdentifier: entry.universalIdentifier, id: entry.id },
|
||||
]),
|
||||
),
|
||||
universalIdentifierById: Object.fromEntries(
|
||||
entries.map((entry) => [entry.id, entry.universalIdentifier]),
|
||||
),
|
||||
universalIdentifiersByApplicationId: {},
|
||||
});
|
||||
|
||||
const EMPTY_FLAT_ENTITY_MAPS: FlatEntityMaps<any> = {
|
||||
byUniversalIdentifier: {},
|
||||
universalIdentifierById: {},
|
||||
universalIdentifiersByApplicationId: {},
|
||||
};
|
||||
|
||||
describe('resolveEntityRelationUniversalIdentifiers', () => {
|
||||
describe('non-nullable relations', () => {
|
||||
it('should resolve universal identifier for a provided foreign key', () => {
|
||||
const result = resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'viewField',
|
||||
foreignKeyValues: {
|
||||
fieldMetadataId: 'field-id-1',
|
||||
viewId: 'view-id-1',
|
||||
},
|
||||
flatEntityMaps: {
|
||||
flatFieldMetadataMaps: buildFlatEntityMaps([
|
||||
{ id: 'field-id-1', universalIdentifier: 'field-ui-1' },
|
||||
]),
|
||||
flatViewMaps: buildFlatEntityMaps([
|
||||
{ id: 'view-id-1', universalIdentifier: 'view-ui-1' },
|
||||
]),
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
fieldMetadataUniversalIdentifier: 'field-ui-1',
|
||||
viewUniversalIdentifier: 'view-ui-1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw when a non-nullable foreign key id does not exist in maps', () => {
|
||||
expect(() =>
|
||||
resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'viewField',
|
||||
foreignKeyValues: {
|
||||
fieldMetadataId: 'non-existent-id',
|
||||
viewId: 'view-id-1',
|
||||
},
|
||||
flatEntityMaps: {
|
||||
flatFieldMetadataMaps: EMPTY_FLAT_ENTITY_MAPS,
|
||||
flatViewMaps: buildFlatEntityMaps([
|
||||
{ id: 'view-id-1', universalIdentifier: 'view-ui-1' },
|
||||
]),
|
||||
},
|
||||
}),
|
||||
).toThrow(
|
||||
expect.objectContaining({
|
||||
code: FlatEntityMapsExceptionCode.RELATION_UNIVERSAL_IDENTIFIER_NOT_FOUND,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('nullable relations', () => {
|
||||
it('should return null when a nullable foreign key value is null', () => {
|
||||
const result = resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'pageLayout',
|
||||
foreignKeyValues: {
|
||||
objectMetadataId: null,
|
||||
defaultTabToFocusOnMobileAndSidePanelId: undefined,
|
||||
},
|
||||
flatEntityMaps: {
|
||||
flatObjectMetadataMaps: EMPTY_FLAT_ENTITY_MAPS,
|
||||
flatPageLayoutTabMaps: EMPTY_FLAT_ENTITY_MAPS,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
objectMetadataUniversalIdentifier: null,
|
||||
defaultTabToFocusOnMobileAndSidePanelUniversalIdentifier: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should resolve universal identifier when a nullable foreign key has a valid id', () => {
|
||||
const result = resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'pageLayout',
|
||||
foreignKeyValues: {
|
||||
objectMetadataId: 'obj-id-1',
|
||||
},
|
||||
flatEntityMaps: {
|
||||
flatObjectMetadataMaps: buildFlatEntityMaps([
|
||||
{ id: 'obj-id-1', universalIdentifier: 'obj-ui-1' },
|
||||
]),
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
objectMetadataUniversalIdentifier: 'obj-ui-1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw when a nullable foreign key has a non-existent id', () => {
|
||||
expect(() =>
|
||||
resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'pageLayout',
|
||||
foreignKeyValues: {
|
||||
objectMetadataId: 'non-existent-id',
|
||||
},
|
||||
flatEntityMaps: {
|
||||
flatObjectMetadataMaps: EMPTY_FLAT_ENTITY_MAPS,
|
||||
},
|
||||
}),
|
||||
).toThrow(
|
||||
expect.objectContaining({
|
||||
code: FlatEntityMapsExceptionCode.RELATION_UNIVERSAL_IDENTIFIER_NOT_FOUND,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('partial foreign key resolution', () => {
|
||||
it('should only resolve universal identifiers for provided foreign keys', () => {
|
||||
const result = resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'viewField',
|
||||
foreignKeyValues: {
|
||||
fieldMetadataId: 'field-id-1',
|
||||
},
|
||||
flatEntityMaps: {
|
||||
flatFieldMetadataMaps: buildFlatEntityMaps([
|
||||
{ id: 'field-id-1', universalIdentifier: 'field-ui-1' },
|
||||
]),
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
fieldMetadataUniversalIdentifier: 'field-ui-1',
|
||||
});
|
||||
expect(result).not.toHaveProperty('viewUniversalIdentifier');
|
||||
});
|
||||
});
|
||||
});
|
||||
+3
-4
@@ -7,18 +7,17 @@ import {
|
||||
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util';
|
||||
import { type UniversalSyncableFlatEntity } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type';
|
||||
|
||||
export const findFlatEntityByUniversalIdentifierOrThrow = <
|
||||
T extends SyncableFlatEntity,
|
||||
T extends SyncableFlatEntity | UniversalSyncableFlatEntity,
|
||||
>({
|
||||
flatEntityMaps,
|
||||
universalIdentifier,
|
||||
}: {
|
||||
flatEntityMaps: FlatEntityMaps<T>;
|
||||
universalIdentifier: string;
|
||||
}): T & {
|
||||
id: string;
|
||||
} => {
|
||||
}): T => {
|
||||
const flatEntity = findFlatEntityByUniversalIdentifier({
|
||||
flatEntityMaps,
|
||||
universalIdentifier,
|
||||
|
||||
+2
-1
@@ -1,8 +1,9 @@
|
||||
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { type UniversalSyncableFlatEntity } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type';
|
||||
|
||||
export const findFlatEntityByUniversalIdentifier = <
|
||||
T extends SyncableFlatEntity,
|
||||
T extends SyncableFlatEntity | UniversalSyncableFlatEntity,
|
||||
>({
|
||||
flatEntityMaps,
|
||||
universalIdentifier,
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
|
||||
import { type UniversalSyncableFlatEntity } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type';
|
||||
|
||||
export type FindManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMapsOrThrowArgs<
|
||||
T extends SyncableFlatEntity | UniversalSyncableFlatEntity,
|
||||
> = {
|
||||
flatEntityMaps: FlatEntityMaps<T>;
|
||||
universalIdentifiers: string[];
|
||||
};
|
||||
|
||||
export const findManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMapsOrThrow =
|
||||
<T extends SyncableFlatEntity | UniversalSyncableFlatEntity>({
|
||||
flatEntityMaps,
|
||||
universalIdentifiers,
|
||||
}: FindManyFlatEntityByUniversalIdentifierInUniversalFlatEntityMapsOrThrowArgs<T>): T[] =>
|
||||
universalIdentifiers.map((universalIdentifier) =>
|
||||
findFlatEntityByUniversalIdentifierOrThrow({
|
||||
flatEntityMaps,
|
||||
universalIdentifier,
|
||||
}),
|
||||
);
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { type AllMetadataName } from 'twenty-shared/metadata';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
ALL_METADATA_RELATIONS,
|
||||
type ManyToOneRelationValue,
|
||||
} from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-relations.constant';
|
||||
import {
|
||||
FlatEntityMapsException,
|
||||
FlatEntityMapsExceptionCode,
|
||||
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
|
||||
import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
import { type ExtractEntityManyToOneEntityRelationProperties } from 'src/engine/metadata-modules/flat-entity/types/extract-entity-many-to-one-entity-relation-properties.type';
|
||||
import { type MetadataEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-entity.type';
|
||||
import { type MetadataManyToOneJoinColumn } from 'src/engine/metadata-modules/flat-entity/types/metadata-many-to-one-join-column.type';
|
||||
import { type MetadataToFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/types/metadata-to-flat-entity-maps-key';
|
||||
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
|
||||
import { type RemoveSuffix } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/remove-suffix.type';
|
||||
|
||||
type ManyToOneConfig<T extends AllMetadataName> =
|
||||
(typeof ALL_METADATA_RELATIONS)[T]['manyToOne'];
|
||||
|
||||
type TargetMetadataNamesForForeignKeys<
|
||||
T extends AllMetadataName,
|
||||
TProvidedKeys extends string,
|
||||
> = {
|
||||
[K in keyof ManyToOneConfig<T>]: ManyToOneConfig<T>[K] extends {
|
||||
foreignKey: infer _FK extends TProvidedKeys;
|
||||
metadataName: infer MN extends AllMetadataName;
|
||||
}
|
||||
? MN
|
||||
: never;
|
||||
}[keyof ManyToOneConfig<T>];
|
||||
|
||||
type RequiredFlatEntityMapsForForeignKeys<
|
||||
T extends AllMetadataName,
|
||||
TProvidedKeys extends string,
|
||||
> = Pick<
|
||||
AllFlatEntityMaps,
|
||||
MetadataToFlatEntityMapsKey<
|
||||
TargetMetadataNamesForForeignKeys<T, TProvidedKeys>
|
||||
>
|
||||
>;
|
||||
|
||||
type ResolvedUniversalIdentifiers<
|
||||
T extends AllMetadataName,
|
||||
TProvidedKeys extends Extract<MetadataManyToOneJoinColumn<T>, string>,
|
||||
> = {
|
||||
[K in keyof ManyToOneConfig<T> as ManyToOneConfig<T>[K] extends {
|
||||
foreignKey: infer FK extends string;
|
||||
}
|
||||
? FK extends TProvidedKeys
|
||||
? `${RemoveSuffix<FK, 'Id'>}UniversalIdentifier`
|
||||
: never
|
||||
: never]: ManyToOneConfig<T>[K] extends { isNullable: true }
|
||||
? string | null
|
||||
: string;
|
||||
};
|
||||
|
||||
export const resolveEntityRelationUniversalIdentifiers = <
|
||||
T extends AllMetadataName,
|
||||
TProvidedKeys extends Extract<
|
||||
MetadataManyToOneJoinColumn<T>,
|
||||
string
|
||||
> = Extract<MetadataManyToOneJoinColumn<T>, string>,
|
||||
>({
|
||||
metadataName,
|
||||
foreignKeyValues,
|
||||
flatEntityMaps,
|
||||
}: {
|
||||
metadataName: T;
|
||||
foreignKeyValues: Record<TProvidedKeys, string | null | undefined>;
|
||||
flatEntityMaps: RequiredFlatEntityMapsForForeignKeys<T, TProvidedKeys>;
|
||||
}): ResolvedUniversalIdentifiers<T, TProvidedKeys> => {
|
||||
const relations = ALL_METADATA_RELATIONS[metadataName].manyToOne;
|
||||
const result: Record<string, string | null> = {};
|
||||
|
||||
for (const relation of Object.values(relations) as ManyToOneRelationValue<
|
||||
T,
|
||||
ExtractEntityManyToOneEntityRelationProperties<MetadataEntity<T>>
|
||||
>[]) {
|
||||
if (!isDefined(relation)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const {
|
||||
foreignKey,
|
||||
metadataName: targetMetadataName,
|
||||
isNullable,
|
||||
} = relation;
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(foreignKeyValues, foreignKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const foreignKeyValue =
|
||||
foreignKeyValues[foreignKey as keyof typeof foreignKeyValues];
|
||||
|
||||
const mapsKey = getMetadataFlatEntityMapsKey(
|
||||
targetMetadataName,
|
||||
) as keyof RequiredFlatEntityMapsForForeignKeys<T, TProvidedKeys>;
|
||||
const targetFlatEntityMaps = flatEntityMaps[mapsKey];
|
||||
|
||||
// TODO refactor using the new ALL_METADATA_UNIVERSAL_RELATION afterwards
|
||||
const universalIdentifierKey = foreignKey.replace(
|
||||
/Id$/,
|
||||
'UniversalIdentifier',
|
||||
);
|
||||
|
||||
if (isNullable && !isDefined(foreignKeyValue)) {
|
||||
result[universalIdentifierKey] = null;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const resolvedUniversalIdentifier =
|
||||
targetFlatEntityMaps.universalIdentifierById[foreignKeyValue as string];
|
||||
|
||||
if (!isDefined(resolvedUniversalIdentifier)) {
|
||||
throw new FlatEntityMapsException(
|
||||
t`Could not find ${targetMetadataName} for given ${foreignKey}`,
|
||||
FlatEntityMapsExceptionCode.RELATION_UNIVERSAL_IDENTIFIER_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
result[universalIdentifierKey] = resolvedUniversalIdentifier;
|
||||
}
|
||||
|
||||
return result as ResolvedUniversalIdentifiers<T, TProvidedKeys>;
|
||||
};
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { type AllMetadataName } from 'twenty-shared/metadata';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
FlatEntityMapsException,
|
||||
FlatEntityMapsExceptionCode,
|
||||
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
|
||||
import { type MetadataFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
|
||||
// @deprecated Use resolveEntityRelationUniversalIdentifiers instead
|
||||
export const resolveUniversalIdentifierFromFlatEntityIdOrThrow = <
|
||||
T extends AllMetadataName,
|
||||
>({
|
||||
flatEntityMaps,
|
||||
flatEntityId,
|
||||
metadataName,
|
||||
}: {
|
||||
flatEntityMaps: MetadataFlatEntityMaps<T>;
|
||||
flatEntityId: string;
|
||||
metadataName: T;
|
||||
}): string => {
|
||||
const flatEntity = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityMaps,
|
||||
flatEntityId,
|
||||
});
|
||||
|
||||
if (!isDefined(flatEntity)) {
|
||||
throw new FlatEntityMapsException(
|
||||
t`Could not find ${metadataName}`,
|
||||
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return flatEntity.universalIdentifier;
|
||||
};
|
||||
|
||||
// @deprecated Use resolveEntityRelationUniversalIdentifiers instead
|
||||
export const resolveNullableUniversalIdentifierFromFlatEntityId = <
|
||||
T extends AllMetadataName,
|
||||
>({
|
||||
flatEntityMaps,
|
||||
flatEntityId,
|
||||
metadataName,
|
||||
}: {
|
||||
flatEntityMaps: MetadataFlatEntityMaps<T>;
|
||||
flatEntityId: string | null | undefined;
|
||||
metadataName: T;
|
||||
}): string | null => {
|
||||
if (!isDefined(flatEntityId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return resolveUniversalIdentifierFromFlatEntityIdOrThrow({
|
||||
flatEntityMaps,
|
||||
flatEntityId,
|
||||
metadataName,
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user