Files
twenty/packages/twenty-server/src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util.ts
T
Paul Rastoin 8c951d3623 Migrate Views-xxx Index Field Object Skill to be fully universal ( all actions and metadata runner and builder ) + all metadata update actions runner (#17687)
# What this PR does
Overall naming `universal` versus `flat` is not always the most updated
and so on
Will make a big cleaning tour after I've finished the whole migration
Migrating all `view` and ( filter fields etc ) `field` `object` `index`
to the universal pattern on all `services`, `builder` and `runner`
levels

## Universal and flat optimistic tooling

`addUniversalFlatEntityToUniversalFlatEntityAndRelatedEntityMapsThroughMutationOrThrow`
and its delete counterpart maintain the consistency of
`UniversalFlatEntityMaps` when an entity is created or removed. Beyond
inserting/removing the entity from its own maps, they walk through
`ALL_UNIVERSAL_METADATA_RELATIONS` to update the **aggregator arrays**
on related parent entities — the add appends the new entity's
`universalIdentifier` to the parent's aggregator (e.g. a new viewField's
identifier gets appended to its parent view's
`viewFieldUniversalIdentifiers`), and the delete filters it out. This
keeps the maps in sync so that diff computations and relation lookups
remain accurate throughout the migration building process.

## ALL_UNIVERSAL_METADATA_RELATIONS
`ALL_UNIVERSAL_METADATA_RELATIONS` is the universal counterpart of
`ALL_METADATA_RELATIONS`. It maps each metadata entity to its
many-to-one and one-to-many relations using universal foreign keys
(`*UniversalIdentifier`) instead of database IDs (`*Id`). This allows
migration actions to reference related entities in a workspace-agnostic
way. Relations that are workspace-specific (e.g. `workspace`,
`dataSource`, `userWorkspace`) are set to `null` and skipped during
resolution.

## `workspaceMigrationCreateIdEnrichment`
Reserved to API metadata ( will be able to validate at app installation
lvl )
- Workspace migration `create` actions now carry an optional `id` (and
`fieldIdByUniversalIdentifier` for object/field actions) so that
caller-provided IDs flow through the entire build-validate-run pipeline.
- New `enrichCreateWorkspaceMigrationActionsWithIds` utility resolves
`universalIdentifier → id` mappings after the builder runs and injects
them into the migration actions before the runner persists entities.
- Runner action handlers use the provided IDs instead of generating new
UUIDs, enabling deterministic entity creation for synchronization
workflows.


## `resolveUniversalUpdateRelationIdentifiersToIds`
`resolveUniversalUpdateRelationIdentifiersToIds` converts universal
identifiers (workspace-agnostic, stable keys) in a migration update
payload into concrete database UUIDs, so the update can be applied to a
specific workspace.

It iterates over the many-to-one relations defined in
`ALL_UNIVERSAL_METADATA_RELATIONS` for the given entity type, replaces
each `*UniversalIdentifier` property with its corresponding `*Id` by
looking up the target entity in `allFlatEntityMaps`, and throws if a
non-null identifier can't be resolved.

Used by all `update` action handlers in
`transpileUniversalActionToFlatAction`, avoiding duplicated resolution
logic across handlers.

## What this PR does not
- Migrating twenty-standard declaration to universal
- Migrating all the inputs transpilers to universal
- Migrating all metadata to be fully universal ( we still need to
de-scope the type of all of them and refactor their validator very close
)

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-02-09 19:02:38 +01:00

134 lines
4.6 KiB
TypeScript

import { t } from '@lingui/core/macro';
import { type AllMetadataName } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import {
ALL_METADATA_RELATIONS,
type MetadataManyToOneRelationConfiguration,
} 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 MetadataManyToOneRelationConfiguration<
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>;
};