diff --git a/packages/twenty-server/src/workspace/workspace-health/services/workspace-fix-default-value.service.ts b/packages/twenty-server/src/workspace/workspace-health/services/workspace-fix-default-value.service.ts new file mode 100644 index 0000000000..c25260aaac --- /dev/null +++ b/packages/twenty-server/src/workspace/workspace-health/services/workspace-fix-default-value.service.ts @@ -0,0 +1,117 @@ +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 { FieldMetadataDefaultValue } from 'src/metadata/field-metadata/interfaces/field-metadata-default-value.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'; + +type WorkspaceHealthDefaultValueIssue = + WorkspaceHealthColumnIssue; + +@Injectable() +export class WorkspaceFixDefaultValueService { + constructor( + private readonly workspaceMigrationFieldFactory: WorkspaceMigrationFieldFactory, + ) {} + + async fix( + manager: EntityManager, + objectMetadataCollection: ObjectMetadataEntity[], + issues: WorkspaceHealthDefaultValueIssue[], + ): Promise[]> { + const workspaceMigrations: Partial[] = []; + + for (const issue of issues) { + switch (issue.type) { + case WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT: { + const columnNullabilityWorkspaceMigrations = + await this.fixColumnDefaultValueIssues( + objectMetadataCollection, + issues.filter( + (issue) => + issue.type === + WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT, + ) as WorkspaceHealthColumnIssue[], + ); + + workspaceMigrations.push(...columnNullabilityWorkspaceMigrations); + break; + } + } + } + + return workspaceMigrations; + } + + private async fixColumnDefaultValueIssues( + objectMetadataCollection: ObjectMetadataEntity[], + issues: WorkspaceHealthColumnIssue[], + ): Promise[]> { + const fieldMetadataUpdateCollection = issues.map((issue) => { + const oldDefaultValue = + this.computeFieldMetadataDefaultValueFromColumnDefault( + issue.columnStructure?.columnDefault, + ); + + return { + current: { + ...issue.fieldMetadata, + defaultValue: oldDefaultValue, + }, + altered: issue.fieldMetadata, + }; + }); + + return this.workspaceMigrationFieldFactory.create( + objectMetadataCollection, + fieldMetadataUpdateCollection, + WorkspaceMigrationBuilderAction.UPDATE, + ); + } + + private computeFieldMetadataDefaultValueFromColumnDefault( + columnDefault: string | undefined, + ): FieldMetadataDefaultValue<'default'> { + if ( + columnDefault === undefined || + columnDefault === null || + columnDefault === 'NULL' + ) { + return null; + } + + if (!isNaN(Number(columnDefault))) { + return { value: +columnDefault }; + } + + if (columnDefault === 'true') { + return { value: true }; + } + + if (columnDefault === 'false') { + return { value: false }; + } + + if (columnDefault === '') { + return { value: '' }; + } + + if (columnDefault === 'now()') { + return { type: 'now' }; + } + + if (columnDefault.startsWith('public.uuid_generate_v4')) { + return { type: 'uuid' }; + } + + return { value: columnDefault }; + } +} 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 9016fade97..ce55285ccb 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 @@ -8,18 +8,21 @@ 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 { + isWorkspaceHealthDefaultValueIssue, 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'; +import { WorkspaceFixDefaultValueService } from './workspace-fix-default-value.service'; @Injectable() export class WorkspaceFixService { constructor( private readonly workspaceFixNullableService: WorkspaceFixNullableService, private readonly workspaceFixTypeService: WorkspaceFixTypeService, + private readonly workspaceFixDefaultValueService: WorkspaceFixDefaultValueService, ) {} async fix( @@ -41,6 +44,12 @@ export class WorkspaceFixService { isWorkspaceHealthTypeIssue(issue.type), ), }, + [WorkspaceHealthFixKind.DefaultValue]: { + service: this.workspaceFixDefaultValueService, + issues: issues.filter((issue) => + isWorkspaceHealthDefaultValueIssue(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 c7f843f9ee..f0e1cb7f5a 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 @@ -11,3 +11,9 @@ export const isWorkspaceHealthTypeIssue = ( ): type is WorkspaceHealthIssueType.COLUMN_DATA_TYPE_CONFLICT => { return type === WorkspaceHealthIssueType.COLUMN_DATA_TYPE_CONFLICT; }; + +export const isWorkspaceHealthDefaultValueIssue = ( + type: WorkspaceHealthIssueType, +): type is WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_CONFLICT => { + return type === WorkspaceHealthIssueType.COLUMN_DEFAULT_VALUE_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 6b55bdb8cf..7b45f89356 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 @@ -15,6 +15,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'; +import { WorkspaceFixDefaultValueService } from './services/workspace-fix-default-value.service'; @Module({ imports: [ @@ -33,6 +34,7 @@ import { WorkspaceFixTypeService } from './services/workspace-fix-type.service'; RelationMetadataHealthService, WorkspaceFixNullableService, WorkspaceFixTypeService, + WorkspaceFixDefaultValueService, WorkspaceFixService, ], exports: [WorkspaceHealthService],