diff --git a/packages/twenty-server/src/metadata/workspace-migration/utils/generate-migration-name.util.ts b/packages/twenty-server/src/metadata/workspace-migration/utils/generate-migration-name.util.ts index 2d9057d47..988ff1be8 100644 --- a/packages/twenty-server/src/metadata/workspace-migration/utils/generate-migration-name.util.ts +++ b/packages/twenty-server/src/metadata/workspace-migration/utils/generate-migration-name.util.ts @@ -1,6 +1,3 @@ -export function generateMigrationName( - name?: string, - addMilliseconds: number = 0, -): string { - return `${new Date().getTime() + addMilliseconds}${name ? `-${name}` : ''}`; +export function generateMigrationName(name?: string): string { + return `${new Date().getTime()}${name ? `-${name}` : ''}`; } diff --git a/packages/twenty-server/src/workspace/workspace-health/commands/workspace-health.command.ts b/packages/twenty-server/src/workspace/workspace-health/commands/workspace-health.command.ts index 210c8a5f2..94bdf4be0 100644 --- a/packages/twenty-server/src/workspace/workspace-health/commands/workspace-health.command.ts +++ b/packages/twenty-server/src/workspace/workspace-health/commands/workspace-health.command.ts @@ -2,6 +2,7 @@ import { Command, CommandRunner, Option } from 'nest-commander'; import chalk from 'chalk'; import { WorkspaceHealthMode } from 'src/workspace/workspace-health/interfaces/workspace-health-options.interface'; +import { WorkspaceHealthFixKind } from 'src/workspace/workspace-health/interfaces/workspace-health-fix-kind.interface'; import { WorkspaceHealthService } from 'src/workspace/workspace-health/workspace-health.service'; @@ -9,6 +10,7 @@ interface WorkspaceHealthCommandOptions { workspaceId: string; verbose?: boolean; mode?: WorkspaceHealthMode; + fix?: WorkspaceHealthFixKind; } @Command({ @@ -44,6 +46,14 @@ export class WorkspaceHealthCommand extends CommandRunner { console.groupEnd(); } } + + if (options.fix) { + await this.workspaceHealthService.fixIssues( + options.workspaceId, + issues, + options.fix, + ); + } } @Option({ @@ -55,6 +65,19 @@ export class WorkspaceHealthCommand extends CommandRunner { return value; } + @Option({ + flags: '-f, --fix [kind]', + description: 'fix issues', + required: false, + }) + fix(value: string): WorkspaceHealthFixKind { + if (!Object.values(WorkspaceHealthFixKind).includes(value as any)) { + throw new Error(`Invalid fix kind ${value}`); + } + + return value as WorkspaceHealthFixKind; + } + @Option({ flags: '-v, --verbose', description: 'Detailed output', @@ -71,6 +94,10 @@ export class WorkspaceHealthCommand extends CommandRunner { defaultValue: WorkspaceHealthMode.All, }) parseMode(value: string): WorkspaceHealthMode { + if (!Object.values(WorkspaceHealthMode).includes(value as any)) { + throw new Error(`Invalid mode ${value}`); + } + return value as WorkspaceHealthMode; } } diff --git a/packages/twenty-server/src/workspace/workspace-health/interfaces/workspace-health-fix-kind.interface.ts b/packages/twenty-server/src/workspace/workspace-health/interfaces/workspace-health-fix-kind.interface.ts new file mode 100644 index 000000000..58bf30646 --- /dev/null +++ b/packages/twenty-server/src/workspace/workspace-health/interfaces/workspace-health-fix-kind.interface.ts @@ -0,0 +1,6 @@ +export enum WorkspaceHealthFixKind { + Nullable = 'nullable', + Type = 'type', + DefaultValue = 'default-value', + TargetColumnMap = 'target-column-map', +} diff --git a/packages/twenty-server/src/workspace/workspace-health/interfaces/workspace-health-issue.interface.ts b/packages/twenty-server/src/workspace/workspace-health/interfaces/workspace-health-issue.interface.ts index c7d1ac0b0..c250f2c3c 100644 --- a/packages/twenty-server/src/workspace/workspace-health/interfaces/workspace-health-issue.interface.ts +++ b/packages/twenty-server/src/workspace/workspace-health/interfaces/workspace-health-issue.interface.ts @@ -31,19 +31,31 @@ export enum WorkspaceHealthIssueType { RELATION_TYPE_NOT_VALID = 'RELATION_TYPE_NOT_VALID', } -export interface WorkspaceHealthTableIssue { - type: +type ConditionalType< + T extends WorkspaceHealthIssueType | null, + U, +> = T extends WorkspaceHealthIssueType ? T : U; + +export interface WorkspaceHealthTableIssue< + T extends WorkspaceHealthIssueType | null = null, +> { + type: ConditionalType< + T, | WorkspaceHealthIssueType.MISSING_TABLE | WorkspaceHealthIssueType.TABLE_NAME_SHOULD_BE_CUSTOM | WorkspaceHealthIssueType.TABLE_TARGET_TABLE_NAME_NOT_VALID | WorkspaceHealthIssueType.TABLE_DATA_SOURCE_ID_NOT_VALID - | WorkspaceHealthIssueType.TABLE_NAME_NOT_VALID; + | WorkspaceHealthIssueType.TABLE_NAME_NOT_VALID + >; objectMetadata: ObjectMetadataEntity; message: string; } -export interface WorkspaceHealthColumnIssue { - type: +export interface WorkspaceHealthColumnIssue< + T extends WorkspaceHealthIssueType | null = null, +> { + type: ConditionalType< + T, | WorkspaceHealthIssueType.MISSING_COLUMN | WorkspaceHealthIssueType.MISSING_INDEX | WorkspaceHealthIssueType.MISSING_FOREIGN_KEY @@ -57,19 +69,24 @@ export interface WorkspaceHealthColumnIssue { | WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT | WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT | WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_NOT_VALID - | WorkspaceHealthIssueType.COLUMN_OPTIONS_NOT_VALID; + | WorkspaceHealthIssueType.COLUMN_OPTIONS_NOT_VALID + >; fieldMetadata: FieldMetadataEntity; columnStructure?: WorkspaceTableStructure; message: string; } -export interface WorkspaceHealthRelationIssue { - type: +export interface WorkspaceHealthRelationIssue< + T extends WorkspaceHealthIssueType | null = null, +> { + type: ConditionalType< + T, | WorkspaceHealthIssueType.RELATION_FROM_OR_TO_FIELD_METADATA_NOT_VALID | WorkspaceHealthIssueType.RELATION_FOREIGN_KEY_NOT_VALID | WorkspaceHealthIssueType.RELATION_NULLABILITY_CONFLICT | WorkspaceHealthIssueType.RELATION_FOREIGN_KEY_CONFLICT - | WorkspaceHealthIssueType.RELATION_TYPE_NOT_VALID; + | WorkspaceHealthIssueType.RELATION_TYPE_NOT_VALID + >; fromFieldMetadata: FieldMetadataEntity | undefined; toFieldMetadata: FieldMetadataEntity | undefined; relationMetadata: RelationMetadataEntity; diff --git a/packages/twenty-server/src/workspace/workspace-health/services/workspace-fix-nullable.service.ts b/packages/twenty-server/src/workspace/workspace-health/services/workspace-fix-nullable.service.ts new file mode 100644 index 000000000..59c3748e6 --- /dev/null +++ b/packages/twenty-server/src/workspace/workspace-health/services/workspace-fix-nullable.service.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@nestjs/common'; + +import { EntityManager } from 'typeorm'; + +import { + WorkspaceHealthColumnIssue, + WorkspaceHealthIssueType, + WorkspaceHealthRelationIssue, +} from 'src/workspace/workspace-health/interfaces/workspace-health-issue.interface'; + +import { WorkspaceMigrationEntity } from 'src/metadata/workspace-migration/workspace-migration.entity'; +import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity'; + +type WorkspaceHealthNullableIssue = + | WorkspaceHealthColumnIssue + | WorkspaceHealthRelationIssue; + +@Injectable() +export class WorkspaceFixNullableService { + constructor() {} + + async fix( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + manager: EntityManager, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + objectMetadataCollection: ObjectMetadataEntity[], + // eslint-disable-next-line @typescript-eslint/no-unused-vars + issues: WorkspaceHealthNullableIssue[], + ): Promise[]> { + // TODO: Implement nullable fix + return []; + } +} diff --git a/packages/twenty-server/src/workspace/workspace-health/services/workspace-fix.service.ts b/packages/twenty-server/src/workspace/workspace-health/services/workspace-fix.service.ts new file mode 100644 index 000000000..74b8c465c --- /dev/null +++ b/packages/twenty-server/src/workspace/workspace-health/services/workspace-fix.service.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@nestjs/common'; + +import { EntityManager } from 'typeorm'; + +import { WorkspaceHealthFixKind } from 'src/workspace/workspace-health/interfaces/workspace-health-fix-kind.interface'; +import { WorkspaceHealthIssue } from 'src/workspace/workspace-health/interfaces/workspace-health-issue.interface'; + +import { WorkspaceMigrationEntity } from 'src/metadata/workspace-migration/workspace-migration.entity'; +import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity'; +import { isWorkspaceHealthNullableIssue } from 'src/workspace/workspace-health/utils/is-workspace-health-issue-type.util'; + +import { WorkspaceFixNullableService } from './workspace-fix-nullable.service'; + +@Injectable() +export class WorkspaceFixService { + constructor( + private readonly workspaceFixNullableService: WorkspaceFixNullableService, + ) {} + + async fix( + manager: EntityManager, + objectMetadataCollection: ObjectMetadataEntity[], + type: WorkspaceHealthFixKind, + issues: WorkspaceHealthIssue[], + ): Promise[]> { + const services = { + [WorkspaceHealthFixKind.Nullable]: { + service: this.workspaceFixNullableService, + issues: issues.filter((issue) => + isWorkspaceHealthNullableIssue(issue.type), + ), + }, + }; + + return services[type].service.fix( + manager, + objectMetadataCollection, + services[type].issues, + ); + } +} diff --git a/packages/twenty-server/src/workspace/workspace-health/utils/is-workspace-health-issue-type.util.ts b/packages/twenty-server/src/workspace/workspace-health/utils/is-workspace-health-issue-type.util.ts new file mode 100644 index 000000000..777709c57 --- /dev/null +++ b/packages/twenty-server/src/workspace/workspace-health/utils/is-workspace-health-issue-type.util.ts @@ -0,0 +1,12 @@ +import { WorkspaceHealthIssueType } from 'src/workspace/workspace-health/interfaces/workspace-health-issue.interface'; + +export const isWorkspaceHealthNullableIssue = ( + type: WorkspaceHealthIssueType, +): type is + | WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT + | WorkspaceHealthIssueType.RELATION_NULLABILITY_CONFLICT => { + return type === WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT || + type === WorkspaceHealthIssueType.RELATION_NULLABILITY_CONFLICT + ? true + : false; +}; diff --git a/packages/twenty-server/src/workspace/workspace-health/workspace-health.module.ts b/packages/twenty-server/src/workspace/workspace-health/workspace-health.module.ts index 71c661d9f..88c63fe4f 100644 --- a/packages/twenty-server/src/workspace/workspace-health/workspace-health.module.ts +++ b/packages/twenty-server/src/workspace/workspace-health/workspace-health.module.ts @@ -9,6 +9,11 @@ import { FieldMetadataHealthService } from 'src/workspace/workspace-health/servi import { ObjectMetadataHealthService } from 'src/workspace/workspace-health/services/object-metadata-health.service'; import { RelationMetadataHealthService } from 'src/workspace/workspace-health/services/relation-metadata.health.service'; import { WorkspaceHealthService } from 'src/workspace/workspace-health/workspace-health.service'; +import { WorkspaceMigrationBuilderModule } from 'src/workspace/workspace-migration-builder/workspace-migration-builder.module'; +import { WorkspaceMigrationRunnerModule } from 'src/workspace/workspace-migration-runner/workspace-migration-runner.module'; + +import { WorkspaceFixService } from './services/workspace-fix.service'; +import { WorkspaceFixNullableService } from './services/workspace-fix-nullable.service'; @Module({ imports: [ @@ -16,6 +21,8 @@ import { WorkspaceHealthService } from 'src/workspace/workspace-health/workspace TypeORMModule, ObjectMetadataModule, WorkspaceDataSourceModule, + WorkspaceMigrationRunnerModule, + WorkspaceMigrationBuilderModule, ], providers: [ WorkspaceHealthService, @@ -23,6 +30,8 @@ import { WorkspaceHealthService } from 'src/workspace/workspace-health/workspace ObjectMetadataHealthService, FieldMetadataHealthService, RelationMetadataHealthService, + WorkspaceFixNullableService, + WorkspaceFixService, ], exports: [WorkspaceHealthService], }) diff --git a/packages/twenty-server/src/workspace/workspace-health/workspace-health.service.ts b/packages/twenty-server/src/workspace/workspace-health/workspace-health.service.ts index b781df389..ab5692d0d 100644 --- a/packages/twenty-server/src/workspace/workspace-health/workspace-health.service.ts +++ b/packages/twenty-server/src/workspace/workspace-health/workspace-health.service.ts @@ -1,10 +1,14 @@ import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectDataSource } from '@nestjs/typeorm'; + +import { DataSource } from 'typeorm'; import { WorkspaceHealthIssue } from 'src/workspace/workspace-health/interfaces/workspace-health-issue.interface'; import { WorkspaceHealthMode, WorkspaceHealthOptions, } from 'src/workspace/workspace-health/interfaces/workspace-health-options.interface'; +import { WorkspaceHealthFixKind } from 'src/workspace/workspace-health/interfaces/workspace-health-fix-kind.interface'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; import { DataSourceService } from 'src/metadata/data-source/data-source.service'; @@ -15,10 +19,15 @@ import { FieldMetadataHealthService } from 'src/workspace/workspace-health/servi import { RelationMetadataHealthService } from 'src/workspace/workspace-health/services/relation-metadata.health.service'; import { DatabaseStructureService } from 'src/workspace/workspace-health/services/database-structure.service'; import { computeObjectTargetTable } from 'src/workspace/utils/compute-object-target-table.util'; +import { WorkspaceMigrationEntity } from 'src/metadata/workspace-migration/workspace-migration.entity'; +import { WorkspaceMigrationRunnerService } from 'src/workspace/workspace-migration-runner/workspace-migration-runner.service'; +import { WorkspaceFixService } from 'src/workspace/workspace-health/services/workspace-fix.service'; @Injectable() export class WorkspaceHealthService { constructor( + @InjectDataSource('metadata') + private readonly metadataDataSource: DataSource, private readonly dataSourceService: DataSourceService, private readonly typeORMService: TypeORMService, private readonly objectMetadataService: ObjectMetadataService, @@ -27,6 +36,8 @@ export class WorkspaceHealthService { private readonly objectMetadataHealthService: ObjectMetadataHealthService, private readonly fieldMetadataHealthService: FieldMetadataHealthService, private readonly relationMetadataHealthService: RelationMetadataHealthService, + private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService, + private readonly workspaceFixService: WorkspaceFixService, ) {} async healthCheck( @@ -106,4 +117,48 @@ export class WorkspaceHealthService { return issues; } + + async fixIssues( + workspaceId: string, + issues: WorkspaceHealthIssue[], + type: WorkspaceHealthFixKind, + ): Promise { + const queryRunner = this.metadataDataSource.createQueryRunner(); + + await queryRunner.connect(); + await queryRunner.startTransaction(); + + const manager = queryRunner.manager; + + try { + const workspaceMigrationRepository = manager.getRepository( + WorkspaceMigrationEntity, + ); + const objectMetadataCollection = + await this.objectMetadataService.findManyWithinWorkspace(workspaceId); + + const workspaceMigrations = await this.workspaceFixService.fix( + manager, + objectMetadataCollection, + type, + issues, + ); + + // Save workspace migrations into the database + await workspaceMigrationRepository.save(workspaceMigrations); + + // Commit the transaction + await queryRunner.commitTransaction(); + + // Apply pending migrations + await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations( + workspaceId, + ); + } catch (error) { + await queryRunner.rollbackTransaction(); + console.error('Fix of issues failed with:', error); + } finally { + await queryRunner.release(); + } + } } diff --git a/packages/twenty-server/src/workspace/workspace-manager/workspace-manager.module.ts b/packages/twenty-server/src/workspace/workspace-manager/workspace-manager.module.ts index 3f798e92d..4c404070b 100644 --- a/packages/twenty-server/src/workspace/workspace-manager/workspace-manager.module.ts +++ b/packages/twenty-server/src/workspace/workspace-manager/workspace-manager.module.ts @@ -5,6 +5,7 @@ import { ObjectMetadataModule } from 'src/metadata/object-metadata/object-metada import { WorkspaceMigrationModule } from 'src/metadata/workspace-migration/workspace-migration.module'; import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module'; import { WorkspaceSyncMetadataModule } from 'src/workspace/workspace-sync-metadata/workspace-sync-metadata.module'; +import { WorkspaceHealthModule } from 'src/workspace/workspace-health/workspace-health.module'; import { WorkspaceManagerService } from './workspace-manager.service'; @@ -15,6 +16,7 @@ import { WorkspaceManagerService } from './workspace-manager.service'; ObjectMetadataModule, DataSourceModule, WorkspaceSyncMetadataModule, + WorkspaceHealthModule, ], exports: [WorkspaceManagerService], providers: [WorkspaceManagerService], diff --git a/packages/twenty-server/src/workspace/workspace-migration-builder/factories/index.ts b/packages/twenty-server/src/workspace/workspace-migration-builder/factories/index.ts new file mode 100644 index 000000000..50f96d394 --- /dev/null +++ b/packages/twenty-server/src/workspace/workspace-migration-builder/factories/index.ts @@ -0,0 +1,9 @@ +import { WorkspaceMigrationObjectFactory } from './workspace-migration-object.factory'; +import { WorkspaceMigrationFieldFactory } from './workspace-migration-field.factory'; +import { WorkspaceMigrationRelationFactory } from './workspace-migration-relation.factory'; + +export const workspaceMigrationBuilderFactories = [ + WorkspaceMigrationObjectFactory, + WorkspaceMigrationFieldFactory, + WorkspaceMigrationRelationFactory, +]; diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/factories/field-workspace-migration.factory.ts b/packages/twenty-server/src/workspace/workspace-migration-builder/factories/workspace-migration-field.factory.ts similarity index 56% rename from packages/twenty-server/src/workspace/workspace-sync-metadata/factories/field-workspace-migration.factory.ts rename to packages/twenty-server/src/workspace/workspace-migration-builder/factories/workspace-migration-field.factory.ts index caab405af..0ede35e4b 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/factories/field-workspace-migration.factory.ts +++ b/packages/twenty-server/src/workspace/workspace-migration-builder/factories/workspace-migration-field.factory.ts @@ -1,5 +1,7 @@ import { Injectable } from '@nestjs/common'; +import { WorkspaceMigrationBuilderAction } from 'src/workspace/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface'; + import { FieldMetadataEntity, FieldMetadataType, @@ -14,18 +16,38 @@ import { computeObjectTargetTable } from 'src/workspace/utils/compute-object-tar import { WorkspaceMigrationFactory } from 'src/metadata/workspace-migration/workspace-migration.factory'; import { generateMigrationName } from 'src/metadata/workspace-migration/utils/generate-migration-name.util'; +interface FieldMetadataUpdate { + current: FieldMetadataEntity; + altered: FieldMetadataEntity; +} + @Injectable() -export class FieldWorkspaceMigrationFactory { +export class WorkspaceMigrationFieldFactory { constructor( private readonly workspaceMigrationFactory: WorkspaceMigrationFactory, ) {} async create( originalObjectMetadataCollection: ObjectMetadataEntity[], - createFieldMetadataCollection: FieldMetadataEntity[], - deleteFieldMetadataCollection: FieldMetadataEntity[], + fieldMetadataCollection: FieldMetadataEntity[], + action: + | WorkspaceMigrationBuilderAction.CREATE + | WorkspaceMigrationBuilderAction.DELETE, + ): Promise[]>; + + async create( + originalObjectMetadataCollection: ObjectMetadataEntity[], + fieldMetadataUpdateCollection: FieldMetadataUpdate[], + action: WorkspaceMigrationBuilderAction.UPDATE, + ): Promise[]>; + + async create( + originalObjectMetadataCollection: ObjectMetadataEntity[], + fieldMetadataCollectionOrFieldMetadataUpdateCollection: + | FieldMetadataEntity[] + | FieldMetadataUpdate[], + action: WorkspaceMigrationBuilderAction, ): Promise[]> { - const workspaceMigrations: Partial[] = []; const originalObjectMetadataMap = originalObjectMetadataCollection.reduce( (result, currentObject) => { result[currentObject.id] = currentObject; @@ -35,31 +57,25 @@ export class FieldWorkspaceMigrationFactory { {} as Record, ); - /** - * Create field migrations - */ - if (createFieldMetadataCollection.length > 0) { - const createFieldWorkspaceMigrations = await this.createFieldMigration( - originalObjectMetadataMap, - createFieldMetadataCollection, - ); - - workspaceMigrations.push(...createFieldWorkspaceMigrations); + switch (action) { + case WorkspaceMigrationBuilderAction.CREATE: + return this.createFieldMigration( + originalObjectMetadataMap, + fieldMetadataCollectionOrFieldMetadataUpdateCollection as FieldMetadataEntity[], + ); + case WorkspaceMigrationBuilderAction.UPDATE: + return this.updateFieldMigration( + originalObjectMetadataMap, + fieldMetadataCollectionOrFieldMetadataUpdateCollection as FieldMetadataUpdate[], + ); + case WorkspaceMigrationBuilderAction.DELETE: + return this.deleteFieldMigration( + originalObjectMetadataMap, + fieldMetadataCollectionOrFieldMetadataUpdateCollection as FieldMetadataEntity[], + ); + default: + return []; } - - /** - * Delete field migrations - */ - if (deleteFieldMetadataCollection.length > 0) { - const deleteFieldWorkspaceMigrations = await this.deleteFieldMigration( - originalObjectMetadataMap, - deleteFieldMetadataCollection, - ); - - workspaceMigrations.push(...deleteFieldWorkspaceMigrations); - } - - return workspaceMigrations; } private async createFieldMigration( @@ -93,6 +109,42 @@ export class FieldWorkspaceMigrationFactory { return workspaceMigrations; } + private async updateFieldMigration( + originalObjectMetadataMap: Record, + fieldMetadataUpdateCollection: FieldMetadataUpdate[], + ): Promise[]> { + const workspaceMigrations: Partial[] = []; + + for (const fieldMetadataUpdate of fieldMetadataUpdateCollection) { + const migrations: WorkspaceMigrationTableAction[] = [ + { + name: computeObjectTargetTable( + originalObjectMetadataMap[ + fieldMetadataUpdate.current.objectMetadataId + ], + ), + action: 'alter', + columns: this.workspaceMigrationFactory.createColumnActions( + WorkspaceMigrationColumnActionType.ALTER, + fieldMetadataUpdate.current, + fieldMetadataUpdate.altered, + ), + }, + ]; + + workspaceMigrations.push({ + workspaceId: fieldMetadataUpdate.current.workspaceId, + name: generateMigrationName( + `update-${fieldMetadataUpdate.altered.name}`, + ), + isCustom: false, + migrations, + }); + } + + return workspaceMigrations; + } + private async deleteFieldMigration( originalObjectMetadataMap: Record, fieldMetadataCollection: FieldMetadataEntity[], diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/factories/object-workspace-migration.factory.ts b/packages/twenty-server/src/workspace/workspace-migration-builder/factories/workspace-migration-object.factory.ts similarity index 76% rename from packages/twenty-server/src/workspace/workspace-sync-metadata/factories/object-workspace-migration.factory.ts rename to packages/twenty-server/src/workspace/workspace-migration-builder/factories/workspace-migration-object.factory.ts index a26ed264b..9c820ec66 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/factories/object-workspace-migration.factory.ts +++ b/packages/twenty-server/src/workspace/workspace-migration-builder/factories/workspace-migration-object.factory.ts @@ -1,5 +1,7 @@ import { Injectable } from '@nestjs/common'; +import { WorkspaceMigrationBuilderAction } from 'src/workspace/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface'; + import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity'; import { @@ -12,40 +14,23 @@ import { WorkspaceMigrationFactory } from 'src/metadata/workspace-migration/work import { generateMigrationName } from 'src/metadata/workspace-migration/utils/generate-migration-name.util'; @Injectable() -export class ObjectWorkspaceMigrationFactory { +export class WorkspaceMigrationObjectFactory { constructor( private readonly workspaceMigrationFactory: WorkspaceMigrationFactory, ) {} async create( - createObjectMetadataCollection: ObjectMetadataEntity[], - deleteObjectMetadataCollection: ObjectMetadataEntity[], + objectMetadataCollection: ObjectMetadataEntity[], + action: WorkspaceMigrationBuilderAction, ): Promise[]> { - const workspaceMigrations: Partial[] = []; - - /** - * Create object migrations - */ - if (createObjectMetadataCollection.length > 0) { - const createObjectWorkspaceMigrations = await this.createObjectMigration( - createObjectMetadataCollection, - ); - - workspaceMigrations.push(...createObjectWorkspaceMigrations); + switch (action) { + case WorkspaceMigrationBuilderAction.CREATE: + return this.createObjectMigration(objectMetadataCollection); + case WorkspaceMigrationBuilderAction.DELETE: + return this.deleteObjectMigration(objectMetadataCollection); + default: + return []; } - - /** - * Delete object migrations - */ - if (deleteObjectMetadataCollection.length > 0) { - const deleteObjectWorkspaceMigrations = await this.deleteObjectMigration( - deleteObjectMetadataCollection, - ); - - workspaceMigrations.push(...deleteObjectWorkspaceMigrations); - } - - return workspaceMigrations; } private async createObjectMigration( diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/factories/relation-workspace-migration.factory.ts b/packages/twenty-server/src/workspace/workspace-migration-builder/factories/workspace-migration-relation.factory.ts similarity index 87% rename from packages/twenty-server/src/workspace/workspace-sync-metadata/factories/relation-workspace-migration.factory.ts rename to packages/twenty-server/src/workspace/workspace-migration-builder/factories/workspace-migration-relation.factory.ts index 1d35319b8..bbf0891cb 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/factories/relation-workspace-migration.factory.ts +++ b/packages/twenty-server/src/workspace/workspace-migration-builder/factories/workspace-migration-relation.factory.ts @@ -1,5 +1,7 @@ import { Injectable } from '@nestjs/common'; +import { WorkspaceMigrationBuilderAction } from 'src/workspace/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface'; + import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity'; import { WorkspaceMigrationColumnActionType, @@ -15,7 +17,7 @@ import { camelCase } from 'src/utils/camel-case'; import { generateMigrationName } from 'src/metadata/workspace-migration/utils/generate-migration-name.util'; @Injectable() -export class RelationWorkspaceMigrationFactory { +export class WorkspaceMigrationRelationFactory { constructor() {} /** @@ -23,9 +25,9 @@ export class RelationWorkspaceMigrationFactory { */ async create( originalObjectMetadataCollection: ObjectMetadataEntity[], - createRelationMetadataCollection: RelationMetadataEntity[], + relationMetadataCollection: RelationMetadataEntity[], + action: WorkspaceMigrationBuilderAction, ): Promise[]> { - const workspaceMigrations: Partial[] = []; const originalObjectMetadataMap = originalObjectMetadataCollection.reduce( (result, currentObject) => { result[currentObject.id] = currentObject; @@ -35,17 +37,15 @@ export class RelationWorkspaceMigrationFactory { {} as Record, ); - if (createRelationMetadataCollection.length > 0) { - const createRelationWorkspaceMigrations = - await this.createRelationMigration( + switch (action) { + case WorkspaceMigrationBuilderAction.CREATE: + return this.createRelationMigration( originalObjectMetadataMap, - createRelationMetadataCollection, + relationMetadataCollection, ); - - workspaceMigrations.push(...createRelationWorkspaceMigrations); + default: + return []; } - - return workspaceMigrations; } private async createRelationMigration( diff --git a/packages/twenty-server/src/workspace/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface.ts b/packages/twenty-server/src/workspace/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface.ts new file mode 100644 index 000000000..c498f5335 --- /dev/null +++ b/packages/twenty-server/src/workspace/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface.ts @@ -0,0 +1,5 @@ +export enum WorkspaceMigrationBuilderAction { + CREATE = 'create', + UPDATE = 'update', + DELETE = 'delete', +} diff --git a/packages/twenty-server/src/workspace/workspace-migration-builder/workspace-migration-builder.module.ts b/packages/twenty-server/src/workspace/workspace-migration-builder/workspace-migration-builder.module.ts new file mode 100644 index 000000000..fcd035f3b --- /dev/null +++ b/packages/twenty-server/src/workspace/workspace-migration-builder/workspace-migration-builder.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; + +import { WorkspaceMigrationModule } from 'src/metadata/workspace-migration/workspace-migration.module'; + +import { workspaceMigrationBuilderFactories } from './factories'; + +@Module({ + imports: [WorkspaceMigrationModule], + providers: [...workspaceMigrationBuilderFactories], + exports: [...workspaceMigrationBuilderFactories], +}) +export class WorkspaceMigrationBuilderModule {} diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/factories/index.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/factories/index.ts index 3af888db3..b1c95a507 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/factories/index.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/factories/index.ts @@ -1,15 +1,9 @@ import { FeatureFlagFactory } from './feature-flags.factory'; import { StandardObjectFactory } from './standard-object.factory'; import { StandardRelationFactory } from './standard-relation.factory'; -import { ObjectWorkspaceMigrationFactory } from './object-workspace-migration.factory'; -import { FieldWorkspaceMigrationFactory } from './field-workspace-migration.factory'; -import { RelationWorkspaceMigrationFactory } from './relation-workspace-migration.factory'; export const workspaceSyncMetadataFactories = [ FeatureFlagFactory, StandardObjectFactory, StandardRelationFactory, - ObjectWorkspaceMigrationFactory, - FieldWorkspaceMigrationFactory, - RelationWorkspaceMigrationFactory, ]; diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/services/workspace-sync-object-metadata.service.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/services/workspace-sync-object-metadata.service.ts index 88262e722..4e4eacb43 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/services/workspace-sync-object-metadata.service.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/services/workspace-sync-object-metadata.service.ts @@ -5,6 +5,7 @@ import { EntityManager } from 'typeorm'; import { WorkspaceSyncContext } from 'src/workspace/workspace-sync-metadata/interfaces/workspace-sync-context.interface'; import { ComparatorAction } from 'src/workspace/workspace-sync-metadata/interfaces/comparator.interface'; import { FeatureFlagMap } from 'src/core/feature-flag/interfaces/feature-flag-map.interface'; +import { WorkspaceMigrationBuilderAction } from 'src/workspace/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface'; import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity'; import { mapObjectMetadataByUniqueIdentifier } from 'src/workspace/workspace-sync-metadata/utils/sync-metadata.util'; @@ -14,8 +15,8 @@ import { WorkspaceObjectComparator } from 'src/workspace/workspace-sync-metadata import { WorkspaceFieldComparator } from 'src/workspace/workspace-sync-metadata/comparators/workspace-field.comparator'; import { WorkspaceMetadataUpdaterService } from 'src/workspace/workspace-sync-metadata/services/workspace-metadata-updater.service'; import { WorkspaceSyncStorage } from 'src/workspace/workspace-sync-metadata/storage/workspace-sync.storage'; -import { ObjectWorkspaceMigrationFactory } from 'src/workspace/workspace-sync-metadata/factories/object-workspace-migration.factory'; -import { FieldWorkspaceMigrationFactory } from 'src/workspace/workspace-sync-metadata/factories/field-workspace-migration.factory'; +import { WorkspaceMigrationObjectFactory } from 'src/workspace/workspace-migration-builder/factories/workspace-migration-object.factory'; +import { WorkspaceMigrationFieldFactory } from 'src/workspace/workspace-migration-builder/factories/workspace-migration-field.factory'; @Injectable() export class WorkspaceSyncObjectMetadataService { @@ -26,8 +27,8 @@ export class WorkspaceSyncObjectMetadataService { private readonly workspaceObjectComparator: WorkspaceObjectComparator, private readonly workspaceFieldComparator: WorkspaceFieldComparator, private readonly workspaceMetadataUpdaterService: WorkspaceMetadataUpdaterService, - private readonly objectWorkspaceMigrationFactory: ObjectWorkspaceMigrationFactory, - private readonly fieldWorkspaceMigrationFactory: FieldWorkspaceMigrationFactory, + private readonly workspaceMigrationObjectFactory: WorkspaceMigrationObjectFactory, + private readonly workspaceMigrationFieldFactory: WorkspaceMigrationFieldFactory, ) {} async synchronize( @@ -140,21 +141,39 @@ export class WorkspaceSyncObjectMetadataService { this.logger.log('Generating migrations'); // Create migrations - const objectWorkspaceMigrations = - await this.objectWorkspaceMigrationFactory.create( + const createObjectWorkspaceMigrations = + await this.workspaceMigrationObjectFactory.create( metadataObjectUpdaterResult.createdObjectMetadataCollection, - storage.objectMetadataDeleteCollection, + WorkspaceMigrationBuilderAction.CREATE, ); - const fieldWorkspaceMigrations = - await this.fieldWorkspaceMigrationFactory.create( + const deleteObjectWorkspaceMigrations = + await this.workspaceMigrationObjectFactory.create( + storage.objectMetadataDeleteCollection, + WorkspaceMigrationBuilderAction.DELETE, + ); + + const createFieldWorkspaceMigrations = + await this.workspaceMigrationFieldFactory.create( originalObjectMetadataCollection, metadataFieldUpdaterResult.createdFieldMetadataCollection, + WorkspaceMigrationBuilderAction.CREATE, + ); + + const deleteFieldWorkspaceMigrations = + await this.workspaceMigrationFieldFactory.create( + originalObjectMetadataCollection, storage.fieldMetadataDeleteCollection, + WorkspaceMigrationBuilderAction.DELETE, ); this.logger.log('Saving migrations'); - return [...objectWorkspaceMigrations, ...fieldWorkspaceMigrations]; + return [ + ...createObjectWorkspaceMigrations, + ...deleteObjectWorkspaceMigrations, + ...createFieldWorkspaceMigrations, + ...deleteFieldWorkspaceMigrations, + ]; } } diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/services/workspace-sync-relation-metadata.service.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/services/workspace-sync-relation-metadata.service.ts index 90b48b64d..8f4d274c0 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/services/workspace-sync-relation-metadata.service.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/services/workspace-sync-relation-metadata.service.ts @@ -5,6 +5,7 @@ import { EntityManager } from 'typeorm'; import { WorkspaceSyncContext } from 'src/workspace/workspace-sync-metadata/interfaces/workspace-sync-context.interface'; import { FeatureFlagMap } from 'src/core/feature-flag/interfaces/feature-flag-map.interface'; import { ComparatorAction } from 'src/workspace/workspace-sync-metadata/interfaces/comparator.interface'; +import { WorkspaceMigrationBuilderAction } from 'src/workspace/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface'; import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity'; import { RelationMetadataEntity } from 'src/metadata/relation-metadata/relation-metadata.entity'; @@ -14,7 +15,7 @@ import { WorkspaceRelationComparator } from 'src/workspace/workspace-sync-metada import { WorkspaceMetadataUpdaterService } from 'src/workspace/workspace-sync-metadata/services/workspace-metadata-updater.service'; import { WorkspaceMigrationEntity } from 'src/metadata/workspace-migration/workspace-migration.entity'; import { WorkspaceSyncStorage } from 'src/workspace/workspace-sync-metadata/storage/workspace-sync.storage'; -import { RelationWorkspaceMigrationFactory } from 'src/workspace/workspace-sync-metadata/factories/relation-workspace-migration.factory'; +import { WorkspaceMigrationRelationFactory } from 'src/workspace/workspace-migration-builder/factories/workspace-migration-relation.factory'; @Injectable() export class WorkspaceSyncRelationMetadataService { @@ -26,7 +27,7 @@ export class WorkspaceSyncRelationMetadataService { private readonly standardRelationFactory: StandardRelationFactory, private readonly workspaceRelationComparator: WorkspaceRelationComparator, private readonly workspaceMetadataUpdaterService: WorkspaceMetadataUpdaterService, - private readonly relationWorkspaceMigrationFactory: RelationWorkspaceMigrationFactory, + private readonly workspaceMigrationRelationFactory: WorkspaceMigrationRelationFactory, ) {} async synchronize( @@ -95,12 +96,13 @@ export class WorkspaceSyncRelationMetadataService { ); // Create migrations - const workspaceRelationMigrations = - await this.relationWorkspaceMigrationFactory.create( + const createRelationWorkspaceMigrations = + await this.workspaceMigrationRelationFactory.create( originalObjectMetadataCollection, metadataRelationUpdaterResult.createdRelationMetadataCollection, + WorkspaceMigrationBuilderAction.CREATE, ); - return workspaceRelationMigrations; + return createRelationWorkspaceMigrations; } } diff --git a/packages/twenty-server/src/workspace/workspace-sync-metadata/workspace-sync-metadata.module.ts b/packages/twenty-server/src/workspace/workspace-sync-metadata/workspace-sync-metadata.module.ts index fc3c8f71a..05b7df093 100644 --- a/packages/twenty-server/src/workspace/workspace-sync-metadata/workspace-sync-metadata.module.ts +++ b/packages/twenty-server/src/workspace/workspace-sync-metadata/workspace-sync-metadata.module.ts @@ -6,7 +6,6 @@ import { FieldMetadataEntity } from 'src/metadata/field-metadata/field-metadata. import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity'; import { RelationMetadataEntity } from 'src/metadata/relation-metadata/relation-metadata.entity'; import { WorkspaceMigrationEntity } from 'src/metadata/workspace-migration/workspace-migration.entity'; -import { WorkspaceMigrationModule } from 'src/metadata/workspace-migration/workspace-migration.module'; import { WorkspaceMigrationRunnerModule } from 'src/workspace/workspace-migration-runner/workspace-migration-runner.module'; import { WorkspaceSyncMetadataService } from 'src/workspace/workspace-sync-metadata/workspace-sync-metadata.service'; import { workspaceSyncMetadataFactories } from 'src/workspace/workspace-sync-metadata/factories'; @@ -15,10 +14,11 @@ import { WorkspaceMetadataUpdaterService } from 'src/workspace/workspace-sync-me import { WorkspaceSyncObjectMetadataService } from 'src/workspace/workspace-sync-metadata/services/workspace-sync-object-metadata.service'; import { WorkspaceSyncRelationMetadataService } from 'src/workspace/workspace-sync-metadata/services/workspace-sync-relation-metadata.service'; import { WorkspaceLogsService } from 'src/workspace/workspace-sync-metadata/services/workspace-logs.service'; +import { WorkspaceMigrationBuilderModule } from 'src/workspace/workspace-migration-builder/workspace-migration-builder.module'; @Module({ imports: [ - WorkspaceMigrationModule, + WorkspaceMigrationBuilderModule, WorkspaceMigrationRunnerModule, TypeOrmModule.forFeature( [