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:
Paul Rastoin
2026-02-06 18:02:02 +01:00
committed by GitHub
parent a494a7a902
commit 3dc5b162c7
133 changed files with 2141 additions and 1166 deletions
@@ -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,
},
@@ -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:
@@ -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);
}
}
}
@@ -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'];
@@ -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
{});
@@ -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');
});
});
});
@@ -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,
@@ -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,
@@ -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,
}),
);
@@ -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>;
};
@@ -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,
});
};