Factorize from entity to flat entity utils (#21972)

## What

Factorizes the two responsibilities that were copy‑pasted across every
`from-<entity>-entity-to-flat-<entity>` util into two reusable tools.

### `fromEntityToScalarEntity`
Projects a TypeORM entity into its scalar flat shape using an
**allow‑list** driven by
`ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME` (plus the base
columns `id`/`workspaceId`/`applicationId`/`universalIdentifier`). Only
registered scalar columns are forwarded, `Date`s are serialized to ISO
strings, and absent values are normalized to `null`. Replaces the
previous deny‑list (`removePropertiesFromRecord`) approach, so
unregistered/deprecated columns can no longer silently leak into the
flat entity.

### `resolveManyToOneRelationIdsToUniversalIdentifiers`
Resolves an entity's many‑to‑one foreign keys to their universal
identifiers, driven by `ALL_MANY_TO_ONE_METADATA_RELATIONS`. Handles the
always‑present `application`, nullable relations, and throws a
`FlatEntityMapsException` when a referenced id is missing from its
identifier map. Mirrors `resolveUniversalRelationIdentifiersToIds` in
the opposite direction.

Each `from-<entity>` util now reduces to: scalar spread + relation
spread (+ explicit one‑to‑many id/universalIdentifier arrays where
applicable).

### Note
The allow‑list drops `isUIReadOnly` (a `WasRemovedInUpgrade` column not
in the config) from `fieldMetadata`, which is the only
integration‑snapshot change.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21972?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Paul Rastoin
2026-06-24 12:20:09 +02:00
committed by GitHub
parent 41d1b478b0
commit d2387430a1
34 changed files with 1106 additions and 1614 deletions
@@ -0,0 +1,236 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { FlatEntityMapsExceptionCode } from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { type EntityManyToOneIdByUniversalIdentifierMaps } from 'src/engine/workspace-cache/types/entity-many-to-one-id-by-universal-identifier-maps.type';
import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type';
import { resolveManyToOneRelationIdsToUniversalIdentifiers } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-many-to-one-relation-ids-to-universal-identifiers.util';
const buildArgs = <T extends AllMetadataName>(
args: {
metadataName: T;
entity: Record<string, string | null | undefined>;
} & EntityManyToOneIdByUniversalIdentifierMaps<T>,
): { metadataName: T } & FromEntityToFlatEntityArgs<T> => ({
...args,
entity: args.entity as unknown as FromEntityToFlatEntityArgs<T>['entity'],
});
describe('resolveManyToOneRelationIdsToUniversalIdentifiers', () => {
describe('application resolution', () => {
it('should resolve only the application universal identifier when the entity has no other many-to-one relations', () => {
const result = resolveManyToOneRelationIdsToUniversalIdentifiers(
buildArgs({
metadataName: 'permissionFlag',
entity: {
id: 'permission-flag-id-1',
applicationId: 'app-id-1',
},
applicationIdToUniversalIdentifierMap: new Map([
['app-id-1', 'app-ui-1'],
]),
}),
);
expect(result).toEqual({
applicationUniversalIdentifier: 'app-ui-1',
});
});
it('should throw when the applicationId is not present in the map', () => {
expect(() =>
resolveManyToOneRelationIdsToUniversalIdentifiers(
buildArgs({
metadataName: 'permissionFlag',
entity: {
id: 'permission-flag-id-1',
applicationId: 'app-id-1',
},
applicationIdToUniversalIdentifierMap: new Map(),
}),
),
).toThrow(
expect.objectContaining({
code: FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
}),
);
});
it('should throw when the entity has no applicationId', () => {
expect(() =>
resolveManyToOneRelationIdsToUniversalIdentifiers(
buildArgs({
metadataName: 'permissionFlag',
entity: {
id: 'permission-flag-id-1',
applicationId: null,
},
applicationIdToUniversalIdentifierMap: new Map([
['app-id-1', 'app-ui-1'],
]),
}),
),
).toThrow(
expect.objectContaining({
code: FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
}),
);
});
});
describe('non-nullable relations', () => {
it('should resolve universal identifiers for application and non-nullable foreign keys', () => {
const result = resolveManyToOneRelationIdsToUniversalIdentifiers(
buildArgs({
metadataName: 'viewField',
entity: {
id: 'view-field-id-1',
applicationId: 'app-id-1',
fieldMetadataId: 'field-id-1',
viewId: 'view-id-1',
viewFieldGroupId: 'view-field-group-id-1',
},
applicationIdToUniversalIdentifierMap: new Map([
['app-id-1', 'app-ui-1'],
]),
fieldMetadataIdToUniversalIdentifierMap: new Map([
['field-id-1', 'field-ui-1'],
]),
viewIdToUniversalIdentifierMap: new Map([['view-id-1', 'view-ui-1']]),
viewFieldGroupIdToUniversalIdentifierMap: new Map([
['view-field-group-id-1', 'view-field-group-ui-1'],
]),
}),
);
expect(result).toEqual({
applicationUniversalIdentifier: 'app-ui-1',
fieldMetadataUniversalIdentifier: 'field-ui-1',
viewUniversalIdentifier: 'view-ui-1',
viewFieldGroupUniversalIdentifier: 'view-field-group-ui-1',
});
});
it('should throw when a non-nullable foreign key id is missing from the map', () => {
expect(() =>
resolveManyToOneRelationIdsToUniversalIdentifiers(
buildArgs({
metadataName: 'viewField',
entity: {
id: 'view-field-id-1',
applicationId: 'app-id-1',
fieldMetadataId: 'non-existent-field-id',
viewId: 'view-id-1',
viewFieldGroupId: null,
},
applicationIdToUniversalIdentifierMap: new Map([
['app-id-1', 'app-ui-1'],
]),
fieldMetadataIdToUniversalIdentifierMap: new Map(),
viewIdToUniversalIdentifierMap: new Map([
['view-id-1', 'view-ui-1'],
]),
viewFieldGroupIdToUniversalIdentifierMap: new Map(),
}),
),
).toThrow(
expect.objectContaining({
code: FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
}),
);
});
it('should throw a malformed-entity error when a non-nullable foreign key is itself missing', () => {
expect(() =>
resolveManyToOneRelationIdsToUniversalIdentifiers(
buildArgs({
metadataName: 'viewField',
entity: {
id: 'view-field-id-1',
applicationId: 'app-id-1',
fieldMetadataId: null,
viewId: 'view-id-1',
viewFieldGroupId: null,
},
applicationIdToUniversalIdentifierMap: new Map([
['app-id-1', 'app-ui-1'],
]),
fieldMetadataIdToUniversalIdentifierMap: new Map([
['field-id-1', 'field-ui-1'],
]),
viewIdToUniversalIdentifierMap: new Map([
['view-id-1', 'view-ui-1'],
]),
viewFieldGroupIdToUniversalIdentifierMap: new Map(),
}),
),
).toThrow(
expect.objectContaining({
code: FlatEntityMapsExceptionCode.ENTITY_MALFORMED,
}),
);
});
});
describe('nullable relations', () => {
it('should resolve a nullable foreign key to null when its id is not defined', () => {
const result = resolveManyToOneRelationIdsToUniversalIdentifiers(
buildArgs({
metadataName: 'viewField',
entity: {
id: 'view-field-id-1',
applicationId: 'app-id-1',
fieldMetadataId: 'field-id-1',
viewId: 'view-id-1',
viewFieldGroupId: null,
},
applicationIdToUniversalIdentifierMap: new Map([
['app-id-1', 'app-ui-1'],
]),
fieldMetadataIdToUniversalIdentifierMap: new Map([
['field-id-1', 'field-ui-1'],
]),
viewIdToUniversalIdentifierMap: new Map([['view-id-1', 'view-ui-1']]),
viewFieldGroupIdToUniversalIdentifierMap: new Map(),
}),
);
expect(result).toEqual({
applicationUniversalIdentifier: 'app-ui-1',
fieldMetadataUniversalIdentifier: 'field-ui-1',
viewUniversalIdentifier: 'view-ui-1',
viewFieldGroupUniversalIdentifier: null,
});
});
it('should throw when a nullable foreign key has an id that is missing from the map', () => {
expect(() =>
resolveManyToOneRelationIdsToUniversalIdentifiers(
buildArgs({
metadataName: 'viewField',
entity: {
id: 'view-field-id-1',
applicationId: 'app-id-1',
fieldMetadataId: 'field-id-1',
viewId: 'view-id-1',
viewFieldGroupId: 'non-existent-view-field-group-id',
},
applicationIdToUniversalIdentifierMap: new Map([
['app-id-1', 'app-ui-1'],
]),
fieldMetadataIdToUniversalIdentifierMap: new Map([
['field-id-1', 'field-ui-1'],
]),
viewIdToUniversalIdentifierMap: new Map([
['view-id-1', 'view-ui-1'],
]),
viewFieldGroupIdToUniversalIdentifierMap: new Map(),
}),
),
).toThrow(
expect.objectContaining({
code: FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
}),
);
});
});
});
@@ -0,0 +1,130 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { capitalize, isDefined } from 'twenty-shared/utils';
import { ALL_MANY_TO_ONE_METADATA_RELATIONS } from 'src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-relations.constant';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { type FromEntityToFlatEntityArgs } from 'src/engine/workspace-cache/types/from-entity-to-flat-entity-args.type';
type ManyToOneRelationsConfig<T extends AllMetadataName> =
(typeof ALL_MANY_TO_ONE_METADATA_RELATIONS)[T];
// Mirrors ResolvedForeignKeyIds from resolveUniversalRelationIdentifiersToIds but
// keyed on universalForeignKey, so the resolved record is the many-to-one part of a
// flat entity (plus the always-present applicationUniversalIdentifier).
export type ResolvedManyToOneRelationUniversalIdentifiers<
T extends AllMetadataName,
> = {
[K in keyof ManyToOneRelationsConfig<T> as ManyToOneRelationsConfig<T>[K] extends {
universalForeignKey: infer UniversalForeignKey extends string;
}
? UniversalForeignKey
: never]: ManyToOneRelationsConfig<T>[K] extends { isNullable: true }
? string | null
: string;
} & {
applicationUniversalIdentifier: string;
};
export const resolveManyToOneRelationIdsToUniversalIdentifiers = <
T extends AllMetadataName,
>({
metadataName,
entity,
...idToUniversalIdentifierMaps
}: {
metadataName: T;
} & FromEntityToFlatEntityArgs<T>): ResolvedManyToOneRelationUniversalIdentifiers<T> => {
const readEntityForeignKey = (
propertyName: string,
): string | null | undefined =>
entity[propertyName as keyof typeof entity] as string | null | undefined;
const entityId = readEntityForeignKey('id');
const resolvedUniversalIdentifierByForeignKey: Record<string, string | null> =
{};
const applicationId = readEntityForeignKey('applicationId');
const applicationUniversalIdentifier = isDefined(applicationId)
? idToUniversalIdentifierMaps.applicationIdToUniversalIdentifierMap.get(
applicationId,
)
: undefined;
if (!isDefined(applicationUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Application with id ${applicationId} not found for ${metadataName} ${entityId}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
resolvedUniversalIdentifierByForeignKey.applicationUniversalIdentifier =
applicationUniversalIdentifier;
const relationEntries = ALL_MANY_TO_ONE_METADATA_RELATIONS[metadataName];
for (const relationPropertyName of Object.keys(relationEntries)) {
const relationEntry = relationEntries[
relationPropertyName as keyof typeof relationEntries
] as {
foreignKey: string;
metadataName: AllMetadataName;
isNullable: boolean;
universalForeignKey: string;
} | null;
if (!isDefined(relationEntry)) {
continue;
}
const {
foreignKey,
metadataName: targetMetadataName,
universalForeignKey,
isNullable,
} = relationEntry;
const foreignKeyId = readEntityForeignKey(foreignKey);
if (!isDefined(foreignKeyId)) {
if (isNullable) {
resolvedUniversalIdentifierByForeignKey[universalForeignKey] = null;
continue;
}
throw new FlatEntityMapsException(
`Missing non-nullable foreign key ${foreignKey} on ${metadataName} ${entityId} (relation to ${targetMetadataName})`,
FlatEntityMapsExceptionCode.ENTITY_MALFORMED,
);
}
const mapKey = `${targetMetadataName}IdToUniversalIdentifierMap`;
const targetIdToUniversalIdentifierMap = idToUniversalIdentifierMaps[
mapKey as keyof typeof idToUniversalIdentifierMaps
] as unknown as Map<string, string> | undefined;
if (!isDefined(targetIdToUniversalIdentifierMap)) {
throw new FlatEntityMapsException(
`Missing ${mapKey} when resolving ${metadataName} ${entityId}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
const universalIdentifier =
targetIdToUniversalIdentifierMap.get(foreignKeyId);
if (!isDefined(universalIdentifier)) {
throw new FlatEntityMapsException(
`${capitalize(targetMetadataName)} with id ${foreignKeyId} not found for ${metadataName} ${entityId}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
resolvedUniversalIdentifierByForeignKey[universalForeignKey] =
universalIdentifier;
}
return resolvedUniversalIdentifierByForeignKey as ResolvedManyToOneRelationUniversalIdentifiers<T>;
};