[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 <paul@twenty.com>
This commit is contained in:
@ -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,
|
||||
|
||||
@ -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<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'];
|
||||
@ -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
|
||||
};
|
||||
@ -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 ?
|
||||
}
|
||||
@ -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([]);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,56 @@
|
||||
import { FromTo } from 'src/engine/workspace-manager/workspace-migration-v2/types/workspace-migration-action-v2';
|
||||
|
||||
export type DeletedCreatedUpdatedMatrix<T> = {
|
||||
created: T[];
|
||||
deleted: T[];
|
||||
updated: FromTo<T>[];
|
||||
};
|
||||
|
||||
export type CustomDeletedCreatedUpdatedMatrix<TLabel extends string, TInput> = {
|
||||
[P in keyof DeletedCreatedUpdatedMatrix<TInput> as `${P}${Capitalize<TLabel>}`]: DeletedCreatedUpdatedMatrix<TInput>[P];
|
||||
};
|
||||
|
||||
export type UniqueIdentifierItem = {
|
||||
uniqueIdentifier: string;
|
||||
};
|
||||
|
||||
export const deletedCreatedUpdatedMatrixDispatcher = <
|
||||
T extends UniqueIdentifierItem,
|
||||
>({
|
||||
from,
|
||||
to,
|
||||
}: FromTo<T[]>): DeletedCreatedUpdatedMatrix<T> => {
|
||||
const initialDispatcher: DeletedCreatedUpdatedMatrix<T> = {
|
||||
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;
|
||||
};
|
||||
@ -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,
|
||||
});
|
||||
@ -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,
|
||||
});
|
||||
@ -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 {}
|
||||
@ -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<WorkspaceMigrationObjectInput>;
|
||||
|
||||
@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,
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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<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);
|
||||
|
||||
let allUpdatedObjectMetadataFieldActions: WorkspaceMigrationFieldActionV2[] =
|
||||
[];
|
||||
|
||||
for (const {
|
||||
createdFieldMetadata,
|
||||
deletedFieldMetadata,
|
||||
objectMetadataUniqueIdentifier,
|
||||
updatedFieldMetadata,
|
||||
} of objectMetadataDeletedCreatedUpdatedFields) {
|
||||
const updateFieldAction =
|
||||
updatedFieldMetadata.flatMap<WorkspaceMigrationFieldActionV2>(
|
||||
({ 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;
|
||||
};
|
||||
@ -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<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');
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
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<UpdateObjectAction | null>(({ 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,
|
||||
];
|
||||
};
|
||||
@ -0,0 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceMetadataMigrationRunnerService {
|
||||
constructor() {}
|
||||
}
|
||||
@ -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 {}
|
||||
@ -0,0 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceSchemaMigrationRunnerService {
|
||||
constructor() {}
|
||||
}
|
||||
@ -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 {}
|
||||
Reference in New Issue
Block a user