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>
This commit is contained in:
Paul Rastoin
2026-02-09 19:02:38 +01:00
committed by GitHub
parent 2a76e1791e
commit 8c951d3623
366 changed files with 10036 additions and 6606 deletions
@@ -1,305 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect a created entity 1`] = `
{
"createdFlatEntityMaps": {
"byUniversalIdentifier": {
"universal-identifier-1": {
"applicationId": "application-id-1",
"applicationUniversalIdentifier": "application-universal-identifier-1",
"calendarViewIds": [],
"calendarViewUniversalIdentifiers": [],
"createdAt": "2024-01-01T00:00:00.000Z",
"defaultValue": null,
"description": "default flat field metadata description",
"icon": "icon",
"id": "field-id-1",
"isActive": true,
"isCustom": true,
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"kanbanAggregateOperationViewIds": [],
"kanbanAggregateOperationViewUniversalIdentifiers": [],
"label": "flat field metadata label",
"mainGroupByFieldMetadataViewIds": [],
"mainGroupByFieldMetadataViewUniversalIdentifiers": [],
"morphId": null,
"name": "flatFieldMetadataName",
"objectMetadataId": "object-metadata-id-1",
"objectMetadataUniversalIdentifier": "object-metadata-universal-identifier-1",
"options": null,
"relationTargetFieldMetadataId": null,
"relationTargetFieldMetadataUniversalIdentifier": null,
"relationTargetObjectMetadataId": null,
"relationTargetObjectMetadataUniversalIdentifier": null,
"settings": null,
"standardOverrides": null,
"type": "TEXT",
"universalIdentifier": "universal-identifier-1",
"universalSettings": null,
"updatedAt": "2024-01-01T00:00:00.000Z",
"viewFieldIds": [],
"viewFieldUniversalIdentifiers": [],
"viewFilterIds": [],
"viewFilterUniversalIdentifiers": [],
"workspaceId": "workspace-id-1",
},
},
"universalIdentifierById": {
"field-id-1": "universal-identifier-1",
},
"universalIdentifiersByApplicationId": {
"application-id-1": [
"universal-identifier-1",
],
},
},
"deletedFlatEntityMaps": {
"byUniversalIdentifier": {},
"universalIdentifierById": {},
"universalIdentifiersByApplicationId": {},
},
"updatedFlatEntityMaps": {
"byUniversalIdentifier": {},
},
}
`;
exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect a deleted entity 1`] = `
{
"createdFlatEntityMaps": {
"byUniversalIdentifier": {},
"universalIdentifierById": {},
"universalIdentifiersByApplicationId": {},
},
"deletedFlatEntityMaps": {
"byUniversalIdentifier": {
"universal-identifier-1": {
"applicationId": "application-id-1",
"applicationUniversalIdentifier": "application-universal-identifier-1",
"calendarViewIds": [],
"calendarViewUniversalIdentifiers": [],
"createdAt": "2024-01-01T00:00:00.000Z",
"defaultValue": null,
"description": "default flat field metadata description",
"icon": "icon",
"id": "field-id-1",
"isActive": true,
"isCustom": true,
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"kanbanAggregateOperationViewIds": [],
"kanbanAggregateOperationViewUniversalIdentifiers": [],
"label": "flat field metadata label",
"mainGroupByFieldMetadataViewIds": [],
"mainGroupByFieldMetadataViewUniversalIdentifiers": [],
"morphId": null,
"name": "flatFieldMetadataName",
"objectMetadataId": "object-metadata-id-1",
"objectMetadataUniversalIdentifier": "object-metadata-universal-identifier-1",
"options": null,
"relationTargetFieldMetadataId": null,
"relationTargetFieldMetadataUniversalIdentifier": null,
"relationTargetObjectMetadataId": null,
"relationTargetObjectMetadataUniversalIdentifier": null,
"settings": null,
"standardOverrides": null,
"type": "TEXT",
"universalIdentifier": "universal-identifier-1",
"universalSettings": null,
"updatedAt": "2024-01-01T00:00:00.000Z",
"viewFieldIds": [],
"viewFieldUniversalIdentifiers": [],
"viewFilterIds": [],
"viewFilterUniversalIdentifiers": [],
"workspaceId": "workspace-id-1",
},
},
"universalIdentifierById": {
"field-id-1": "universal-identifier-1",
},
"universalIdentifiersByApplicationId": {
"application-id-1": [
"universal-identifier-1",
],
},
},
"updatedFlatEntityMaps": {
"byUniversalIdentifier": {},
},
}
`;
exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect an updated entity 1`] = `
{
"createdFlatEntityMaps": {
"byUniversalIdentifier": {},
"universalIdentifierById": {},
"universalIdentifiersByApplicationId": {},
},
"deletedFlatEntityMaps": {
"byUniversalIdentifier": {},
"universalIdentifierById": {},
"universalIdentifiersByApplicationId": {},
},
"updatedFlatEntityMaps": {
"byUniversalIdentifier": {
"universal-identifier-1": {
"id": "field-id-1",
"update": {
"isActive": true,
},
},
},
},
}
`;
exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should detect created, deleted and updated entities 1`] = `
{
"createdFlatEntityMaps": {
"byUniversalIdentifier": {
"universal-identifier-3": {
"applicationId": "application-id-1",
"applicationUniversalIdentifier": "application-universal-identifier-1",
"calendarViewIds": [],
"calendarViewUniversalIdentifiers": [],
"createdAt": "2024-01-01T00:00:00.000Z",
"defaultValue": null,
"description": "default flat field metadata description",
"icon": "icon",
"id": "field-id-3",
"isActive": true,
"isCustom": true,
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"kanbanAggregateOperationViewIds": [],
"kanbanAggregateOperationViewUniversalIdentifiers": [],
"label": "flat field metadata label",
"mainGroupByFieldMetadataViewIds": [],
"mainGroupByFieldMetadataViewUniversalIdentifiers": [],
"morphId": null,
"name": "flatFieldMetadataName",
"objectMetadataId": "object-metadata-id-1",
"objectMetadataUniversalIdentifier": "object-metadata-universal-identifier-1",
"options": null,
"relationTargetFieldMetadataId": null,
"relationTargetFieldMetadataUniversalIdentifier": null,
"relationTargetObjectMetadataId": null,
"relationTargetObjectMetadataUniversalIdentifier": null,
"settings": null,
"standardOverrides": null,
"type": "TEXT",
"universalIdentifier": "universal-identifier-3",
"universalSettings": null,
"updatedAt": "2024-01-01T00:00:00.000Z",
"viewFieldIds": [],
"viewFieldUniversalIdentifiers": [],
"viewFilterIds": [],
"viewFilterUniversalIdentifiers": [],
"workspaceId": "workspace-id-1",
},
},
"universalIdentifierById": {
"field-id-3": "universal-identifier-3",
},
"universalIdentifiersByApplicationId": {
"application-id-1": [
"universal-identifier-3",
],
},
},
"deletedFlatEntityMaps": {
"byUniversalIdentifier": {
"universal-identifier-2": {
"applicationId": "application-id-1",
"applicationUniversalIdentifier": "application-universal-identifier-1",
"calendarViewIds": [],
"calendarViewUniversalIdentifiers": [],
"createdAt": "2024-01-01T00:00:00.000Z",
"defaultValue": null,
"description": "default flat field metadata description",
"icon": "icon",
"id": "field-id-2",
"isActive": true,
"isCustom": true,
"isLabelSyncedWithName": false,
"isNullable": true,
"isSystem": false,
"isUIReadOnly": false,
"isUnique": false,
"kanbanAggregateOperationViewIds": [],
"kanbanAggregateOperationViewUniversalIdentifiers": [],
"label": "flat field metadata label",
"mainGroupByFieldMetadataViewIds": [],
"mainGroupByFieldMetadataViewUniversalIdentifiers": [],
"morphId": null,
"name": "flatFieldMetadataName",
"objectMetadataId": "object-metadata-id-1",
"objectMetadataUniversalIdentifier": "object-metadata-universal-identifier-1",
"options": null,
"relationTargetFieldMetadataId": null,
"relationTargetFieldMetadataUniversalIdentifier": null,
"relationTargetObjectMetadataId": null,
"relationTargetObjectMetadataUniversalIdentifier": null,
"settings": null,
"standardOverrides": null,
"type": "TEXT",
"universalIdentifier": "universal-identifier-2",
"universalSettings": null,
"updatedAt": "2024-01-01T00:00:00.000Z",
"viewFieldIds": [],
"viewFieldUniversalIdentifiers": [],
"viewFilterIds": [],
"viewFilterUniversalIdentifiers": [],
"workspaceId": "workspace-id-1",
},
},
"universalIdentifierById": {
"field-id-2": "universal-identifier-2",
},
"universalIdentifiersByApplicationId": {
"application-id-1": [
"universal-identifier-2",
],
},
},
"updatedFlatEntityMaps": {
"byUniversalIdentifier": {
"universal-identifier-1": {
"id": "field-id-1",
"update": {
"isActive": false,
},
},
},
},
}
`;
exports[`flatEntityDeletedCreatedUpdatedMatrixDispatcher It should not detect deleted entities when inferDeletionFromMissingEntities is false 1`] = `
{
"createdFlatEntityMaps": {
"byUniversalIdentifier": {},
"universalIdentifierById": {},
"universalIdentifiersByApplicationId": {},
},
"deletedFlatEntityMaps": {
"byUniversalIdentifier": {},
"universalIdentifierById": {},
"universalIdentifiersByApplicationId": {},
},
"updatedFlatEntityMaps": {
"byUniversalIdentifier": {},
},
}
`;
@@ -1,232 +0,0 @@
import {
eachTestingContextFilter,
type EachTestingContext,
} from 'twenty-shared/testing';
import { FieldMetadataType, type FromTo } from 'twenty-shared/types';
import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type';
import { getFlatFieldMetadataMock } from 'src/engine/metadata-modules/flat-field-metadata/__mocks__/get-flat-field-metadata.mock';
import { flatEntityDeletedCreatedUpdatedMatrixDispatcher } from 'src/engine/workspace-manager/workspace-migration/utils/flat-entity-deleted-created-updated-matrix-dispatcher.util';
import { type WorkspaceMigrationBuilderOptions } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-builder-options.type';
type TestContext = FromTo<MetadataFlatEntity<'fieldMetadata'>[]> & {
metadataName: 'fieldMetadata';
buildOptions: WorkspaceMigrationBuilderOptions;
};
describe('flatEntityDeletedCreatedUpdatedMatrixDispatcher', () => {
const testCases = [
{
title: 'It should detect a created entity',
context: {
from: [],
to: [
getFlatFieldMetadataMock({
objectMetadataId: 'object-metadata-id-1',
type: FieldMetadataType.TEXT,
universalIdentifier: 'universal-identifier-1',
id: 'field-id-1',
workspaceId: 'workspace-id-1',
applicationId: 'application-id-1',
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
applicationUniversalIdentifier:
'application-universal-identifier-1',
objectMetadataUniversalIdentifier:
'object-metadata-universal-identifier-1',
}),
],
metadataName: 'fieldMetadata',
buildOptions: {
inferDeletionFromMissingEntities: { fieldMetadata: false },
isSystemBuild: false,
},
},
},
{
title: 'It should detect a deleted entity',
context: {
from: [
getFlatFieldMetadataMock({
objectMetadataId: 'object-metadata-id-1',
type: FieldMetadataType.TEXT,
universalIdentifier: 'universal-identifier-1',
id: 'field-id-1',
workspaceId: 'workspace-id-1',
applicationId: 'application-id-1',
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
applicationUniversalIdentifier:
'application-universal-identifier-1',
objectMetadataUniversalIdentifier:
'object-metadata-universal-identifier-1',
}),
],
to: [],
metadataName: 'fieldMetadata',
buildOptions: {
inferDeletionFromMissingEntities: { fieldMetadata: true },
isSystemBuild: false,
},
},
},
{
title: 'It should detect an updated entity',
context: {
from: [
getFlatFieldMetadataMock({
objectMetadataId: 'object-metadata-id-1',
type: FieldMetadataType.TEXT,
universalIdentifier: 'universal-identifier-1',
id: 'field-id-1',
workspaceId: 'workspace-id-1',
applicationId: 'application-id-1',
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
isActive: false,
applicationUniversalIdentifier:
'application-universal-identifier-1',
objectMetadataUniversalIdentifier:
'object-metadata-universal-identifier-1',
}),
],
to: [
getFlatFieldMetadataMock({
objectMetadataId: 'object-metadata-id-1',
type: FieldMetadataType.TEXT,
universalIdentifier: 'universal-identifier-1',
id: 'field-id-1',
workspaceId: 'workspace-id-1',
applicationId: 'application-id-1',
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
isActive: true,
applicationUniversalIdentifier:
'application-universal-identifier-1',
objectMetadataUniversalIdentifier:
'object-metadata-universal-identifier-1',
}),
],
metadataName: 'fieldMetadata',
buildOptions: {
inferDeletionFromMissingEntities: { fieldMetadata: false },
isSystemBuild: false,
},
},
},
{
title: 'It should detect created, deleted and updated entities',
context: {
from: [
getFlatFieldMetadataMock({
objectMetadataId: 'object-metadata-id-1',
type: FieldMetadataType.TEXT,
universalIdentifier: 'universal-identifier-1',
id: 'field-id-1',
workspaceId: 'workspace-id-1',
applicationId: 'application-id-1',
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
applicationUniversalIdentifier:
'application-universal-identifier-1',
objectMetadataUniversalIdentifier:
'object-metadata-universal-identifier-1',
isActive: true,
}),
getFlatFieldMetadataMock({
objectMetadataId: 'object-metadata-id-1',
type: FieldMetadataType.TEXT,
universalIdentifier: 'universal-identifier-2',
id: 'field-id-2',
workspaceId: 'workspace-id-1',
applicationId: 'application-id-1',
createdAt: '2024-01-01T00:00:00.000Z',
updatedAt: '2024-01-01T00:00:00.000Z',
applicationUniversalIdentifier:
'application-universal-identifier-1',
objectMetadataUniversalIdentifier:
'object-metadata-universal-identifier-1',
}),
],
to: [
getFlatFieldMetadataMock({
objectMetadataId: 'object-metadata-id-1',
type: FieldMetadataType.TEXT,
universalIdentifier: 'universal-identifier-1',
id: 'field-id-1',
workspaceId: 'workspace-id-1',
applicationId: 'application-id-1',
createdAt: '2024-01-01T00:00:00.000Z',
applicationUniversalIdentifier:
'application-universal-identifier-1',
objectMetadataUniversalIdentifier:
'object-metadata-universal-identifier-1',
updatedAt: '2024-01-01T00:00:00.000Z',
isActive: false,
}),
getFlatFieldMetadataMock({
objectMetadataId: 'object-metadata-id-1',
type: FieldMetadataType.TEXT,
universalIdentifier: 'universal-identifier-3',
id: 'field-id-3',
workspaceId: 'workspace-id-1',
applicationId: 'application-id-1',
createdAt: '2024-01-01T00:00:00.000Z',
applicationUniversalIdentifier:
'application-universal-identifier-1',
objectMetadataUniversalIdentifier:
'object-metadata-universal-identifier-1',
updatedAt: '2024-01-01T00:00:00.000Z',
}),
],
metadataName: 'fieldMetadata',
buildOptions: {
inferDeletionFromMissingEntities: { fieldMetadata: true },
isSystemBuild: false,
},
},
},
{
title:
'It should not detect deleted entities when inferDeletionFromMissingEntities is false',
context: {
from: [
getFlatFieldMetadataMock({
objectMetadataId: 'object-metadata-id-1',
type: FieldMetadataType.TEXT,
universalIdentifier: 'universal-identifier-1',
id: 'field-id-1',
workspaceId: 'workspace-id-1',
applicationId: 'application-id-1',
createdAt: '2024-01-01T00:00:00.000Z',
applicationUniversalIdentifier:
'application-universal-identifier-1',
objectMetadataUniversalIdentifier:
'object-metadata-universal-identifier-1',
updatedAt: '2024-01-01T00:00:00.000Z',
}),
],
to: [],
metadataName: 'fieldMetadata',
buildOptions: {
inferDeletionFromMissingEntities: { fieldMetadata: false },
isSystemBuild: false,
},
},
},
] as EachTestingContext<TestContext>[];
test.each(eachTestingContextFilter(testCases))(
'$title',
({ context: { from, to, metadataName, buildOptions } }) => {
const result = flatEntityDeletedCreatedUpdatedMatrixDispatcher({
from: from as any,
to: to as any,
metadataName,
buildOptions,
});
expect(result).toMatchSnapshot();
},
);
});
@@ -1,42 +1,36 @@
import { type EachTestingContext } from 'twenty-shared/testing';
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';
import { type UniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-maps.type';
import {
type CircularDependencyValidationResult,
validateFlatEntityCircularDependency,
} from 'src/engine/workspace-manager/workspace-migration/utils/validate-flat-entity-circular-dependency.util';
type TestFlatEntity = SyncableFlatEntity & {
parentId: string | null;
type TestFlatEntity = UniversalSyncableFlatEntity & {
parentUniversalIdentifier: string | null;
};
const createFlatEntityMaps = (
const createUniversalFlatEntityMaps = (
entities: TestFlatEntity[],
): FlatEntityMaps<TestFlatEntity> => ({
): UniversalFlatEntityMaps<TestFlatEntity> => ({
byUniversalIdentifier: Object.fromEntries(
entities.map((entity) => [entity.universalIdentifier, entity]),
),
universalIdentifierById: Object.fromEntries(
entities.map((entity) => [entity.id, entity.universalIdentifier]),
),
universalIdentifiersByApplicationId: {},
});
const createTestEntity = (
id: string,
parentId: string | null = null,
universalIdentifier: string,
parentUniversalIdentifier: string | null = null,
): TestFlatEntity => ({
id,
parentId,
universalIdentifier: id,
applicationId: 'test-app',
workspaceId: 'test-workspace',
parentUniversalIdentifier,
universalIdentifier,
applicationUniversalIdentifier: 'test-app',
});
type TestContext = {
flatEntityId: string;
flatEntityParentId: string;
flatEntityUniversalIdentifier: string;
flatEntityParentUniversalIdentifier: string;
maxDepth?: number;
entities: TestFlatEntity[];
expected: CircularDependencyValidationResult;
@@ -48,8 +42,8 @@ const testCases: ValidateCircularDependencyTestCase[] = [
{
title: 'should fail with self_reference when entity is its own parent',
context: {
flatEntityId: 'entity-1',
flatEntityParentId: 'entity-1',
flatEntityUniversalIdentifier: 'entity-1',
flatEntityParentUniversalIdentifier: 'entity-1',
entities: [],
expected: {
status: 'fail',
@@ -60,8 +54,8 @@ const testCases: ValidateCircularDependencyTestCase[] = [
{
title: 'should succeed with depth 2 when parent has no parent',
context: {
flatEntityId: 'entity-child',
flatEntityParentId: 'entity-parent',
flatEntityUniversalIdentifier: 'entity-child',
flatEntityParentUniversalIdentifier: 'entity-parent',
entities: [createTestEntity('entity-parent', null)],
expected: {
status: 'success',
@@ -72,8 +66,8 @@ const testCases: ValidateCircularDependencyTestCase[] = [
{
title: 'should succeed with depth 3 when parent has a grandparent',
context: {
flatEntityId: 'entity-child',
flatEntityParentId: 'entity-parent',
flatEntityUniversalIdentifier: 'entity-child',
flatEntityParentUniversalIdentifier: 'entity-parent',
entities: [
createTestEntity('entity-grandparent', null),
createTestEntity('entity-parent', 'entity-grandparent'),
@@ -88,8 +82,8 @@ const testCases: ValidateCircularDependencyTestCase[] = [
title:
'should fail with circular_dependency when parent points back to entity',
context: {
flatEntityId: 'entity-a',
flatEntityParentId: 'entity-b',
flatEntityUniversalIdentifier: 'entity-a',
flatEntityParentUniversalIdentifier: 'entity-b',
entities: [createTestEntity('entity-b', 'entity-a')],
expected: {
status: 'fail',
@@ -102,8 +96,8 @@ const testCases: ValidateCircularDependencyTestCase[] = [
title:
'should fail with circular_dependency when chain creates a cycle (A -> B -> C -> A)',
context: {
flatEntityId: 'entity-a',
flatEntityParentId: 'entity-b',
flatEntityUniversalIdentifier: 'entity-a',
flatEntityParentUniversalIdentifier: 'entity-b',
entities: [
createTestEntity('entity-b', 'entity-c'),
createTestEntity('entity-c', 'entity-a'),
@@ -119,8 +113,8 @@ const testCases: ValidateCircularDependencyTestCase[] = [
title:
'should fail with circular_dependency when ancestors form a cycle among themselves',
context: {
flatEntityId: 'entity-new',
flatEntityParentId: 'entity-a',
flatEntityUniversalIdentifier: 'entity-new',
flatEntityParentUniversalIdentifier: 'entity-a',
entities: [
createTestEntity('entity-a', 'entity-b'),
createTestEntity('entity-b', 'entity-a'),
@@ -135,8 +129,8 @@ const testCases: ValidateCircularDependencyTestCase[] = [
{
title: 'should succeed when parent does not exist in maps',
context: {
flatEntityId: 'entity-child',
flatEntityParentId: 'non-existent-parent',
flatEntityUniversalIdentifier: 'entity-child',
flatEntityParentUniversalIdentifier: 'non-existent-parent',
entities: [],
expected: {
status: 'success',
@@ -147,8 +141,8 @@ const testCases: ValidateCircularDependencyTestCase[] = [
{
title: 'should fail with max_depth_exceeded when depth exceeds maxDepth',
context: {
flatEntityId: 'entity-child',
flatEntityParentId: 'entity-parent',
flatEntityUniversalIdentifier: 'entity-child',
flatEntityParentUniversalIdentifier: 'entity-parent',
maxDepth: 2,
entities: [
createTestEntity('entity-grandparent', null),
@@ -164,8 +158,8 @@ const testCases: ValidateCircularDependencyTestCase[] = [
{
title: 'should succeed when depth equals maxDepth',
context: {
flatEntityId: 'entity-child',
flatEntityParentId: 'entity-parent',
flatEntityUniversalIdentifier: 'entity-child',
flatEntityParentUniversalIdentifier: 'entity-parent',
maxDepth: 3,
entities: [
createTestEntity('entity-grandparent', null),
@@ -180,8 +174,8 @@ const testCases: ValidateCircularDependencyTestCase[] = [
{
title: 'should succeed with depth 2 when maxDepth is 2 and parent is root',
context: {
flatEntityId: 'entity-child',
flatEntityParentId: 'entity-parent',
flatEntityUniversalIdentifier: 'entity-child',
flatEntityParentUniversalIdentifier: 'entity-parent',
maxDepth: 2,
entities: [createTestEntity('entity-parent', null)],
expected: {
@@ -194,8 +188,8 @@ const testCases: ValidateCircularDependencyTestCase[] = [
title:
'should check max depth before circular dependency to prevent infinite loop',
context: {
flatEntityId: 'entity-new',
flatEntityParentId: 'entity-a',
flatEntityUniversalIdentifier: 'entity-new',
flatEntityParentUniversalIdentifier: 'entity-a',
maxDepth: 2,
entities: [
createTestEntity('entity-a', 'entity-b'),
@@ -213,8 +207,8 @@ const testCases: ValidateCircularDependencyTestCase[] = [
{
title: 'should handle deep valid hierarchy without maxDepth',
context: {
flatEntityId: 'entity-new',
flatEntityParentId: 'entity-1',
flatEntityUniversalIdentifier: 'entity-new',
flatEntityParentUniversalIdentifier: 'entity-1',
entities: [
createTestEntity('entity-5', null),
createTestEntity('entity-4', 'entity-5'),
@@ -232,13 +226,14 @@ const testCases: ValidateCircularDependencyTestCase[] = [
describe('validateFlatEntityCircularDependency', () => {
test.each(testCases)('$title', ({ context }) => {
const flatEntityMaps = createFlatEntityMaps(context.entities);
const flatEntityMaps = createUniversalFlatEntityMaps(context.entities);
const result = validateFlatEntityCircularDependency({
flatEntityId: context.flatEntityId,
flatEntityParentId: context.flatEntityParentId,
flatEntityUniversalIdentifier: context.flatEntityUniversalIdentifier,
flatEntityParentUniversalIdentifier:
context.flatEntityParentUniversalIdentifier,
maxDepth: context.maxDepth,
parentIdKey: 'parentId',
parentUniversalIdentifierKey: 'parentUniversalIdentifier',
flatEntityMaps,
});
@@ -0,0 +1,39 @@
import { isDefined } from 'twenty-shared/utils';
import {
FlatEntityMapsException,
FlatEntityMapsExceptionCode,
} from 'src/engine/metadata-modules/flat-entity/exceptions/flat-entity-maps.exception';
import { type SyncableFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
import { type UniversalSyncableFlatEntity } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-from.type';
import { type UniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-maps.type';
type AddUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrowArgs<
T extends SyncableFlatEntity | UniversalSyncableFlatEntity,
> = {
universalFlatEntity: T;
universalFlatEntityMapsToMutate: UniversalFlatEntityMaps<T>;
};
export const addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow =
<T extends SyncableFlatEntity | UniversalSyncableFlatEntity>({
universalFlatEntity: flatEntity,
universalFlatEntityMapsToMutate,
}: AddUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrowArgs<T>): void => {
if (
isDefined(
universalFlatEntityMapsToMutate.byUniversalIdentifier[
flatEntity.universalIdentifier
],
)
) {
throw new FlatEntityMapsException(
'addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow: flat entity to add already exists',
FlatEntityMapsExceptionCode.ENTITY_ALREADY_EXISTS,
);
}
universalFlatEntityMapsToMutate.byUniversalIdentifier[
flatEntity.universalIdentifier
] = flatEntity;
};
@@ -0,0 +1,66 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type FlatEntityToCreateDeleteUpdate } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-to-create-delete-update.type';
import { getSubUniversalFlatEntityByUniversalIdentifiersMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/get-sub-universal-flat-entity-by-universal-identifiers-maps-or-throw.util';
import { type MetadataUniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/metadata-universal-flat-entity-maps.type';
import { addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/add-universal-flat-entity-to-universal-flat-entity-maps-through-mutation-or-throw.util';
import { deleteUniversalFlatEntityFromUniversalFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/delete-universal-flat-entity-from-universal-flat-entity-maps-through-mutation-or-throw.util';
import { replaceUniversalFlatEntityInUniversalFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/replace-universal-flat-entity-in-universal-flat-entity-maps-through-mutation-or-throw.util';
export type ComputeUniversalFlatEntityMapsFromToArgs<
T extends AllMetadataName,
> = {
flatEntityMaps: MetadataUniversalFlatEntityMaps<T>;
} & FlatEntityToCreateDeleteUpdate<T>;
export const computeUniversalFlatEntityMapsFromTo = <
T extends AllMetadataName,
>({
flatEntityMaps,
flatEntityToCreate,
flatEntityToDelete,
flatEntityToUpdate,
}: ComputeUniversalFlatEntityMapsFromToArgs<T>): {
from: MetadataUniversalFlatEntityMaps<T>;
to: MetadataUniversalFlatEntityMaps<T>;
} => {
const fromFlatEntityMaps =
flatEntityToDelete.length > 0
? getSubUniversalFlatEntityByUniversalIdentifiersMapsOrThrow({
universalIdentifiers: [
...flatEntityToDelete,
...flatEntityToUpdate,
].map(({ universalIdentifier }) => universalIdentifier),
universalFlatEntityMaps: flatEntityMaps,
})
: flatEntityMaps;
const toFlatEntityMaps: MetadataUniversalFlatEntityMaps<T> =
structuredClone(fromFlatEntityMaps);
for (const flatEntity of flatEntityToDelete) {
deleteUniversalFlatEntityFromUniversalFlatEntityMapsThroughMutationOrThrow({
universalIdentifierToDelete: flatEntity.universalIdentifier,
universalFlatEntityMapsToMutate: toFlatEntityMaps,
});
}
for (const flatEntity of flatEntityToUpdate) {
replaceUniversalFlatEntityInUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: flatEntity,
universalFlatEntityMapsToMutate: toFlatEntityMaps,
});
}
for (const flatEntity of flatEntityToCreate) {
addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow({
universalFlatEntity: flatEntity,
universalFlatEntityMapsToMutate: toFlatEntityMaps,
});
}
return {
from: fromFlatEntityMaps,
to: toFlatEntityMaps,
};
};
@@ -1,117 +0,0 @@
import { type AllMetadataName } from 'twenty-shared/metadata';
import { type FromTo } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY } from 'src/engine/metadata-modules/flat-entity/constant/all-flat-entity-properties-to-compare-and-stringify.constant';
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
import { type FlatEntityPropertiesToCompare } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-properties-to-compare.type';
import { type FlatEntityPropertiesToStringify } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-properties-to-stringify.type';
import { type FlatEntityUpdate } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-properties-updates.type';
import { type MetadataFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity-maps.type';
import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type';
import { compareTwoFlatEntity } from 'src/engine/metadata-modules/flat-entity/utils/compare-two-flat-entity.util';
import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util';
import { shouldInferDeletionFromMissingEntities } from 'src/engine/workspace-manager/workspace-migration/utils/should-infer-deletion-from-missing-entities.util';
import { type WorkspaceMigrationBuilderOptions } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/types/workspace-migration-builder-options.type';
export type DeletedCreatedUpdatedMatrix<T extends AllMetadataName> = {
createdFlatEntityMaps: MetadataFlatEntityMaps<T>;
deletedFlatEntityMaps: MetadataFlatEntityMaps<T>;
updatedFlatEntityMaps: {
byUniversalIdentifier: Record<
string,
{
update: FlatEntityUpdate<T>;
// TMP remove when maps is universal based
id: string;
}
>;
};
};
export type UniversalIdentifierItem = {
universalIdentifier: string;
};
type FlatEntityDeletedCreatedUpdatedMatrixDispatcherArgs<
T extends AllMetadataName,
> = FromTo<MetadataFlatEntity<T>[]> & {
metadataName: T;
buildOptions: WorkspaceMigrationBuilderOptions;
};
export const flatEntityDeletedCreatedUpdatedMatrixDispatcher = <
T extends AllMetadataName,
>({
from,
to,
metadataName,
buildOptions,
}: FlatEntityDeletedCreatedUpdatedMatrixDispatcherArgs<T>): DeletedCreatedUpdatedMatrix<T> => {
const initialDispatcher: DeletedCreatedUpdatedMatrix<T> = {
createdFlatEntityMaps: createEmptyFlatEntityMaps(),
deletedFlatEntityMaps: createEmptyFlatEntityMaps(),
updatedFlatEntityMaps: { byUniversalIdentifier: {} },
};
const fromMap = new Map(from.map((obj) => [obj.universalIdentifier, obj]));
const toMap = new Map(to.map((obj) => [obj.universalIdentifier, obj]));
if (shouldInferDeletionFromMissingEntities({ buildOptions, metadataName })) {
for (const [universalIdentifier, fromEntity] of fromMap) {
if (toMap.has(universalIdentifier)) {
continue;
}
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: fromEntity,
flatEntityMapsToMutate: initialDispatcher.deletedFlatEntityMaps,
});
}
}
for (const [universalIdentifier, toFlatEntity] of toMap) {
if (fromMap.has(universalIdentifier)) {
continue;
}
addFlatEntityToFlatEntityMapsThroughMutationOrThrow({
flatEntity: toFlatEntity,
flatEntityMapsToMutate: initialDispatcher.createdFlatEntityMaps,
});
}
const { propertiesToCompare, propertiesToStringify } =
ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY[metadataName];
for (const [universalIdentifier, fromFlatEntity] of fromMap) {
const toFlatEntity = toMap.get(universalIdentifier);
if (!isDefined(toFlatEntity)) {
continue;
}
const update = compareTwoFlatEntity({
fromFlatEntity,
toFlatEntity,
propertiesToCompare: propertiesToCompare as unknown as Extract<
FlatEntityPropertiesToCompare<T>,
keyof MetadataFlatEntity<T>
>[],
propertiesToStringify: propertiesToStringify as unknown as Extract<
FlatEntityPropertiesToStringify<T>,
keyof MetadataFlatEntity<T>
>[],
});
if (!isDefined(update)) {
continue;
}
initialDispatcher.updatedFlatEntityMaps.byUniversalIdentifier[
fromFlatEntity.universalIdentifier
] = {
id: toFlatEntity.id,
update,
};
}
return initialDispatcher;
};
@@ -1,8 +1,9 @@
import { isDefined } from 'twenty-shared/utils';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
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';
import { type UniversalFlatEntityMaps } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-entity-maps.type';
export type CircularDependencyValidationSuccess = {
status: 'success';
@@ -20,23 +21,23 @@ export type CircularDependencyValidationResult =
| CircularDependencyValidationFailure;
export const validateFlatEntityCircularDependency = <
T extends SyncableFlatEntity,
T extends SyncableFlatEntity | UniversalSyncableFlatEntity,
K extends keyof T,
>({
flatEntityId,
flatEntityParentId,
flatEntityUniversalIdentifier,
flatEntityParentUniversalIdentifier,
maxDepth,
parentIdKey,
parentUniversalIdentifierKey,
flatEntityMaps,
}: {
flatEntityId: string;
flatEntityParentId: string;
flatEntityUniversalIdentifier: string;
flatEntityParentUniversalIdentifier: string;
maxDepth?: number;
parentIdKey: K;
flatEntityMaps: FlatEntityMaps<T>;
parentUniversalIdentifierKey: K;
flatEntityMaps: UniversalFlatEntityMaps<T>;
}): CircularDependencyValidationResult => {
// Direct self-reference check
if (flatEntityId === flatEntityParentId) {
if (flatEntityUniversalIdentifier === flatEntityParentUniversalIdentifier) {
return {
status: 'fail',
reason: 'self_reference',
@@ -45,10 +46,11 @@ export const validateFlatEntityCircularDependency = <
// Traverse ancestor chain to detect cycles and measure depth
const visited = new Set<string>();
let currentParentId: string | null | undefined = flatEntityParentId;
let currentParentUniversalIdentifier: string | null | undefined =
flatEntityParentUniversalIdentifier;
let depth = 1;
while (isDefined(currentParentId)) {
while (isDefined(currentParentUniversalIdentifier)) {
if (isDefined(maxDepth) && depth > maxDepth) {
return {
status: 'fail',
@@ -58,7 +60,7 @@ export const validateFlatEntityCircularDependency = <
}
// Check for circular dependency
if (currentParentId === flatEntityId) {
if (currentParentUniversalIdentifier === flatEntityUniversalIdentifier) {
return {
status: 'fail',
reason: 'circular_dependency',
@@ -67,7 +69,7 @@ export const validateFlatEntityCircularDependency = <
}
// Check for cycle in ancestors (already visited node)
if (visited.has(currentParentId)) {
if (visited.has(currentParentUniversalIdentifier)) {
return {
status: 'fail',
reason: 'circular_dependency',
@@ -75,10 +77,10 @@ export const validateFlatEntityCircularDependency = <
};
}
visited.add(currentParentId);
visited.add(currentParentUniversalIdentifier);
const parentEntity: T | undefined = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: currentParentId,
const parentEntity: T | undefined = findFlatEntityByUniversalIdentifier({
universalIdentifier: currentParentUniversalIdentifier,
flatEntityMaps,
});
@@ -86,11 +88,13 @@ export const validateFlatEntityCircularDependency = <
break;
}
const nextParentId: T[K] = parentEntity[parentIdKey];
const nextParentUniversalIdentifier: T[K] =
parentEntity[parentUniversalIdentifierKey];
currentParentId =
isDefined(nextParentId) && typeof nextParentId === 'string'
? nextParentId
currentParentUniversalIdentifier =
isDefined(nextParentUniversalIdentifier) &&
typeof nextParentUniversalIdentifier === 'string'
? nextParentUniversalIdentifier
: undefined;
depth++;
}