Migration builder v2 handle RELATION through fields actions (#13076)

# Introduction
In this PR we've mainly refactor the typing to be extending existing
entities.
Also handling relations through the field abstraction layer rather than
a dedicated one. We reverted midway

We then still need to:
- Handle indexing
- Uniqueness
- Add strong coverage and avoid static inline snapshoting as right now +
building a coherent testing set
- Deprecate the `standardId` in favor of a `uniqueIdentifier` on each
`objectMetadata` and `fieldMetadata`
- Rename types `input` to `flattened`
- Handle custom or non custom edit edge cases. ( e.g cannot delete a
standard field or object )


## Notes
Right I preferred including too many information ( whole object and
field input ) in the action context, we might wanna evict redundant
information in the future when implementing the runners
This commit is contained in:
Paul Rastoin
2025-07-08 10:24:24 +02:00
committed by GitHub
parent ebaec00cce
commit e722303a2f
21 changed files with 1206 additions and 522 deletions

View File

@ -0,0 +1,4 @@
export type FromTo<T> = {
from: T;
to: T;
};

View File

@ -0,0 +1,12 @@
import { WorkspaceMigrationFieldActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-field-action-v2';
import { WorkspaceMigrationIndexActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-index-action-v2';
import { WorkspaceMigrationV2ObjectAction } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-object-action-v2';
import { WorkspaceMigrationUniquenessActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-uniqueness-action-v2';
export type WorkspaceMigrationActionV2 =
| WorkspaceMigrationV2ObjectAction
| WorkspaceMigrationFieldActionV2
| WorkspaceMigrationUniquenessActionV2
| WorkspaceMigrationIndexActionV2;
export type WorkspaceMigrationActionTypeV2 = WorkspaceMigrationActionV2['type'];

View File

@ -1,106 +0,0 @@
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<TTarget extends string> = {
[P in `${TTarget}UniqueIdentifier`]: string;
};
type ObjectMetadataUniqueIdentifier = UniqueIdentifierRecord<'objectMetadata'>;
type FieldMetadataUniqueIdentifier = UniqueIdentifierRecord<'fieldMetadata'>;
export type FromTo<T> = {
from: T;
to: T;
};
type ObjectActionCommon = ObjectMetadataUniqueIdentifier;
export type CreateObjectAction = {
type: 'create_object';
object: ObjectMetadataEntity;
} & ObjectActionCommon;
export type UpdateObjectAction = {
type: 'update_object';
updates: (FromTo<Partial<ObjectMetadataEntity>> & { property: string })[];
} & ObjectActionCommon;
export type DeleteObjectAction = {
type: 'delete_object';
} & ObjectActionCommon;
export type WorkspaceMigrationV2ObjectAction =
| CreateObjectAction
| UpdateObjectAction
| DeleteObjectAction;
type FieldActionCommon = {
field: Partial<FieldMetadataEntity>;
} & ObjectMetadataUniqueIdentifier &
FieldMetadataUniqueIdentifier;
export type CreateFieldAction = {
type: 'create_field';
} & FieldActionCommon;
export type UpdateFieldAction = {
type: 'update_field';
} & FieldActionCommon;
export type DeleteFieldAction = {
type: 'delete_field';
} & Omit<FieldActionCommon, 'field'>;
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'];

View File

@ -0,0 +1,35 @@
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { FromTo } from 'src/engine/workspace-manager/workspace-migration-v2/types/from-to.type';
import {
FieldMetadataEntityEditableProperties,
WorkspaceMigrationFieldInput,
} from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-field-input';
import { WorkspaceMigrationObjectWithoutFields } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-object-input';
export type FieldAndObjectMetadataWorkspaceMigrationInput = {
fieldMetadataInput: WorkspaceMigrationFieldInput;
objectMetadataInput: WorkspaceMigrationObjectWithoutFields;
};
export type CreateFieldAction = {
type: 'create_field';
} & FieldAndObjectMetadataWorkspaceMigrationInput;
export type UpdateFieldAction = {
type: 'update_field';
updates: Partial<
{
[P in FieldMetadataEntityEditableProperties]: {
property: P;
} & FromTo<FieldMetadataEntity[P]>;
}[FieldMetadataEntityEditableProperties]
>[];
} & FieldAndObjectMetadataWorkspaceMigrationInput;
export type DeleteFieldAction = {
type: 'delete_field';
} & FieldAndObjectMetadataWorkspaceMigrationInput;
export type WorkspaceMigrationFieldActionV2 =
| CreateFieldAction
| UpdateFieldAction
| DeleteFieldAction;

View File

@ -0,0 +1,35 @@
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
export const fieldMetadataEntityEditableProperties = [
'defaultValue',
'description',
'icon',
'isActive',
'isLabelSyncedWithName',
'isUnique', // unsure
'label',
'name',
'options',
// TODO update once we reactivate the relation edition
// 'relationTargetFieldMetadata',
// 'relationTargetFieldMetadataId',
// 'relationTargetObjectMetadata',
// 'relationTargetObjectMetadataId',
// 'settings',
///
'standardOverrides',
] as const satisfies (keyof FieldMetadataEntity)[];
export type FieldMetadataEntityEditableProperties =
(typeof fieldMetadataEntityEditableProperties)[number];
// TODO could describe required minimum keys
export type WorkspaceMigrationFieldInput = Partial<
Omit<FieldMetadataEntity, 'object'>
> & {
uniqueIdentifier: string;
};
export const fieldMetadataPropertiesToStringify = [
'defaultValue',
'standardOverrides',
] as const satisfies FieldMetadataEntityEditableProperties[];

View File

@ -0,0 +1,11 @@
export type CreateIndexAction = {
type: 'create_index';
};
export type DeleteIndexAction = {
type: 'delete_index';
};
export type WorkspaceMigrationIndexActionV2 =
| CreateIndexAction
| DeleteIndexAction;

View File

@ -0,0 +1,33 @@
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { FromTo } from 'src/engine/workspace-manager/workspace-migration-v2/types/from-to.type';
import {
ObjectMetadataEntityEditableProperties,
WorkspaceMigrationObjectWithoutFields,
} from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-object-input';
type ObjectActionCommon = {
objectMetadataInput: WorkspaceMigrationObjectWithoutFields;
};
export type CreateObjectAction = {
type: 'create_object';
} & ObjectActionCommon;
export type UpdateObjectAction = {
type: 'update_object';
updates: Partial<
{
[P in ObjectMetadataEntityEditableProperties]: {
property: P;
} & FromTo<ObjectMetadataEntity[P]>;
}[ObjectMetadataEntityEditableProperties]
>[];
} & ObjectActionCommon;
export type DeleteObjectAction = {
type: 'delete_object';
} & ObjectActionCommon;
export type WorkspaceMigrationV2ObjectAction =
| CreateObjectAction
| UpdateObjectAction
| DeleteObjectAction;

View File

@ -1,22 +1,28 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { WorkspaceMigrationFieldInput } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-field-input';
export type WorkspaceMigrationObjectFieldInput = {
export const objectMetadataEntityEditableProperties = [
'description',
'icon',
'isActive',
'isLabelSyncedWithName',
'labelPlural',
'labelSingular',
'namePlural',
'nameSingular',
'standardOverrides', // Only if standard
] as const satisfies (keyof ObjectMetadataEntity)[];
export type ObjectMetadataEntityEditableProperties =
(typeof objectMetadataEntityEditableProperties)[number];
export type WorkspaceMigrationObjectInput = Partial<
Omit<ObjectMetadataEntity, 'fields'>
> & {
uniqueIdentifier: string;
name: string;
label: string;
defaultValue: unknown;
type: FieldMetadataType;
description?: string;
// TODO this should extend FieldMetadataEntity
fieldInputs: WorkspaceMigrationFieldInput[];
};
export type WorkspaceMigrationObjectInput = {
uniqueIdentifier: string;
nameSingular: string;
namePlural: string;
labelSingular: string;
labelPlural: string;
description?: string;
fields: WorkspaceMigrationObjectFieldInput[];
// TODO this should extend ObjectMetadataEntity
};
export type WorkspaceMigrationObjectWithoutFields = Omit<
WorkspaceMigrationObjectInput,
'fieldInputs'
>;

View File

@ -0,0 +1,11 @@
export type AddUniquenessConstraintAction = {
type: 'add_uniqueness_constraint';
};
export type RemoveUniquenessConstraintAction = {
type: 'remove_uniqueness_constraint';
};
export type WorkspaceMigrationUniquenessActionV2 =
| RemoveUniquenessConstraintAction
| AddUniquenessConstraintAction;

View File

@ -1,12 +1,12 @@
import { WorkspaceMigrationActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-action-v2';
import { WorkspaceMigrationActionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-action-common-v2';
export interface WorkspaceMigrationV2<
export type WorkspaceMigrationV2<
TActions extends WorkspaceMigrationActionV2 = WorkspaceMigrationActionV2,
> {
> = {
// formatVersion: 1;
// createdAt: string;
// name: string;
// description?: string;
actions: TActions[];
// objectActions: TActions[] // could be cool ?
}
};

View File

@ -13,7 +13,7 @@ describe('WorkspaceMigrationBuilderV2Service', () => {
labelSingular: 'Contact',
labelPlural: 'Contacts',
description: 'A contact',
fields: [
fieldInputs: [
{
uniqueIdentifier: '20202020-e89b-12d3-a456-426614174000',
name: 'firstName',
@ -41,7 +41,24 @@ describe('WorkspaceMigrationBuilderV2Service', () => {
{
"actions": [
{
"objectMetadataUniqueIdentifier": "20202020-e89b-12d3-a456-426614175000",
"objectMetadataInput": {
"description": "A contact",
"fieldInputs": [
{
"defaultValue": "",
"description": "",
"label": "First Name",
"name": "firstName",
"type": "FULL_NAME",
"uniqueIdentifier": "20202020-e89b-12d3-a456-426614174000",
},
],
"labelPlural": "Contacts",
"labelSingular": "Contact",
"namePlural": "Contacts",
"nameSingular": "Person",
"uniqueIdentifier": "20202020-e89b-12d3-a456-426614175000",
},
"type": "update_object",
"updates": [
{
@ -64,7 +81,7 @@ describe('WorkspaceMigrationBuilderV2Service', () => {
labelSingular: 'Company',
labelPlural: 'Companies',
description: 'A company',
fields: [
fieldInputs: [
{
uniqueIdentifier: '20202020-e89b-12d3-a456-426614174001',
name: 'name',
@ -82,9 +99,9 @@ describe('WorkspaceMigrationBuilderV2Service', () => {
{
"actions": [
{
"object": {
"objectMetadataInput": {
"description": "A company",
"fields": [
"fieldInputs": [
{
"defaultValue": "",
"description": "",
@ -100,11 +117,10 @@ describe('WorkspaceMigrationBuilderV2Service', () => {
"nameSingular": "Company",
"uniqueIdentifier": "20202020-e89b-12d3-a456-426614175001",
},
"objectMetadataUniqueIdentifier": "20202020-e89b-12d3-a456-426614175001",
"type": "create_object",
},
{
"field": {
"fieldMetadataInput": {
"defaultValue": "",
"description": "",
"label": "Name",
@ -112,8 +128,24 @@ describe('WorkspaceMigrationBuilderV2Service', () => {
"type": "ADDRESS",
"uniqueIdentifier": "20202020-e89b-12d3-a456-426614174001",
},
"fieldMetadataUniqueIdentifier": "20202020-e89b-12d3-a456-426614174001",
"objectMetadataUniqueIdentifier": "20202020-e89b-12d3-a456-426614175001",
"objectMetadataInput": {
"description": "A company",
"fieldInputs": [
{
"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",
},
"type": "create_field",
},
],
@ -128,7 +160,24 @@ describe('WorkspaceMigrationBuilderV2Service', () => {
{
"actions": [
{
"objectMetadataUniqueIdentifier": "20202020-e89b-12d3-a456-426614175000",
"objectMetadataInput": {
"description": "A contact",
"fieldInputs": [
{
"defaultValue": "",
"description": "",
"label": "First Name",
"name": "firstName",
"type": "FULL_NAME",
"uniqueIdentifier": "20202020-e89b-12d3-a456-426614174000",
},
],
"labelPlural": "Contacts",
"labelSingular": "Contact",
"namePlural": "Contacts",
"nameSingular": "Contact",
"uniqueIdentifier": "20202020-e89b-12d3-a456-426614175000",
},
"type": "delete_object",
},
],
@ -140,8 +189,8 @@ describe('WorkspaceMigrationBuilderV2Service', () => {
const objectToUpdate: WorkspaceMigrationObjectInput = {
...baseObject,
nameSingular: 'Person',
fields: [
...baseObject.fields,
fieldInputs: [
...baseObject.fieldInputs,
{
defaultValue: '',
label: 'New field',
@ -163,7 +212,7 @@ describe('WorkspaceMigrationBuilderV2Service', () => {
labelSingular: 'Company',
labelPlural: 'Companies',
description: 'A company',
fields: [
fieldInputs: [
{
uniqueIdentifier: '20202020-1016-4f09-bad6-e75681f385f4',
name: 'name',
@ -184,9 +233,9 @@ describe('WorkspaceMigrationBuilderV2Service', () => {
{
"actions": [
{
"object": {
"objectMetadataInput": {
"description": "A company",
"fields": [
"fieldInputs": [
{
"defaultValue": "",
"description": "",
@ -202,28 +251,56 @@ describe('WorkspaceMigrationBuilderV2Service', () => {
"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",
"objectMetadataInput": {
"description": "A contact",
"fieldInputs": [
{
"defaultValue": "",
"description": "",
"label": "First Name",
"name": "firstName",
"type": "FULL_NAME",
"uniqueIdentifier": "20202020-e89b-12d3-a456-426614174000",
},
],
"labelPlural": "Contacts",
"labelSingular": "Contact",
"namePlural": "Contacts",
"nameSingular": "Contact",
"uniqueIdentifier": "20202020-59ef-4a14-a509-0a02acb248d5",
},
"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",
"objectMetadataInput": {
"description": "A contact",
"fieldInputs": [
{
"defaultValue": "",
"description": "",
"label": "First Name",
"name": "firstName",
"type": "FULL_NAME",
"uniqueIdentifier": "20202020-e89b-12d3-a456-426614174000",
},
{
"defaultValue": "",
"description": "new field description",
"label": "New field",
"name": "newField",
"type": "NUMBER",
"uniqueIdentifier": "20202020-3ad3-4fec-9c46-8dc9158980e3",
},
],
"labelPlural": "Contacts",
"labelSingular": "Contact",
"namePlural": "Contacts",
"nameSingular": "Person",
"uniqueIdentifier": "20202020-e89b-12d3-a456-426614175000",
},
"type": "update_object",
"updates": [
{
@ -234,7 +311,36 @@ describe('WorkspaceMigrationBuilderV2Service', () => {
],
},
{
"field": {
"fieldMetadataInput": {
"defaultValue": "",
"description": "",
"label": "Name",
"name": "name",
"type": "ADDRESS",
"uniqueIdentifier": "20202020-1016-4f09-bad6-e75681f385f4",
},
"objectMetadataInput": {
"description": "A company",
"fieldInputs": [
{
"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",
},
"type": "create_field",
},
{
"fieldMetadataInput": {
"defaultValue": "",
"description": "new field description",
"label": "New field",
@ -242,8 +348,32 @@ describe('WorkspaceMigrationBuilderV2Service', () => {
"type": "NUMBER",
"uniqueIdentifier": "20202020-3ad3-4fec-9c46-8dc9158980e3",
},
"fieldMetadataUniqueIdentifier": "20202020-3ad3-4fec-9c46-8dc9158980e3",
"objectMetadataUniqueIdentifier": "20202020-e89b-12d3-a456-426614175000",
"objectMetadataInput": {
"description": "A contact",
"fieldInputs": [
{
"defaultValue": "",
"description": "",
"label": "First Name",
"name": "firstName",
"type": "FULL_NAME",
"uniqueIdentifier": "20202020-e89b-12d3-a456-426614174000",
},
{
"defaultValue": "",
"description": "new field description",
"label": "New field",
"name": "newField",
"type": "NUMBER",
"uniqueIdentifier": "20202020-3ad3-4fec-9c46-8dc9158980e3",
},
],
"labelPlural": "Contacts",
"labelSingular": "Contact",
"namePlural": "Contacts",
"nameSingular": "Person",
"uniqueIdentifier": "20202020-e89b-12d3-a456-426614175000",
},
"type": "create_field",
},
],
@ -259,7 +389,7 @@ describe('WorkspaceMigrationBuilderV2Service', () => {
labelSingular: 'Duplicate',
labelPlural: 'Duplicates',
description: 'First object',
fields: [
fieldInputs: [
{
uniqueIdentifier: 'field-1',
name: 'fieldA',
@ -277,7 +407,7 @@ describe('WorkspaceMigrationBuilderV2Service', () => {
labelSingular: 'Duplicate',
labelPlural: 'Duplicates',
description: 'Second object',
fields: [
fieldInputs: [
{
uniqueIdentifier: 'field-2',
name: 'fieldB',
@ -291,33 +421,159 @@ describe('WorkspaceMigrationBuilderV2Service', () => {
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',
}),
]),
);
expect(result.actions).toMatchInlineSnapshot(`
[
{
"objectMetadataInput": {
"description": "First object",
"fieldInputs": [
{
"defaultValue": "",
"description": "",
"label": "Field A",
"name": "fieldA",
"type": "FULL_NAME",
"uniqueIdentifier": "field-1",
},
],
"labelPlural": "Duplicates",
"labelSingular": "Duplicate",
"namePlural": "Duplicates",
"nameSingular": "Duplicate",
"uniqueIdentifier": "id-1",
},
"type": "create_object",
},
{
"objectMetadataInput": {
"description": "Second object",
"fieldInputs": [
{
"defaultValue": "",
"description": "",
"label": "Field B",
"name": "fieldB",
"type": "ADDRESS",
"uniqueIdentifier": "field-2",
},
],
"labelPlural": "Duplicates",
"labelSingular": "Duplicate",
"namePlural": "Duplicates",
"nameSingular": "Duplicate",
"uniqueIdentifier": "id-2",
},
"type": "create_object",
},
{
"fieldMetadataInput": {
"defaultValue": "",
"description": "",
"label": "Field A",
"name": "fieldA",
"type": "FULL_NAME",
"uniqueIdentifier": "field-1",
},
"objectMetadataInput": {
"description": "First object",
"fieldInputs": [
{
"defaultValue": "",
"description": "",
"label": "Field A",
"name": "fieldA",
"type": "FULL_NAME",
"uniqueIdentifier": "field-1",
},
],
"labelPlural": "Duplicates",
"labelSingular": "Duplicate",
"namePlural": "Duplicates",
"nameSingular": "Duplicate",
"uniqueIdentifier": "id-1",
},
"type": "create_field",
},
{
"fieldMetadataInput": {
"defaultValue": "",
"description": "",
"label": "Field B",
"name": "fieldB",
"type": "ADDRESS",
"uniqueIdentifier": "field-2",
},
"objectMetadataInput": {
"description": "Second object",
"fieldInputs": [
{
"defaultValue": "",
"description": "",
"label": "Field B",
"name": "fieldB",
"type": "ADDRESS",
"uniqueIdentifier": "field-2",
},
],
"labelPlural": "Duplicates",
"labelSingular": "Duplicate",
"namePlural": "Duplicates",
"nameSingular": "Duplicate",
"uniqueIdentifier": "id-2",
},
"type": "create_field",
},
]
`);
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',
}),
]),
);
expect(deleteResult.actions).toMatchInlineSnapshot(`
[
{
"objectMetadataInput": {
"description": "First object",
"fieldInputs": [
{
"defaultValue": "",
"description": "",
"label": "Field A",
"name": "fieldA",
"type": "FULL_NAME",
"uniqueIdentifier": "field-1",
},
],
"labelPlural": "Duplicates",
"labelSingular": "Duplicate",
"namePlural": "Duplicates",
"nameSingular": "Duplicate",
"uniqueIdentifier": "id-1",
},
"type": "delete_object",
},
{
"objectMetadataInput": {
"description": "Second object",
"fieldInputs": [
{
"defaultValue": "",
"description": "",
"label": "Field B",
"name": "fieldB",
"type": "ADDRESS",
"uniqueIdentifier": "field-2",
},
],
"labelPlural": "Duplicates",
"labelSingular": "Duplicate",
"namePlural": "Duplicates",
"nameSingular": "Duplicate",
"uniqueIdentifier": "id-2",
},
"type": "delete_object",
},
]
`);
});
it('should emit no actions when from and to are deeply equal', () => {

View File

@ -0,0 +1,424 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { RelationOnDeleteAction } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-on-delete-action.interface';
import { RelationType } from 'src/engine/metadata-modules/field-metadata/interfaces/relation-type.interface';
import { WorkspaceMigrationFieldInput } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-field-input';
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('Workspace migration builder relations tests suite', () => {
let service: WorkspaceMigrationBuilderV2Service;
beforeEach(() => {
service = new WorkspaceMigrationBuilderV2Service();
});
const createMockObject = (
identifier: string,
fields: Partial<WorkspaceMigrationFieldInput>[] = [],
): WorkspaceMigrationObjectInput => ({
uniqueIdentifier: identifier,
fieldInputs: fields.map((field) => ({
type: FieldMetadataType.TEXT,
name: 'defaultName',
label: 'Default Label',
isCustom: true,
isActive: true,
isNullable: true,
uniqueIdentifier: 'default-id',
...field,
})),
});
describe('buildWorkspaceMigrationV2RelationActions', () => {
it('should create relation actions for created fields', () => {
const fromObjects: WorkspaceMigrationObjectInput[] = [];
const toObjects: WorkspaceMigrationObjectInput[] = [
createMockObject('company', [
{
type: FieldMetadataType.RELATION,
name: 'employees',
label: 'Employees',
uniqueIdentifier: 'employees',
relationTargetFieldMetadataId: 'field-2',
relationTargetObjectMetadataId: 'obj-2',
},
]),
];
const result = service.build({ from: fromObjects, to: toObjects });
expect(result).toMatchInlineSnapshot(`
{
"actions": [
{
"objectMetadataInput": {
"fieldInputs": [
{
"isActive": true,
"isCustom": true,
"isNullable": true,
"label": "Employees",
"name": "employees",
"relationTargetFieldMetadataId": "field-2",
"relationTargetObjectMetadataId": "obj-2",
"type": "RELATION",
"uniqueIdentifier": "employees",
},
],
"uniqueIdentifier": "company",
},
"type": "create_object",
},
{
"fieldMetadataInput": {
"isActive": true,
"isCustom": true,
"isNullable": true,
"label": "Employees",
"name": "employees",
"relationTargetFieldMetadataId": "field-2",
"relationTargetObjectMetadataId": "obj-2",
"type": "RELATION",
"uniqueIdentifier": "employees",
},
"objectMetadataInput": {
"fieldInputs": [
{
"isActive": true,
"isCustom": true,
"isNullable": true,
"label": "Employees",
"name": "employees",
"relationTargetFieldMetadataId": "field-2",
"relationTargetObjectMetadataId": "obj-2",
"type": "RELATION",
"uniqueIdentifier": "employees",
},
],
"uniqueIdentifier": "company",
},
"type": "create_field",
},
],
}
`);
});
it('should create delete actions for deleted fields', () => {
const fromObjects: WorkspaceMigrationObjectInput[] = [
createMockObject('company', [
{
type: FieldMetadataType.RELATION,
name: 'employees',
label: 'Employees',
uniqueIdentifier: 'employees',
relationTargetFieldMetadataId: 'field-2',
relationTargetObjectMetadataId: 'obj-2',
},
]),
];
const toObjects: WorkspaceMigrationObjectInput[] = [
createMockObject('company'),
];
const result = service.build({ from: fromObjects, to: toObjects });
expect(result).toMatchInlineSnapshot(`
{
"actions": [
{
"fieldMetadataInput": {
"isActive": true,
"isCustom": true,
"isNullable": true,
"label": "Employees",
"name": "employees",
"relationTargetFieldMetadataId": "field-2",
"relationTargetObjectMetadataId": "obj-2",
"type": "RELATION",
"uniqueIdentifier": "employees",
},
"objectMetadataInput": {
"fieldInputs": [],
"uniqueIdentifier": "company",
},
"type": "delete_field",
},
],
}
`);
});
it('should handle multiple relation changes across different objects', () => {
const fromObjects: WorkspaceMigrationObjectInput[] = [
createMockObject('company', [
{
type: FieldMetadataType.RELATION,
name: 'oldRelation',
label: 'Old Relation',
uniqueIdentifier: 'old-relation',
relationTargetFieldMetadataId: 'field-1',
relationTargetObjectMetadataId: 'obj-1',
},
]),
];
const toObjects: WorkspaceMigrationObjectInput[] = [
createMockObject('company', [
{
type: FieldMetadataType.RELATION,
name: 'newRelation',
label: 'New Relation',
uniqueIdentifier: 'new-relation',
relationTargetFieldMetadataId: 'field-2',
relationTargetObjectMetadataId: 'obj-2',
},
]),
createMockObject('person', [
{
type: FieldMetadataType.RELATION,
name: 'manager',
label: 'Manager',
uniqueIdentifier: 'manager',
relationTargetFieldMetadataId: 'field-3',
relationTargetObjectMetadataId: 'obj-3',
},
]),
];
const result = service.build({ from: fromObjects, to: toObjects });
expect(result).toMatchInlineSnapshot(`
{
"actions": [
{
"objectMetadataInput": {
"fieldInputs": [
{
"isActive": true,
"isCustom": true,
"isNullable": true,
"label": "Manager",
"name": "manager",
"relationTargetFieldMetadataId": "field-3",
"relationTargetObjectMetadataId": "obj-3",
"type": "RELATION",
"uniqueIdentifier": "manager",
},
],
"uniqueIdentifier": "person",
},
"type": "create_object",
},
{
"fieldMetadataInput": {
"isActive": true,
"isCustom": true,
"isNullable": true,
"label": "Manager",
"name": "manager",
"relationTargetFieldMetadataId": "field-3",
"relationTargetObjectMetadataId": "obj-3",
"type": "RELATION",
"uniqueIdentifier": "manager",
},
"objectMetadataInput": {
"fieldInputs": [
{
"isActive": true,
"isCustom": true,
"isNullable": true,
"label": "Manager",
"name": "manager",
"relationTargetFieldMetadataId": "field-3",
"relationTargetObjectMetadataId": "obj-3",
"type": "RELATION",
"uniqueIdentifier": "manager",
},
],
"uniqueIdentifier": "person",
},
"type": "create_field",
},
{
"fieldMetadataInput": {
"isActive": true,
"isCustom": true,
"isNullable": true,
"label": "New Relation",
"name": "newRelation",
"relationTargetFieldMetadataId": "field-2",
"relationTargetObjectMetadataId": "obj-2",
"type": "RELATION",
"uniqueIdentifier": "new-relation",
},
"objectMetadataInput": {
"fieldInputs": [
{
"isActive": true,
"isCustom": true,
"isNullable": true,
"label": "New Relation",
"name": "newRelation",
"relationTargetFieldMetadataId": "field-2",
"relationTargetObjectMetadataId": "obj-2",
"type": "RELATION",
"uniqueIdentifier": "new-relation",
},
],
"uniqueIdentifier": "company",
},
"type": "create_field",
},
{
"fieldMetadataInput": {
"isActive": true,
"isCustom": true,
"isNullable": true,
"label": "Old Relation",
"name": "oldRelation",
"relationTargetFieldMetadataId": "field-1",
"relationTargetObjectMetadataId": "obj-1",
"type": "RELATION",
"uniqueIdentifier": "old-relation",
},
"objectMetadataInput": {
"fieldInputs": [
{
"isActive": true,
"isCustom": true,
"isNullable": true,
"label": "New Relation",
"name": "newRelation",
"relationTargetFieldMetadataId": "field-2",
"relationTargetObjectMetadataId": "obj-2",
"type": "RELATION",
"uniqueIdentifier": "new-relation",
},
],
"uniqueIdentifier": "company",
},
"type": "delete_field",
},
],
}
`);
});
it('should handle empty objects', () => {
const result = service.build({ from: [], to: [] });
expect(result).toMatchInlineSnapshot(`
{
"actions": [],
}
`);
});
it('should handle objects with no relation changes', () => {
const objects = [
createMockObject('company', [
{
type: FieldMetadataType.TEXT,
name: 'name',
label: 'Name',
uniqueIdentifier: 'name',
},
]),
];
const result = service.build({ from: objects, to: objects });
expect(result).toMatchInlineSnapshot(`
{
"actions": [],
}
`);
});
it('should handle relation field updates', () => {
const baseField = {
type: FieldMetadataType.RELATION,
name: 'employees',
label: 'Employees',
uniqueIdentifier: 'employees',
isCustom: true,
isActive: true,
isNullable: true,
description: 'Company employees',
};
const fromObjects: WorkspaceMigrationObjectInput[] = [
createMockObject('company', [
{
...baseField,
relationTargetFieldMetadataId: 'field-1',
relationTargetObjectMetadataId: 'obj-1',
settings: {
relationType: RelationType.ONE_TO_MANY,
onDelete: RelationOnDeleteAction.CASCADE,
} as FieldMetadataSettings<FieldMetadataType.RELATION>,
},
]),
];
const toObjects: WorkspaceMigrationObjectInput[] = [
{
...fromObjects[0],
fieldInputs: [
{
...baseField,
name: 'updatedName',
},
],
},
];
const result = service.build({ from: fromObjects, to: toObjects });
expect(result).toMatchInlineSnapshot(`
{
"actions": [
{
"fieldMetadataInput": {
"description": "Company employees",
"isActive": true,
"isCustom": true,
"isNullable": true,
"label": "Employees",
"name": "updatedName",
"type": "RELATION",
"uniqueIdentifier": "employees",
},
"objectMetadataInput": {
"fieldInputs": [
{
"description": "Company employees",
"isActive": true,
"isCustom": true,
"isNullable": true,
"label": "Employees",
"name": "updatedName",
"type": "RELATION",
"uniqueIdentifier": "employees",
},
],
"uniqueIdentifier": "company",
},
"type": "update_field",
"updates": [
{
"from": "employees",
"property": "name",
"to": "updatedName",
},
],
},
],
}
`);
});
});
});

View File

@ -0,0 +1,40 @@
import { FromTo } from 'src/engine/workspace-manager/workspace-migration-v2/types/from-to.type';
import { WorkspaceMigrationFieldInput } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-field-input';
import {
WorkspaceMigrationObjectInput,
WorkspaceMigrationObjectWithoutFields,
} from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-object-input';
import {
CustomDeletedCreatedUpdatedMatrix,
deletedCreatedUpdatedMatrixDispatcher,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/deleted-created-updated-matrix-dispatcher.util';
export type UpdatedObjectMetadataDeletedCreatedUpdatedFieldMatrix = {
objectMetadataInput: WorkspaceMigrationObjectWithoutFields;
} & CustomDeletedCreatedUpdatedMatrix<
'fieldMetadata',
WorkspaceMigrationFieldInput
>;
export const computeUpdatedObjectMetadataDeletedCreatedUpdatedFieldMatrix = (
updatedObjectMetadata: FromTo<WorkspaceMigrationObjectInput>[],
): UpdatedObjectMetadataDeletedCreatedUpdatedFieldMatrix[] => {
const matrixAccumulator: UpdatedObjectMetadataDeletedCreatedUpdatedFieldMatrix[] =
[];
for (const { from, to } of updatedObjectMetadata) {
const fieldMetadataMatrix = deletedCreatedUpdatedMatrixDispatcher({
from: from.fieldInputs,
to: to.fieldInputs,
});
matrixAccumulator.push({
objectMetadataInput: to,
createdFieldMetadata: fieldMetadataMatrix.created,
deletedFieldMetadata: fieldMetadataMatrix.deleted,
updatedFieldMetadata: fieldMetadataMatrix.updated,
});
}
return matrixAccumulator;
};

View File

@ -1,4 +1,4 @@
import { FromTo } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-action-v2';
import { FromTo } from 'src/engine/workspace-manager/workspace-migration-v2/types/from-to.type';
export type DeletedCreatedUpdatedMatrix<T> = {
created: T[];

View File

@ -1,29 +1,23 @@
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';
FieldAndObjectMetadataWorkspaceMigrationInput,
} from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-field-action-v2';
type FieldInputAndObjectUniqueIdentifier = {
field: WorkspaceMigrationObjectFieldInput;
objectMetadataUniqueIdentifier: string;
};
export const getWorkspaceMigrationV2FieldCreateAction = ({
field,
objectMetadataUniqueIdentifier,
}: FieldInputAndObjectUniqueIdentifier): CreateFieldAction => ({
fieldMetadataInput,
objectMetadataInput,
}: FieldAndObjectMetadataWorkspaceMigrationInput): CreateFieldAction => ({
type: 'create_field',
field: field as unknown as FieldMetadataEntity, // TODO prastoin
fieldMetadataUniqueIdentifier: field.uniqueIdentifier,
objectMetadataUniqueIdentifier,
fieldMetadataInput,
objectMetadataInput,
});
export const getWorkspaceMigrationV2FieldDeleteAction = ({
field,
objectMetadataUniqueIdentifier,
}: FieldInputAndObjectUniqueIdentifier): DeleteFieldAction => ({
fieldMetadataInput,
objectMetadataInput,
}: FieldAndObjectMetadataWorkspaceMigrationInput): DeleteFieldAction => ({
type: 'delete_field',
fieldMetadataUniqueIdentifier: field.uniqueIdentifier,
objectMetadataUniqueIdentifier,
fieldMetadataInput,
objectMetadataInput,
});

View File

@ -1,21 +1,19 @@
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';
} from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-object-action-v2';
import { WorkspaceMigrationObjectInput } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-object-input';
export const getWorkspaceMigrationV2ObjectCreateAction = (
input: WorkspaceMigrationObjectInput,
objectMetadataInput: WorkspaceMigrationObjectInput,
): CreateObjectAction => ({
type: 'create_object',
objectMetadataUniqueIdentifier: input.uniqueIdentifier,
object: input as unknown as ObjectMetadataEntity, // TODO prastoin
objectMetadataInput,
});
export const getWorkspaceMigrationV2ObjectDeleteAction = (
input: WorkspaceMigrationObjectInput,
objectMetadataInput: WorkspaceMigrationObjectInput,
): DeleteObjectAction => ({
type: 'delete_object',
objectMetadataUniqueIdentifier: input.uniqueIdentifier,
objectMetadataInput,
});

View File

@ -0,0 +1,101 @@
import diff from 'microdiff';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { FromTo } from 'src/engine/workspace-manager/workspace-migration-v2/types/from-to.type';
import { UpdateFieldAction } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-field-action-v2';
import {
FieldMetadataEntityEditableProperties,
WorkspaceMigrationFieldInput,
fieldMetadataEntityEditableProperties,
fieldMetadataPropertiesToStringify,
} from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-field-input';
import { transformMetadataForComparison } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util';
const shouldNotOverrideDefaultValue = (type: FieldMetadataType) => {
return [
FieldMetadataType.BOOLEAN,
FieldMetadataType.SELECT,
FieldMetadataType.MULTI_SELECT,
FieldMetadataType.CURRENCY,
FieldMetadataType.PHONES,
FieldMetadataType.ADDRESS,
].includes(type);
};
const compareTwoWorkspaceMigrationFieldInput = ({
from,
to,
}: FromTo<WorkspaceMigrationFieldInput>) => {
const compareFieldMetadataOptions = {
shouldIgnoreProperty: (
property: string,
fieldMetadata: WorkspaceMigrationFieldInput,
) => {
if (
!fieldMetadataEntityEditableProperties.includes(
property as FieldMetadataEntityEditableProperties,
)
) {
return true;
}
if (
property === 'defaultValue' &&
isDefined(fieldMetadata.type) &&
shouldNotOverrideDefaultValue(fieldMetadata.type)
) {
return true;
}
return false;
},
propertiesToStringify: fieldMetadataPropertiesToStringify,
};
const fromCompare = transformMetadataForComparison(
from,
compareFieldMetadataOptions,
);
const toCompare = transformMetadataForComparison(
to,
compareFieldMetadataOptions,
);
const fieldMetadataDifference = diff(fromCompare, toCompare);
return fieldMetadataDifference;
};
type GetWorkspaceMigrationUpdateFieldActionArgs =
FromTo<WorkspaceMigrationFieldInput>;
export const compareFieldMetadataInputAndGetUpdateFieldActions = ({
from,
to,
}: GetWorkspaceMigrationUpdateFieldActionArgs) => {
const fieldMetadataDifferences = compareTwoWorkspaceMigrationFieldInput({
from,
to,
});
return fieldMetadataDifferences.flatMap<UpdateFieldAction['updates'][number]>(
(difference) => {
switch (difference.type) {
case 'CHANGE': {
const { oldValue, path, value } = difference;
return {
from: oldValue,
to: value,
property: path[0] as FieldMetadataEntityEditableProperties,
};
}
case 'CREATE':
case 'REMOVE':
default: {
// Should never occurs, we should only provide null never undefined and so on
return [];
}
}
},
);
};

View File

@ -0,0 +1,70 @@
import omit from 'lodash.omit';
import diff from 'microdiff';
import { assertUnreachable } from 'twenty-shared/utils';
import { FromTo } from 'src/engine/workspace-manager/workspace-migration-v2/types/from-to.type';
import { UpdateObjectAction } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-object-action-v2';
import {
ObjectMetadataEntityEditableProperties,
WorkspaceMigrationObjectInput,
objectMetadataEntityEditableProperties,
} from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-object-input';
import { transformMetadataForComparison } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/utils/transform-metadata-for-comparison.util';
type ObjectWorkspaceMigrationUpdate = FromTo<WorkspaceMigrationObjectInput>;
export const compareTwoWorkspaceMigrationObjectInput = ({
from,
to,
}: ObjectWorkspaceMigrationUpdate) => {
const fromCompare = transformMetadataForComparison(from, {});
const toCompare = transformMetadataForComparison(to, {});
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 (
!objectMetadataEntityEditableProperties.includes(
property as ObjectMetadataEntityEditableProperties,
)
) {
return [];
}
return {
property: property as ObjectMetadataEntityEditableProperties,
from: difference.oldValue,
to: difference.value,
};
}
case 'CREATE':
case 'REMOVE': {
// Should never occurs ? should throw ?
return [];
}
default: {
assertUnreachable(
difference,
`Unexpected difference type: ${difference['type']}`,
);
}
}
});
};

View File

@ -1,28 +1,19 @@
import { Injectable } from '@nestjs/common';
import { FromTo } from 'src/engine/workspace-manager/workspace-migration-v2/types/from-to.type';
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 { computeUpdatedObjectMetadataDeletedCreatedUpdatedFieldMatrix } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/compute-updated-object-metadata-deleted-created-updated-field-matrix.util';
import { deletedCreatedUpdatedMatrixDispatcher } 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 { 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<WorkspaceMigrationObjectInput>;
@Injectable()
export class WorkspaceMigrationBuilderV2Service {
constructor() {}
build(
objectMetadataFromToInputs: WorkspaceMigrationBuilderV2ServiceArgs,
objectMetadataFromToInputs: FromTo<WorkspaceMigrationObjectInput[]>,
): WorkspaceMigrationV2 {
const {
created: createdObjectMetadata,
@ -37,12 +28,30 @@ export class WorkspaceMigrationBuilderV2Service {
updatedObjectMetadata,
});
const createdObjectWorkspaceMigrationCreateFieldActions =
createdObjectMetadata.flatMap((objectMetadataInput) =>
objectMetadataInput.fieldInputs.map((fieldMetadataInput) =>
getWorkspaceMigrationV2FieldCreateAction({
fieldMetadataInput,
objectMetadataInput,
}),
),
);
const updatedObjectMetadataFieldAndRelationDeletedCreatedUpdatedMatrix =
computeUpdatedObjectMetadataDeletedCreatedUpdatedFieldMatrix(
updatedObjectMetadata,
);
const fieldWorkspaceMigrationActions =
buildWorkspaceMigrationV2FieldActions({ updatedObjectMetadata });
buildWorkspaceMigrationV2FieldActions(
updatedObjectMetadataFieldAndRelationDeletedCreatedUpdatedMatrix,
);
return {
actions: [
...objectWorkspaceMigrationActions,
...createdObjectWorkspaceMigrationCreateFieldActions,
...fieldWorkspaceMigrationActions,
],
};

View File

@ -1,207 +1,57 @@
import diff from 'microdiff';
import { FieldMetadataType } from 'twenty-shared/types';
import {
FromTo,
UpdateFieldAction,
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';
} from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-field-action-v2';
import { UpdatedObjectMetadataDeletedCreatedUpdatedFieldMatrix } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/compute-updated-object-metadata-deleted-created-updated-field-matrix.util';
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<WorkspaceMigrationObjectFieldInput>) => {
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<WorkspaceMigrationObjectFieldInput> & {
objectMetadataUniqueIdentifier: string;
};
// Still in wip
const buildWorkspaceMigrationV2FieldActionFromUpdatedFieldMetadata = ({
objectMetadataUniqueIdentifier,
from,
to,
}: BuildWorkspaceMigrationV2FieldActionFromUpdatedFieldMetadataArgs) => {
const fieldMetadataDifferences = compareTwoWorkspaceMigrationFieldInput({
from,
to,
});
return fieldMetadataDifferences.flatMap<WorkspaceMigrationFieldActionV2>(
(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);
import { compareFieldMetadataInputAndGetUpdateFieldActions } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/workspace-migration-field-metadata-input-comparator.util';
export const buildWorkspaceMigrationV2FieldActions = (
objectMetadataDeletedCreatedUpdatedFields: UpdatedObjectMetadataDeletedCreatedUpdatedFieldMatrix[],
): WorkspaceMigrationFieldActionV2[] => {
let allUpdatedObjectMetadataFieldActions: WorkspaceMigrationFieldActionV2[] =
[];
for (const {
createdFieldMetadata,
deletedFieldMetadata,
objectMetadataUniqueIdentifier,
updatedFieldMetadata,
objectMetadataInput,
} of objectMetadataDeletedCreatedUpdatedFields) {
const updateFieldAction =
updatedFieldMetadata.flatMap<WorkspaceMigrationFieldActionV2>(
({ from, to }) =>
buildWorkspaceMigrationV2FieldActionFromUpdatedFieldMetadata({
from,
to,
objectMetadataUniqueIdentifier: objectMetadataUniqueIdentifier,
}),
);
const updateFieldActions = updatedFieldMetadata.flatMap<UpdateFieldAction>(
({ from, to }) => {
const updates = compareFieldMetadataInputAndGetUpdateFieldActions({
from,
to,
});
const createFieldAction = createdFieldMetadata.map((field) =>
if (updates.length === 0) {
return [];
}
return {
type: 'update_field',
fieldMetadataInput: to,
objectMetadataInput,
updates,
};
},
);
const createFieldAction = createdFieldMetadata.map((fieldMetadataInput) =>
getWorkspaceMigrationV2FieldCreateAction({
field,
objectMetadataUniqueIdentifier,
fieldMetadataInput,
objectMetadataInput,
}),
);
const deleteFieldAction = deletedFieldMetadata.map((field) =>
const deleteFieldAction = deletedFieldMetadata.map((fieldMetadataInput) =>
getWorkspaceMigrationV2FieldDeleteAction({
field,
objectMetadataUniqueIdentifier,
fieldMetadataInput,
objectMetadataInput,
}),
);
@ -209,7 +59,7 @@ export const buildWorkspaceMigrationV2FieldActions = ({
allUpdatedObjectMetadataFieldActions.concat([
...createFieldAction,
...deleteFieldAction,
...updateFieldAction,
...updateFieldActions,
]);
}

View File

@ -1,101 +1,14 @@
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';
WorkspaceMigrationV2ObjectAction,
} from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-object-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<ObjectMetadataEntity>)[] = [
'nameSingular',
'namePlural',
'labelSingular',
'labelPlural',
'description',
];
/// End
type ObjectWorkspaceMigrationUpdate = FromTo<WorkspaceMigrationObjectInput>;
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');
}
}
});
};
import { compareTwoWorkspaceMigrationObjectInput } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/utils/workspace-migration-object-metadata-input-comparator.util';
export type CreatedDeletedUpdatedObjectMetadataInputMatrix =
CustomDeletedCreatedUpdatedMatrix<
@ -106,44 +19,32 @@ 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];
},
}: CreatedDeletedUpdatedObjectMetadataInputMatrix): WorkspaceMigrationV2ObjectAction[] => {
const createdObjectActions = createdObjectMetadata.map(
getWorkspaceMigrationV2ObjectCreateAction,
);
const deletedObjectActions = deletedObjectMetadata.map(
getWorkspaceMigrationV2ObjectDeleteAction,
);
const updatedObjectActions = updatedObjectMetadata
.map<UpdateObjectAction | null>(({ from, to }) => {
const updatedObjectActions =
updatedObjectMetadata.flatMap<UpdateObjectAction>(({ from, to }) => {
const objectUpdatedProperties = compareTwoWorkspaceMigrationObjectInput({
from,
to,
});
if (objectUpdatedProperties.length === 0) {
return null;
return [];
}
return {
objectMetadataUniqueIdentifier: from.uniqueIdentifier,
type: 'update_object',
objectMetadataInput: to,
updates: objectUpdatedProperties,
};
})
.filter((action): action is UpdateObjectAction => action !== null);
});
return [
...createdObjectActions,