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
@@ -1,79 +1,30 @@
import { isDefined } from 'twenty-shared/utils';
import { fromCommandMenuItemOverridesToUniversalOverrides } from 'src/engine/metadata-modules/flat-command-menu-item/utils/from-command-menu-item-overrides-to-universal-overrides.util';
import { type FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-menu-item/types/flat-command-menu-item.type';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { fromCommandMenuItemOverridesToUniversalOverrides } from 'src/engine/metadata-modules/flat-command-menu-item/utils/from-command-menu-item-overrides-to-universal-overrides.util';
import { fromEntityToScalarEntity } from 'src/engine/metadata-modules/flat-entity/utils/from-entity-to-scalar-entity.util';
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';
export const fromCommandMenuItemEntityToFlatCommandMenuItem = ({
entity: commandMenuItemEntity,
applicationIdToUniversalIdentifierMap,
objectMetadataIdToUniversalIdentifierMap,
frontComponentIdToUniversalIdentifierMap,
pageLayoutIdToUniversalIdentifierMap,
}: FromEntityToFlatEntityArgs<'commandMenuItem'>): FlatCommandMenuItem => {
const applicationUniversalIdentifier =
applicationIdToUniversalIdentifierMap.get(
commandMenuItemEntity.applicationId,
);
export const fromCommandMenuItemEntityToFlatCommandMenuItem = (
args: FromEntityToFlatEntityArgs<'commandMenuItem'>,
): FlatCommandMenuItem => {
const {
entity: commandMenuItemEntity,
objectMetadataIdToUniversalIdentifierMap,
pageLayoutIdToUniversalIdentifierMap,
} = args;
if (!isDefined(applicationUniversalIdentifier)) {
throw new FlatEntityMapsException(
`Application with id ${commandMenuItemEntity.applicationId} not found for commandMenuItem ${commandMenuItemEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
const commandMenuItemScalarEntity = fromEntityToScalarEntity({
metadataName: 'commandMenuItem',
entity: commandMenuItemEntity,
});
let availabilityObjectMetadataUniversalIdentifier: string | null = null;
if (isDefined(commandMenuItemEntity.availabilityObjectMetadataId)) {
availabilityObjectMetadataUniversalIdentifier =
objectMetadataIdToUniversalIdentifierMap.get(
commandMenuItemEntity.availabilityObjectMetadataId,
) ?? null;
if (!isDefined(availabilityObjectMetadataUniversalIdentifier)) {
throw new FlatEntityMapsException(
`ObjectMetadata with id ${commandMenuItemEntity.availabilityObjectMetadataId} not found for commandMenuItem ${commandMenuItemEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
}
let frontComponentUniversalIdentifier: string | null = null;
if (isDefined(commandMenuItemEntity.frontComponentId)) {
frontComponentUniversalIdentifier =
frontComponentIdToUniversalIdentifierMap.get(
commandMenuItemEntity.frontComponentId,
) ?? null;
if (!isDefined(frontComponentUniversalIdentifier)) {
throw new FlatEntityMapsException(
`FrontComponent with id ${commandMenuItemEntity.frontComponentId} not found for commandMenuItem ${commandMenuItemEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
}
let pageLayoutUniversalIdentifier: string | null = null;
if (isDefined(commandMenuItemEntity.pageLayoutId)) {
pageLayoutUniversalIdentifier =
pageLayoutIdToUniversalIdentifierMap.get(
commandMenuItemEntity.pageLayoutId,
) ?? null;
if (!isDefined(pageLayoutUniversalIdentifier)) {
throw new FlatEntityMapsException(
`PageLayout with id ${commandMenuItemEntity.pageLayoutId} not found for commandMenuItem ${commandMenuItemEntity.id}`,
FlatEntityMapsExceptionCode.ENTITY_NOT_FOUND,
);
}
}
const relationUniversalIdentifiers =
resolveManyToOneRelationIdsToUniversalIdentifiers({
metadataName: 'commandMenuItem',
...args,
});
const universalOverrides = isDefined(commandMenuItemEntity.overrides)
? fromCommandMenuItemOverridesToUniversalOverrides({
@@ -89,35 +40,8 @@ export const fromCommandMenuItemEntityToFlatCommandMenuItem = ({
: null;
return {
id: commandMenuItemEntity.id,
workflowVersionId: commandMenuItemEntity.workflowVersionId,
frontComponentId: commandMenuItemEntity.frontComponentId,
engineComponentKey: commandMenuItemEntity.engineComponentKey,
label: commandMenuItemEntity.label,
icon: commandMenuItemEntity.icon,
shortLabel: commandMenuItemEntity.shortLabel,
position: commandMenuItemEntity.position,
isPinned: commandMenuItemEntity.isPinned,
payload: commandMenuItemEntity.payload,
hotKeys: commandMenuItemEntity.hotKeys,
availabilityType: commandMenuItemEntity.availabilityType,
availabilityObjectMetadataId:
commandMenuItemEntity.availabilityObjectMetadataId,
workspaceId: commandMenuItemEntity.workspaceId,
universalIdentifier: commandMenuItemEntity.universalIdentifier,
applicationId: commandMenuItemEntity.applicationId,
createdAt: commandMenuItemEntity.createdAt.toISOString(),
updatedAt: commandMenuItemEntity.updatedAt.toISOString(),
applicationUniversalIdentifier,
conditionalAvailabilityExpression:
commandMenuItemEntity.conditionalAvailabilityExpression,
availabilityObjectMetadataUniversalIdentifier,
frontComponentUniversalIdentifier,
pageLayoutId: commandMenuItemEntity.pageLayoutId,
pageLayoutUniversalIdentifier,
isActive: commandMenuItemEntity.isActive,
isSystemSideEffect: commandMenuItemEntity.isSystemSideEffect,
overrides: commandMenuItemEntity.overrides,
...commandMenuItemScalarEntity,
...relationUniversalIdentifiers,
universalOverrides,
};
};