From f6e38bd280c8405f2741e6fde0011d8dd297b21c Mon Sep 17 00:00:00 2001 From: Weiko Date: Mon, 7 Jul 2025 09:59:54 +0200 Subject: [PATCH] [POC] Workspace migration builder v2 (#13026) # Introduction In this PR we've initialized the `workspace-migration-v2` folder. Focusing on the builder in the first place. From now it contains: - Basic temporary types ( `fieldMetadataEntity` and `ObjectMetadataEntity` ) - Object actions builder ( create, delete, update ) - Fields actions builder ( create, delete ) ( update coming in a following PR ) We will still have to handle specific conditions such as: - Index creation - Uniqueness addition removal - Relation We also need to determine when we want to compute and transpile the object no field `uniqueIdentifier` We're aiming to merge this first in order to avoid accumulating code in this PR --------- Co-authored-by: prastoin --- .../workspace-manager.module.ts | 2 + .../types/workspace-migration-action-v2.ts | 106 ++++++ .../types/workspace-migration-object-input.ts | 22 ++ .../types/workspace-migration-v2.ts | 12 + .../workspace-migration-v2-builder.spec.ts | 329 ++++++++++++++++++ ...-created-updated-matrix-dispatcher.util.ts | 56 +++ ...et-workspace-migration-v2-field-actions.ts | 29 ++ ...t-workspace-migration-v2-object-actions.ts | 21 ++ .../workspace-migration-builder-v2.module.ts | 11 + .../workspace-migration-builder-v2.service.ts | 50 +++ ...pace-migration-v2-field-actions-builder.ts | 217 ++++++++++++ ...ace-migration-v2-object-actions-builder.ts | 153 ++++++++ ...space-metadata-migration-runner.service.ts | 6 + .../workspace-migration-runner-v2.module.ts | 15 + ...rkspace-schema-migration-runner.service.ts | 6 + .../workspace-migration-v2.module.ts | 14 + 16 files changed, 1049 insertions(+) create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-action-v2.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-object-input.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-v2.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/workspace-migration-v2-builder.spec.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/deleted-created-updated-matrix-dispatcher.util.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/get-workspace-migration-v2-field-actions.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/get-workspace-migration-v2-object-actions.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.module.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.service.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-v2-field-actions-builder.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-v2-object-actions-builder.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-metadata-migration-runner/workspace-metadata-migration-runner.service.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-migration-runner-v2.module.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-schema-migration-runner/workspace-schema-migration-runner.service.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module.ts diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-manager.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-manager.module.ts index 75b98fa96..a91944b7f 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-manager.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-manager.module.ts @@ -16,6 +16,7 @@ import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace- import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { DevSeederModule } from 'src/engine/workspace-manager/dev-seeder/dev-seeder.module'; import { WorkspaceHealthModule } from 'src/engine/workspace-manager/workspace-health/workspace-health.module'; +import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module'; import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.module'; import { WorkspaceManagerService } from './workspace-manager.service'; @@ -24,6 +25,7 @@ import { WorkspaceManagerService } from './workspace-manager.service'; imports: [ WorkspaceDataSourceModule, WorkspaceMigrationModule, + WorkspaceMigrationV2Module, ObjectMetadataModule, DevSeederModule, DataSourceModule, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-action-v2.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-action-v2.ts new file mode 100644 index 000000000..b8d312fbe --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-action-v2.ts @@ -0,0 +1,106 @@ +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; + +type UniqueIdentifierRecord = { + [P in `${TTarget}UniqueIdentifier`]: string; +}; + +type ObjectMetadataUniqueIdentifier = UniqueIdentifierRecord<'objectMetadata'>; + +type FieldMetadataUniqueIdentifier = UniqueIdentifierRecord<'fieldMetadata'>; + +export type FromTo = { + from: T; + to: T; +}; + +type ObjectActionCommon = ObjectMetadataUniqueIdentifier; +export type CreateObjectAction = { + type: 'create_object'; + object: ObjectMetadataEntity; +} & ObjectActionCommon; + +export type UpdateObjectAction = { + type: 'update_object'; + updates: (FromTo> & { property: string })[]; +} & ObjectActionCommon; + +export type DeleteObjectAction = { + type: 'delete_object'; +} & ObjectActionCommon; + +export type WorkspaceMigrationV2ObjectAction = + | CreateObjectAction + | UpdateObjectAction + | DeleteObjectAction; + +type FieldActionCommon = { + field: Partial; +} & ObjectMetadataUniqueIdentifier & + FieldMetadataUniqueIdentifier; +export type CreateFieldAction = { + type: 'create_field'; +} & FieldActionCommon; + +export type UpdateFieldAction = { + type: 'update_field'; +} & FieldActionCommon; + +export type DeleteFieldAction = { + type: 'delete_field'; +} & Omit; + +export type WorkspaceMigrationFieldActionV2 = + | CreateFieldAction + | UpdateFieldAction + | DeleteFieldAction; + +export interface CreateRelationAction { + type: 'create_relation'; +} + +export interface UpdateRelationAction { + type: 'update_relation'; +} + +export interface DeleteRelationAction { + type: 'delete_relation'; +} + +export type WorkspaceMigrationRelationActionV2 = + | CreateRelationAction + | UpdateRelationAction + | DeleteRelationAction; + +export interface CreateIndexAction { + type: 'create_index'; +} + +export interface DeleteIndexAction { + type: 'delete_index'; +} + +export type WorkspaceMigrationIndexActionV2 = + | CreateIndexAction + | DeleteIndexAction; + +export interface AddUniquenessConstraintAction { + type: 'add_uniqueness_constraint'; +} + +export interface RemoveUniquenessConstraintAction { + type: 'remove_uniqueness_constraint'; +} + +export type WorkspaceMigrationUniquenessActionV2 = + | RemoveUniquenessConstraintAction + | AddUniquenessConstraintAction; + +export type WorkspaceMigrationActionV2 = + | WorkspaceMigrationRelationActionV2 + | WorkspaceMigrationV2ObjectAction + | WorkspaceMigrationFieldActionV2 + | WorkspaceMigrationUniquenessActionV2 + | WorkspaceMigrationIndexActionV2; + +export type WorkspaceMigrationActionTypeV2 = WorkspaceMigrationActionV2['type']; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-object-input.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-object-input.ts new file mode 100644 index 000000000..cee695b80 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-object-input.ts @@ -0,0 +1,22 @@ +import { FieldMetadataType } from 'twenty-shared/types'; + +export type WorkspaceMigrationObjectFieldInput = { + uniqueIdentifier: string; + name: string; + label: string; + defaultValue: unknown; + type: FieldMetadataType; + description?: string; + // TODO this should extend FieldMetadataEntity +}; + +export type WorkspaceMigrationObjectInput = { + uniqueIdentifier: string; + nameSingular: string; + namePlural: string; + labelSingular: string; + labelPlural: string; + description?: string; + fields: WorkspaceMigrationObjectFieldInput[]; + // TODO this should extend ObjectMetadataEntity +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-v2.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-v2.ts new file mode 100644 index 000000000..f3ba2c365 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-v2.ts @@ -0,0 +1,12 @@ +import { WorkspaceMigrationActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-action-v2'; + +export interface WorkspaceMigrationV2< + TActions extends WorkspaceMigrationActionV2 = WorkspaceMigrationActionV2, +> { + // formatVersion: 1; + // createdAt: string; + // name: string; + // description?: string; + actions: TActions[]; + // objectActions: TActions[] // could be cool ? +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/workspace-migration-v2-builder.spec.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/workspace-migration-v2-builder.spec.ts new file mode 100644 index 000000000..843bbdfee --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/__tests__/workspace-migration-v2-builder.spec.ts @@ -0,0 +1,329 @@ +import { FieldMetadataType } from 'twenty-shared/types'; + +import { WorkspaceMigrationObjectInput } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-object-input'; +import { WorkspaceMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.service'; + +describe('WorkspaceMigrationBuilderV2Service', () => { + let service: WorkspaceMigrationBuilderV2Service; + + const baseObject: WorkspaceMigrationObjectInput = { + uniqueIdentifier: '20202020-e89b-12d3-a456-426614175000', + nameSingular: 'Contact', + namePlural: 'Contacts', + labelSingular: 'Contact', + labelPlural: 'Contacts', + description: 'A contact', + fields: [ + { + uniqueIdentifier: '20202020-e89b-12d3-a456-426614174000', + name: 'firstName', + label: 'First Name', + type: FieldMetadataType.FULL_NAME, + defaultValue: '', + description: '', + }, + ], + }; + + beforeEach(() => { + service = new WorkspaceMigrationBuilderV2Service(); + }); + + it('should return a migration when nameSingular changes', () => { + const from: WorkspaceMigrationObjectInput = baseObject; + const to: WorkspaceMigrationObjectInput = { + ...from, + nameSingular: 'Person', + }; + const result = service.build({ from: [from], to: [to] }); + + expect(result).toMatchInlineSnapshot(` +{ + "actions": [ + { + "objectMetadataUniqueIdentifier": "20202020-e89b-12d3-a456-426614175000", + "type": "update_object", + "updates": [ + { + "from": "Contact", + "property": "nameSingular", + "to": "Person", + }, + ], + }, + ], +} +`); + }); + + it('should return a migration when creating a new object', () => { + const newObject: WorkspaceMigrationObjectInput = { + uniqueIdentifier: '20202020-e89b-12d3-a456-426614175001', + nameSingular: 'Company', + namePlural: 'Companies', + labelSingular: 'Company', + labelPlural: 'Companies', + description: 'A company', + fields: [ + { + uniqueIdentifier: '20202020-e89b-12d3-a456-426614174001', + name: 'name', + label: 'Name', + type: FieldMetadataType.ADDRESS, + defaultValue: '', + description: '', + }, + ], + }; + + const result = service.build({ from: [], to: [newObject] }); + + expect(result).toMatchInlineSnapshot(` +{ + "actions": [ + { + "object": { + "description": "A company", + "fields": [ + { + "defaultValue": "", + "description": "", + "label": "Name", + "name": "name", + "type": "ADDRESS", + "uniqueIdentifier": "20202020-e89b-12d3-a456-426614174001", + }, + ], + "labelPlural": "Companies", + "labelSingular": "Company", + "namePlural": "Companies", + "nameSingular": "Company", + "uniqueIdentifier": "20202020-e89b-12d3-a456-426614175001", + }, + "objectMetadataUniqueIdentifier": "20202020-e89b-12d3-a456-426614175001", + "type": "create_object", + }, + { + "field": { + "defaultValue": "", + "description": "", + "label": "Name", + "name": "name", + "type": "ADDRESS", + "uniqueIdentifier": "20202020-e89b-12d3-a456-426614174001", + }, + "fieldMetadataUniqueIdentifier": "20202020-e89b-12d3-a456-426614174001", + "objectMetadataUniqueIdentifier": "20202020-e89b-12d3-a456-426614175001", + "type": "create_field", + }, + ], +} +`); + }); + + it('should return a migration when deleting an object', () => { + const result = service.build({ from: [baseObject], to: [] }); + + expect(result).toMatchInlineSnapshot(` +{ + "actions": [ + { + "objectMetadataUniqueIdentifier": "20202020-e89b-12d3-a456-426614175000", + "type": "delete_object", + }, + ], +} +`); + }); + + it('should handle multiple operations in a single migration', () => { + const objectToUpdate: WorkspaceMigrationObjectInput = { + ...baseObject, + nameSingular: 'Person', + fields: [ + ...baseObject.fields, + { + defaultValue: '', + label: 'New field', + type: FieldMetadataType.NUMBER, + name: 'newField', + uniqueIdentifier: '20202020-3ad3-4fec-9c46-8dc9158980e3', + description: 'new field description', + }, + ], + }; + const objectToDelete = { + ...baseObject, + uniqueIdentifier: '20202020-59ef-4a14-a509-0a02acb248d5', + }; + const objectToCreate: WorkspaceMigrationObjectInput = { + uniqueIdentifier: '20202020-1218-4fc0-b32d-fc4f005c4bab', + nameSingular: 'Company', + namePlural: 'Companies', + labelSingular: 'Company', + labelPlural: 'Companies', + description: 'A company', + fields: [ + { + uniqueIdentifier: '20202020-1016-4f09-bad6-e75681f385f4', + name: 'name', + label: 'Name', + type: FieldMetadataType.ADDRESS, + defaultValue: '', + description: '', + }, + ], + }; + + const result = service.build({ + from: [baseObject, objectToDelete], + to: [objectToUpdate, objectToCreate], + }); + + expect(result).toMatchInlineSnapshot(` +{ + "actions": [ + { + "object": { + "description": "A company", + "fields": [ + { + "defaultValue": "", + "description": "", + "label": "Name", + "name": "name", + "type": "ADDRESS", + "uniqueIdentifier": "20202020-1016-4f09-bad6-e75681f385f4", + }, + ], + "labelPlural": "Companies", + "labelSingular": "Company", + "namePlural": "Companies", + "nameSingular": "Company", + "uniqueIdentifier": "20202020-1218-4fc0-b32d-fc4f005c4bab", + }, + "objectMetadataUniqueIdentifier": "20202020-1218-4fc0-b32d-fc4f005c4bab", + "type": "create_object", + }, + { + "field": { + "defaultValue": "", + "description": "", + "label": "Name", + "name": "name", + "type": "ADDRESS", + "uniqueIdentifier": "20202020-1016-4f09-bad6-e75681f385f4", + }, + "fieldMetadataUniqueIdentifier": "20202020-1016-4f09-bad6-e75681f385f4", + "objectMetadataUniqueIdentifier": "20202020-1218-4fc0-b32d-fc4f005c4bab", + "type": "create_field", + }, + { + "objectMetadataUniqueIdentifier": "20202020-59ef-4a14-a509-0a02acb248d5", + "type": "delete_object", + }, + { + "objectMetadataUniqueIdentifier": "20202020-e89b-12d3-a456-426614175000", + "type": "update_object", + "updates": [ + { + "from": "Contact", + "property": "nameSingular", + "to": "Person", + }, + ], + }, + { + "field": { + "defaultValue": "", + "description": "new field description", + "label": "New field", + "name": "newField", + "type": "NUMBER", + "uniqueIdentifier": "20202020-3ad3-4fec-9c46-8dc9158980e3", + }, + "fieldMetadataUniqueIdentifier": "20202020-3ad3-4fec-9c46-8dc9158980e3", + "objectMetadataUniqueIdentifier": "20202020-e89b-12d3-a456-426614175000", + "type": "create_field", + }, + ], +} +`); + }); + + it('should treat objects with the same name but different IDs as distinct', () => { + const objectA: WorkspaceMigrationObjectInput = { + uniqueIdentifier: 'id-1', + nameSingular: 'Duplicate', + namePlural: 'Duplicates', + labelSingular: 'Duplicate', + labelPlural: 'Duplicates', + description: 'First object', + fields: [ + { + uniqueIdentifier: 'field-1', + name: 'fieldA', + label: 'Field A', + type: FieldMetadataType.FULL_NAME, + defaultValue: '', + description: '', + }, + ], + }; + const objectB: WorkspaceMigrationObjectInput = { + uniqueIdentifier: 'id-2', + nameSingular: 'Duplicate', + namePlural: 'Duplicates', + labelSingular: 'Duplicate', + labelPlural: 'Duplicates', + description: 'Second object', + fields: [ + { + uniqueIdentifier: 'field-2', + name: 'fieldB', + label: 'Field B', + type: FieldMetadataType.ADDRESS, + defaultValue: '', + description: '', + }, + ], + }; + + const result = service.build({ from: [], to: [objectA, objectB] }); + + expect(result.actions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: 'create_object', + objectMetadataUniqueIdentifier: 'id-1', + }), + expect.objectContaining({ + type: 'create_object', + objectMetadataUniqueIdentifier: 'id-2', + }), + ]), + ); + + const deleteResult = service.build({ from: [objectA, objectB], to: [] }); + + expect(deleteResult.actions).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: 'delete_object', + objectMetadataUniqueIdentifier: 'id-1', + }), + expect.objectContaining({ + type: 'delete_object', + objectMetadataUniqueIdentifier: 'id-2', + }), + ]), + ); + }); + + it('should emit no actions when from and to are deeply equal', () => { + const obj: WorkspaceMigrationObjectInput = { ...baseObject }; + const result = service.build({ from: [obj], to: [obj] }); + + expect(result.actions).toEqual([]); + }); +}); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/deleted-created-updated-matrix-dispatcher.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/deleted-created-updated-matrix-dispatcher.util.ts new file mode 100644 index 000000000..6f1c9837d --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/deleted-created-updated-matrix-dispatcher.util.ts @@ -0,0 +1,56 @@ +import { FromTo } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-action-v2'; + +export type DeletedCreatedUpdatedMatrix = { + created: T[]; + deleted: T[]; + updated: FromTo[]; +}; + +export type CustomDeletedCreatedUpdatedMatrix = { + [P in keyof DeletedCreatedUpdatedMatrix as `${P}${Capitalize}`]: DeletedCreatedUpdatedMatrix[P]; +}; + +export type UniqueIdentifierItem = { + uniqueIdentifier: string; +}; + +export const deletedCreatedUpdatedMatrixDispatcher = < + T extends UniqueIdentifierItem, +>({ + from, + to, +}: FromTo): DeletedCreatedUpdatedMatrix => { + const initialDispatcher: DeletedCreatedUpdatedMatrix = { + created: [], + updated: [], + deleted: [], + }; + + const fromMap = new Map(from.map((obj) => [obj.uniqueIdentifier, obj])); + const toMap = new Map(to.map((obj) => [obj.uniqueIdentifier, obj])); + + for (const [identifier, fromObj] of fromMap) { + if (!toMap.has(identifier)) { + initialDispatcher.deleted.push(fromObj); + } + } + + for (const [identifier, toObj] of toMap) { + if (!fromMap.has(identifier)) { + initialDispatcher.created.push(toObj); + } + } + + for (const [identifier, fromObj] of fromMap) { + const toObj = toMap.get(identifier); + + if (toObj) { + initialDispatcher.updated.push({ + from: fromObj, + to: toObj, + }); + } + } + + return initialDispatcher; +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/get-workspace-migration-v2-field-actions.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/get-workspace-migration-v2-field-actions.ts new file mode 100644 index 000000000..3607155ae --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/get-workspace-migration-v2-field-actions.ts @@ -0,0 +1,29 @@ +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { + CreateFieldAction, + DeleteFieldAction, +} from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-action-v2'; +import { WorkspaceMigrationObjectFieldInput } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-object-input'; + +type FieldInputAndObjectUniqueIdentifier = { + field: WorkspaceMigrationObjectFieldInput; + objectMetadataUniqueIdentifier: string; +}; +export const getWorkspaceMigrationV2FieldCreateAction = ({ + field, + objectMetadataUniqueIdentifier, +}: FieldInputAndObjectUniqueIdentifier): CreateFieldAction => ({ + type: 'create_field', + field: field as unknown as FieldMetadataEntity, // TODO prastoin + fieldMetadataUniqueIdentifier: field.uniqueIdentifier, + objectMetadataUniqueIdentifier, +}); + +export const getWorkspaceMigrationV2FieldDeleteAction = ({ + field, + objectMetadataUniqueIdentifier, +}: FieldInputAndObjectUniqueIdentifier): DeleteFieldAction => ({ + type: 'delete_field', + fieldMetadataUniqueIdentifier: field.uniqueIdentifier, + objectMetadataUniqueIdentifier, +}); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/get-workspace-migration-v2-object-actions.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/get-workspace-migration-v2-object-actions.ts new file mode 100644 index 000000000..b0be2dc31 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/get-workspace-migration-v2-object-actions.ts @@ -0,0 +1,21 @@ +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { + CreateObjectAction, + DeleteObjectAction, +} from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-action-v2'; +import { WorkspaceMigrationObjectInput } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-object-input'; + +export const getWorkspaceMigrationV2ObjectCreateAction = ( + input: WorkspaceMigrationObjectInput, +): CreateObjectAction => ({ + type: 'create_object', + objectMetadataUniqueIdentifier: input.uniqueIdentifier, + object: input as unknown as ObjectMetadataEntity, // TODO prastoin +}); + +export const getWorkspaceMigrationV2ObjectDeleteAction = ( + input: WorkspaceMigrationObjectInput, +): DeleteObjectAction => ({ + type: 'delete_object', + objectMetadataUniqueIdentifier: input.uniqueIdentifier, +}); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.module.ts new file mode 100644 index 000000000..347a568dd --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; + +import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; +import { WorkspaceMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.service'; + +@Module({ + imports: [FeatureFlagModule], + providers: [WorkspaceMigrationBuilderV2Service], + exports: [], +}) +export class WorkspaceMigrationBuilderV2Module {} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.service.ts new file mode 100644 index 000000000..dac47dc1a --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.service.ts @@ -0,0 +1,50 @@ +import { Injectable } from '@nestjs/common'; + +import { WorkspaceMigrationObjectInput } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-object-input'; +import { WorkspaceMigrationV2 } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-v2'; +import { + DeletedCreatedUpdatedMatrix, + deletedCreatedUpdatedMatrixDispatcher, +} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/deleted-created-updated-matrix-dispatcher.util'; +import { buildWorkspaceMigrationV2FieldActions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-v2-field-actions-builder'; +import { buildWorkspaceMigrationV2ObjectActions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-v2-object-actions-builder'; + +type WorkspaceMigrationBuilderV2ServiceArgs = { + from: WorkspaceMigrationObjectInput[]; + to: WorkspaceMigrationObjectInput[]; +}; + +export type UniqueIdentifierWorkspaceMigrationObjectInputMapDispatcher = + DeletedCreatedUpdatedMatrix; + +@Injectable() +export class WorkspaceMigrationBuilderV2Service { + constructor() {} + + build( + objectMetadataFromToInputs: WorkspaceMigrationBuilderV2ServiceArgs, + ): WorkspaceMigrationV2 { + const { + created: createdObjectMetadata, + deleted: deletedObjectMetadata, + updated: updatedObjectMetadata, + } = deletedCreatedUpdatedMatrixDispatcher(objectMetadataFromToInputs); + + const objectWorkspaceMigrationActions = + buildWorkspaceMigrationV2ObjectActions({ + createdObjectMetadata, + deletedObjectMetadata, + updatedObjectMetadata, + }); + + const fieldWorkspaceMigrationActions = + buildWorkspaceMigrationV2FieldActions({ updatedObjectMetadata }); + + return { + actions: [ + ...objectWorkspaceMigrationActions, + ...fieldWorkspaceMigrationActions, + ], + }; + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-v2-field-actions-builder.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-v2-field-actions-builder.ts new file mode 100644 index 000000000..e107841e6 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-v2-field-actions-builder.ts @@ -0,0 +1,217 @@ +import diff from 'microdiff'; +import { FieldMetadataType } from 'twenty-shared/types'; + +import { + FromTo, + WorkspaceMigrationFieldActionV2, +} from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-action-v2'; +import { WorkspaceMigrationObjectFieldInput } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-object-input'; +import { + getWorkspaceMigrationV2FieldCreateAction, + getWorkspaceMigrationV2FieldDeleteAction, +} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/get-workspace-migration-v2-field-actions'; +import { UniqueIdentifierWorkspaceMigrationObjectInputMapDispatcher } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.service'; +import { CreatedDeletedUpdatedObjectMetadataInputMatrix } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-v2-object-actions-builder'; +import { transformMetadataForComparison } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util'; + +import { + CustomDeletedCreatedUpdatedMatrix, + deletedCreatedUpdatedMatrixDispatcher, +} from './utils/deleted-created-updated-matrix-dispatcher.util'; + +// Start TODO prastoin refactor and strictly type +const commonFieldPropertiesToIgnore = [ + 'id', + 'createdAt', + 'updatedAt', + 'objectMetadataId', + 'isActive', + 'options', + 'settings', + 'joinColumn', + 'gate', + 'asExpression', + 'generatedType', + 'isLabelSyncedWithName', + // uniqueIdentifier ? +]; + +const shouldNotOverrideDefaultValue = (type: FieldMetadataType) => { + return [ + FieldMetadataType.BOOLEAN, + FieldMetadataType.SELECT, + FieldMetadataType.MULTI_SELECT, + FieldMetadataType.CURRENCY, + FieldMetadataType.PHONES, + FieldMetadataType.ADDRESS, + ].includes(type); +}; + +const fieldPropertiesToStringify = ['defaultValue'] as const; +/// End + +export const compareTwoWorkspaceMigrationFieldInput = ({ + from, + to, +}: FromTo) => { + const compareFieldMetadataOptions = { + shouldIgnoreProperty: ( + property: string, + fieldMetadata: WorkspaceMigrationObjectFieldInput, + ) => { + if ( + property === 'defaultValue' && + shouldNotOverrideDefaultValue(fieldMetadata.type) + ) { + return true; + } + + if (commonFieldPropertiesToIgnore.includes(property)) { + return true; + } + + return false; + }, + propertiesToStringify: fieldPropertiesToStringify, + }; + const fromCompare = transformMetadataForComparison( + from, + compareFieldMetadataOptions, + ); + const toCompare = transformMetadataForComparison( + to, + compareFieldMetadataOptions, + ); + + const fieldMetadataDifference = diff(fromCompare, toCompare); + + return fieldMetadataDifference; +}; + +type BuildWorkspaceMigrationV2FieldActionFromUpdatedFieldMetadataArgs = + FromTo & { + objectMetadataUniqueIdentifier: string; + }; +// Still in wip +const buildWorkspaceMigrationV2FieldActionFromUpdatedFieldMetadata = ({ + objectMetadataUniqueIdentifier, + from, + to, +}: BuildWorkspaceMigrationV2FieldActionFromUpdatedFieldMetadataArgs) => { + const fieldMetadataDifferences = compareTwoWorkspaceMigrationFieldInput({ + from, + to, + }); + + return fieldMetadataDifferences.flatMap( + (difference) => { + switch (difference.type) { + case 'CREATE': { + return { + type: 'create_field', + field: difference.value, + fieldMetadataUniqueIdentifier: 'TODO', + objectMetadataUniqueIdentifier, + }; + } + case 'CHANGE': { + // TODO prastoin + return []; + } + case 'REMOVE': { + return { + type: 'delete_field', + fieldMetadataUniqueIdentifier: difference.oldValue.uniqueIdentifier, + objectMetadataUniqueIdentifier, + }; + } + default: { + return []; + } + } + }, + ); +}; + +type DeletedCreatedUpdatedFieldInputMatrix = { + objectMetadataUniqueIdentifier: string; +} & CustomDeletedCreatedUpdatedMatrix< + 'fieldMetadata', + WorkspaceMigrationObjectFieldInput +>; + +const updatedFieldMetadataMatriceMapDispatcher = ( + updatedObjectMetadata: UniqueIdentifierWorkspaceMigrationObjectInputMapDispatcher['updated'], +): DeletedCreatedUpdatedFieldInputMatrix[] => { + const matriceAccumulator: DeletedCreatedUpdatedFieldInputMatrix[] = []; + + for (const { from, to } of updatedObjectMetadata) { + const matrixResult = deletedCreatedUpdatedMatrixDispatcher({ + from: from.fields, + to: to.fields, + }); + + matriceAccumulator.push({ + objectMetadataUniqueIdentifier: from.uniqueIdentifier, + createdFieldMetadata: matrixResult.created, + deletedFieldMetadata: matrixResult.deleted, + updatedFieldMetadata: matrixResult.updated, + }); + } + + return matriceAccumulator; +}; + +type BuildWorkspaceMigrationV2FieldActionsArgs = Pick< + CreatedDeletedUpdatedObjectMetadataInputMatrix, + 'updatedObjectMetadata' +>; +export const buildWorkspaceMigrationV2FieldActions = ({ + updatedObjectMetadata, +}: BuildWorkspaceMigrationV2FieldActionsArgs): WorkspaceMigrationFieldActionV2[] => { + const objectMetadataDeletedCreatedUpdatedFields = + updatedFieldMetadataMatriceMapDispatcher(updatedObjectMetadata); + + let allUpdatedObjectMetadataFieldActions: WorkspaceMigrationFieldActionV2[] = + []; + + for (const { + createdFieldMetadata, + deletedFieldMetadata, + objectMetadataUniqueIdentifier, + updatedFieldMetadata, + } of objectMetadataDeletedCreatedUpdatedFields) { + const updateFieldAction = + updatedFieldMetadata.flatMap( + ({ from, to }) => + buildWorkspaceMigrationV2FieldActionFromUpdatedFieldMetadata({ + from, + to, + objectMetadataUniqueIdentifier: objectMetadataUniqueIdentifier, + }), + ); + + const createFieldAction = createdFieldMetadata.map((field) => + getWorkspaceMigrationV2FieldCreateAction({ + field, + objectMetadataUniqueIdentifier, + }), + ); + + const deleteFieldAction = deletedFieldMetadata.map((field) => + getWorkspaceMigrationV2FieldDeleteAction({ + field, + objectMetadataUniqueIdentifier, + }), + ); + + allUpdatedObjectMetadataFieldActions = + allUpdatedObjectMetadataFieldActions.concat([ + ...createFieldAction, + ...deleteFieldAction, + ...updateFieldAction, + ]); + } + + return allUpdatedObjectMetadataFieldActions; +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-v2-object-actions-builder.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-v2-object-actions-builder.ts new file mode 100644 index 000000000..5fdbdbeb4 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-v2-object-actions-builder.ts @@ -0,0 +1,153 @@ +import omit from 'lodash.omit'; +import diff from 'microdiff'; +import { assertUnreachable } from 'twenty-shared/utils'; + +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { + FromTo, + UpdateObjectAction, + WorkspaceMigrationActionV2, +} from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-action-v2'; +import { WorkspaceMigrationObjectInput } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-object-input'; +import { CustomDeletedCreatedUpdatedMatrix } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/deleted-created-updated-matrix-dispatcher.util'; +import { getWorkspaceMigrationV2FieldCreateAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/get-workspace-migration-v2-field-actions'; +import { + getWorkspaceMigrationV2ObjectCreateAction, + getWorkspaceMigrationV2ObjectDeleteAction, +} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/get-workspace-migration-v2-object-actions'; +import { transformMetadataForComparison } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util'; + +// Start TODO prastoin refactor and strictly type +const objectPropertiesToIgnore = [ + 'id', + 'createdAt', + 'updatedAt', + 'labelIdentifierFieldMetadataId', + 'imageIdentifierFieldMetadataId', + 'isActive', + 'fields', +]; + +// Not the same for standard and custom +const allowedObjectProps: (keyof Partial)[] = [ + 'nameSingular', + 'namePlural', + 'labelSingular', + 'labelPlural', + 'description', +]; +/// End + +type ObjectWorkspaceMigrationUpdate = FromTo; + +const compareTwoWorkspaceMigrationObjectInput = ({ + from, + to, +}: ObjectWorkspaceMigrationUpdate) => { + const fromCompare = transformMetadataForComparison(from, { + shouldIgnoreProperty: (property) => + objectPropertiesToIgnore.includes(property), + }); + const toCompare = transformMetadataForComparison(to, { + shouldIgnoreProperty: (property) => + objectPropertiesToIgnore.includes(property), + }); + const objectMetadataDifference = diff(fromCompare, omit(toCompare, 'fields')); + + return objectMetadataDifference.flatMap< + UpdateObjectAction['updates'][number] + >((difference) => { + switch (difference.type) { + case 'CHANGE': { + if ( + difference.oldValue === null && + (difference.value === null || difference.value === undefined) + ) { + return []; + } + const property = difference.path[0]; + + // TODO investigate why it would be a number, in case of array I guess ? + if (typeof property === 'number') { + return []; + } + + // Could be handled directly from the diff we do above + if ( + !allowedObjectProps.includes(property as keyof ObjectMetadataEntity) + ) { + return []; + } + + return { + property, + from: difference.oldValue, + to: difference.value, + }; + } + case 'CREATE': + case 'REMOVE': { + // Should never occurs ? should throw ? + return []; + } + default: { + assertUnreachable(difference, 'TODO'); + } + } + }); +}; + +export type CreatedDeletedUpdatedObjectMetadataInputMatrix = + CustomDeletedCreatedUpdatedMatrix< + 'objectMetadata', + WorkspaceMigrationObjectInput + >; +export const buildWorkspaceMigrationV2ObjectActions = ({ + createdObjectMetadata, + deletedObjectMetadata, + updatedObjectMetadata, +}: CreatedDeletedUpdatedObjectMetadataInputMatrix): WorkspaceMigrationActionV2[] => { + const createdObjectActions = createdObjectMetadata.flatMap( + (objectMetadata) => { + const createObjectAction = + getWorkspaceMigrationV2ObjectCreateAction(objectMetadata); + const createFieldActions = objectMetadata.fields.map((field) => + getWorkspaceMigrationV2FieldCreateAction({ + field, + objectMetadataUniqueIdentifier: objectMetadata.uniqueIdentifier, + }), + ); + + return [createObjectAction, ...createFieldActions]; + }, + ); + + const deletedObjectActions = deletedObjectMetadata.map( + getWorkspaceMigrationV2ObjectDeleteAction, + ); + + const updatedObjectActions = updatedObjectMetadata + .map(({ from, to }) => { + const objectUpdatedProperties = compareTwoWorkspaceMigrationObjectInput({ + from, + to, + }); + + if (objectUpdatedProperties.length === 0) { + return null; + } + + return { + objectMetadataUniqueIdentifier: from.uniqueIdentifier, + type: 'update_object', + updates: objectUpdatedProperties, + }; + }) + .filter((action): action is UpdateObjectAction => action !== null); + + return [ + ...createdObjectActions, + ...deletedObjectActions, + ...updatedObjectActions, + ]; +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-metadata-migration-runner/workspace-metadata-migration-runner.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-metadata-migration-runner/workspace-metadata-migration-runner.service.ts new file mode 100644 index 000000000..415178a3c --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-metadata-migration-runner/workspace-metadata-migration-runner.service.ts @@ -0,0 +1,6 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class WorkspaceMetadataMigrationRunnerService { + constructor() {} +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-migration-runner-v2.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-migration-runner-v2.module.ts new file mode 100644 index 000000000..8c392387b --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-migration-runner-v2.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; + +import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; +import { WorkspaceMetadataMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-metadata-migration-runner/workspace-metadata-migration-runner.service'; +import { WorkspaceSchemaMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-schema-migration-runner/workspace-schema-migration-runner.service'; + +@Module({ + imports: [FeatureFlagModule], + providers: [ + WorkspaceMetadataMigrationRunnerService, + WorkspaceSchemaMigrationRunnerService, + ], + exports: [], +}) +export class WorkspaceMigrationRunnerV2Module {} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-schema-migration-runner/workspace-schema-migration-runner.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-schema-migration-runner/workspace-schema-migration-runner.service.ts new file mode 100644 index 000000000..25455968e --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-schema-migration-runner/workspace-schema-migration-runner.service.ts @@ -0,0 +1,6 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class WorkspaceSchemaMigrationRunnerService { + constructor() {} +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module.ts new file mode 100644 index 000000000..06bb092ca --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; + +import { WorkspaceMigrationBuilderV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/workspace-migration-builder-v2.module'; +import { WorkspaceMigrationRunnerV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-migration-runner-v2.module'; + +@Module({ + imports: [ + WorkspaceMigrationBuilderV2Module, + WorkspaceMigrationRunnerV2Module, + ], + providers: [], + exports: [], +}) +export class WorkspaceMigrationV2Module {}