From 7ec968d5a2701e0553f043159c8698ed1e9493ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20M?= Date: Thu, 8 Feb 2024 18:59:17 +0100 Subject: [PATCH] feat: workspace health type fix (#3890) * feat: workspace health type fix * Fix --------- Co-authored-by: Charles Bochet --- .../services/database-structure.service.ts | 21 +++++ .../services/workspace-fix-type.service.ts | 89 +++++++++++++++++++ .../services/workspace-fix.service.ts | 13 ++- .../is-workspace-health-issue-type.util.ts | 6 ++ .../workspace-health.module.ts | 2 + .../workspace-migration-type.service.ts | 35 ++++++++ .../workspace-migration-runner.module.ts | 8 +- .../workspace-migration-runner.service.ts | 19 ++++ 8 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 packages/twenty-server/src/workspace/workspace-health/services/workspace-fix-type.service.ts create mode 100644 packages/twenty-server/src/workspace/workspace-migration-runner/services/workspace-migration-type.service.ts diff --git a/packages/twenty-server/src/workspace/workspace-health/services/database-structure.service.ts b/packages/twenty-server/src/workspace/workspace-health/services/database-structure.service.ts index c80a57c0d..c5260ee7a 100644 --- a/packages/twenty-server/src/workspace/workspace-health/services/database-structure.service.ts +++ b/packages/twenty-server/src/workspace/workspace-health/services/database-structure.service.ts @@ -140,6 +140,27 @@ export class DatabaseStructureService { }); } + getFieldMetadataTypeFromPostgresDataType( + postgresDataType: string, + ): FieldMetadataType | null { + const mainDataSource = this.typeORMService.getMainDataSource(); + + for (const type in FieldMetadataType) { + const typeORMType = fieldMetadataTypeToColumnType( + FieldMetadataType[type], + ) as ColumnType; + const dataType = mainDataSource.driver.normalizeType({ + type: typeORMType, + }); + + if (postgresDataType === dataType) { + return FieldMetadataType[type]; + } + } + + return null; + } + getPostgresDefault( fieldMetadataType: FieldMetadataType, defaultValue: FieldMetadataDefaultValue | null, diff --git a/packages/twenty-server/src/workspace/workspace-health/services/workspace-fix-type.service.ts b/packages/twenty-server/src/workspace/workspace-health/services/workspace-fix-type.service.ts new file mode 100644 index 000000000..582eb552c --- /dev/null +++ b/packages/twenty-server/src/workspace/workspace-health/services/workspace-fix-type.service.ts @@ -0,0 +1,89 @@ +import { Injectable } from '@nestjs/common'; + +import { EntityManager } from 'typeorm'; + +import { + WorkspaceHealthColumnIssue, + WorkspaceHealthIssueType, +} from 'src/workspace/workspace-health/interfaces/workspace-health-issue.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 { WorkspaceMigrationEntity } from 'src/metadata/workspace-migration/workspace-migration.entity'; +import { WorkspaceMigrationFieldFactory } from 'src/workspace/workspace-migration-builder/factories/workspace-migration-field.factory'; + +import { DatabaseStructureService } from './database-structure.service'; + +type WorkspaceHealthTypeIssue = + WorkspaceHealthColumnIssue; + +@Injectable() +export class WorkspaceFixTypeService { + constructor( + private readonly workspaceMigrationFieldFactory: WorkspaceMigrationFieldFactory, + private readonly databaseStructureService: DatabaseStructureService, + ) {} + + async fix( + manager: EntityManager, + objectMetadataCollection: ObjectMetadataEntity[], + issues: WorkspaceHealthTypeIssue[], + ): Promise[]> { + const workspaceMigrations: Partial[] = []; + + for (const issue of issues) { + switch (issue.type) { + case WorkspaceHealthIssueType.COLUMN_DATA_TYPE_CONFLICT: { + const columnNullabilityWorkspaceMigrations = + await this.fixColumnTypeIssues( + objectMetadataCollection, + issues.filter( + (issue) => + issue.type === + WorkspaceHealthIssueType.COLUMN_DATA_TYPE_CONFLICT, + ) as WorkspaceHealthColumnIssue[], + ); + + workspaceMigrations.push(...columnNullabilityWorkspaceMigrations); + break; + } + } + } + + return workspaceMigrations; + } + + private async fixColumnTypeIssues( + objectMetadataCollection: ObjectMetadataEntity[], + issues: WorkspaceHealthColumnIssue[], + ): Promise[]> { + const fieldMetadataUpdateCollection = issues.map((issue) => { + if (!issue.columnStructure?.dataType) { + throw new Error('Column structure data type is missing'); + } + + const type = + this.databaseStructureService.getFieldMetadataTypeFromPostgresDataType( + issue.columnStructure?.dataType, + ); + + if (!type) { + throw new Error("Can't find field metadata type from column structure"); + } + + return { + current: { + ...issue.fieldMetadata, + type, + }, + altered: issue.fieldMetadata, + }; + }); + + return this.workspaceMigrationFieldFactory.create( + objectMetadataCollection, + fieldMetadataUpdateCollection, + WorkspaceMigrationBuilderAction.UPDATE, + ); + } +} 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 index 74b8c465c..9016fade9 100644 --- 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 @@ -7,14 +7,19 @@ import { WorkspaceHealthIssue } from 'src/workspace/workspace-health/interfaces/ 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 { + isWorkspaceHealthNullableIssue, + isWorkspaceHealthTypeIssue, +} from 'src/workspace/workspace-health/utils/is-workspace-health-issue-type.util'; import { WorkspaceFixNullableService } from './workspace-fix-nullable.service'; +import { WorkspaceFixTypeService } from './workspace-fix-type.service'; @Injectable() export class WorkspaceFixService { constructor( private readonly workspaceFixNullableService: WorkspaceFixNullableService, + private readonly workspaceFixTypeService: WorkspaceFixTypeService, ) {} async fix( @@ -30,6 +35,12 @@ export class WorkspaceFixService { isWorkspaceHealthNullableIssue(issue.type), ), }, + [WorkspaceHealthFixKind.Type]: { + service: this.workspaceFixTypeService, + issues: issues.filter((issue) => + isWorkspaceHealthTypeIssue(issue.type), + ), + }, }; return services[type].service.fix( 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 index f606a5c74..c7f843f9e 100644 --- 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 @@ -5,3 +5,9 @@ export const isWorkspaceHealthNullableIssue = ( ): type is WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT => { return type === WorkspaceHealthIssueType.COLUMN_NULLABILITY_CONFLICT; }; + +export const isWorkspaceHealthTypeIssue = ( + type: WorkspaceHealthIssueType, +): type is WorkspaceHealthIssueType.COLUMN_DATA_TYPE_CONFLICT => { + return type === WorkspaceHealthIssueType.COLUMN_DATA_TYPE_CONFLICT; +}; 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 88c63fe4f..6b55bdb8c 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 @@ -14,6 +14,7 @@ import { WorkspaceMigrationRunnerModule } from 'src/workspace/workspace-migratio import { WorkspaceFixService } from './services/workspace-fix.service'; import { WorkspaceFixNullableService } from './services/workspace-fix-nullable.service'; +import { WorkspaceFixTypeService } from './services/workspace-fix-type.service'; @Module({ imports: [ @@ -31,6 +32,7 @@ import { WorkspaceFixNullableService } from './services/workspace-fix-nullable.s FieldMetadataHealthService, RelationMetadataHealthService, WorkspaceFixNullableService, + WorkspaceFixTypeService, WorkspaceFixService, ], exports: [WorkspaceHealthService], diff --git a/packages/twenty-server/src/workspace/workspace-migration-runner/services/workspace-migration-type.service.ts b/packages/twenty-server/src/workspace/workspace-migration-runner/services/workspace-migration-type.service.ts new file mode 100644 index 000000000..d34778c4c --- /dev/null +++ b/packages/twenty-server/src/workspace/workspace-migration-runner/services/workspace-migration-type.service.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@nestjs/common'; + +import { QueryRunner } from 'typeorm'; + +import { WorkspaceMigrationColumnAlter } from 'src/metadata/workspace-migration/workspace-migration.entity'; + +@Injectable() +export class WorkspaceMigrationTypeService { + constructor() {} + + async alterType( + queryRunner: QueryRunner, + schemaName: string, + tableName: string, + migrationColumn: WorkspaceMigrationColumnAlter, + ) { + const columnDefinition = migrationColumn.alteredColumnDefinition; + + // Update the column type + // If casting is not possible, the query will fail + await queryRunner.query(` + ALTER TABLE "${schemaName}"."${tableName}" + ALTER COLUMN "${columnDefinition.columnName}" TYPE ${columnDefinition.columnType} + USING "${columnDefinition.columnName}"::${columnDefinition.columnType} + `); + + // Update the column default value + if (columnDefinition.defaultValue) { + await queryRunner.query(` + ALTER TABLE "${schemaName}"."${tableName}" + ALTER COLUMN "${columnDefinition.columnName}" SET DEFAULT ${columnDefinition.defaultValue}::${columnDefinition.columnType}; + `); + } + } +} diff --git a/packages/twenty-server/src/workspace/workspace-migration-runner/workspace-migration-runner.module.ts b/packages/twenty-server/src/workspace/workspace-migration-runner/workspace-migration-runner.module.ts index 81ec80f2b..257a6cf3c 100644 --- a/packages/twenty-server/src/workspace/workspace-migration-runner/workspace-migration-runner.module.ts +++ b/packages/twenty-server/src/workspace/workspace-migration-runner/workspace-migration-runner.module.ts @@ -7,13 +7,19 @@ import { WorkspaceMigrationEnumService } from 'src/workspace/workspace-migration import { WorkspaceMigrationRunnerService } from './workspace-migration-runner.service'; +import { WorkspaceMigrationTypeService } from './services/workspace-migration-type.service'; + @Module({ imports: [ WorkspaceDataSourceModule, WorkspaceMigrationModule, WorkspaceCacheVersionModule, ], - providers: [WorkspaceMigrationRunnerService, WorkspaceMigrationEnumService], + providers: [ + WorkspaceMigrationRunnerService, + WorkspaceMigrationEnumService, + WorkspaceMigrationTypeService, + ], exports: [WorkspaceMigrationRunnerService], }) export class WorkspaceMigrationRunnerModule {} diff --git a/packages/twenty-server/src/workspace/workspace-migration-runner/workspace-migration-runner.service.ts b/packages/twenty-server/src/workspace/workspace-migration-runner/workspace-migration-runner.service.ts index 7063a8ee0..ec63b1b66 100644 --- a/packages/twenty-server/src/workspace/workspace-migration-runner/workspace-migration-runner.service.ts +++ b/packages/twenty-server/src/workspace/workspace-migration-runner/workspace-migration-runner.service.ts @@ -22,6 +22,7 @@ import { WorkspaceCacheVersionService } from 'src/metadata/workspace-cache-versi import { WorkspaceMigrationEnumService } from 'src/workspace/workspace-migration-runner/services/workspace-migration-enum.service'; import { customTableDefaultColumns } from './utils/custom-table-default-column.util'; +import { WorkspaceMigrationTypeService } from './services/workspace-migration-type.service'; @Injectable() export class WorkspaceMigrationRunnerService { @@ -30,6 +31,7 @@ export class WorkspaceMigrationRunnerService { private readonly workspaceMigrationService: WorkspaceMigrationService, private readonly workspaceCacheVersionService: WorkspaceCacheVersionService, private readonly workspaceMigrationEnumService: WorkspaceMigrationEnumService, + private readonly workspaceMigrationTypeService: WorkspaceMigrationTypeService, ) {} /** @@ -275,6 +277,23 @@ export class WorkspaceMigrationRunnerService { ); } + if ( + migrationColumn.currentColumnDefinition.columnType !== + migrationColumn.alteredColumnDefinition.columnType + ) { + await this.workspaceMigrationTypeService.alterType( + queryRunner, + schemaName, + tableName, + migrationColumn, + ); + + migrationColumn.currentColumnDefinition.columnType = + migrationColumn.alteredColumnDefinition.columnType; + + return; + } + await queryRunner.changeColumn( `${schemaName}.${tableName}`, new TableColumn({