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:
-305
@@ -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": {},
|
||||
},
|
||||
}
|
||||
`;
|
||||
-232
@@ -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();
|
||||
},
|
||||
);
|
||||
});
|
||||
+42
-47
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
+39
@@ -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;
|
||||
};
|
||||
+66
@@ -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,
|
||||
};
|
||||
};
|
||||
-117
@@ -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;
|
||||
};
|
||||
+26
-22
@@ -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++;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user