From 00e970bdf4ff3675af3ccad6757d225ff68b8ca0 Mon Sep 17 00:00:00 2001 From: Etienne <45695613+etiennejouan@users.noreply.github.com> Date: Tue, 2 Dec 2025 13:39:39 +0100 Subject: [PATCH] Null - Second command - Cleaning remaining empty values (#16241) Because of [this code I forget to clean](https://github.com/twentyhq/twenty/pull/16217), - __workspace created between 1.12.0 and 1.12.2 (from friday 28 nov PM > monday 1 dec PM)__ have standard objects with empty string default value set on related TEXT type column (but default value null on field metadata) (ex: jobTitle on person) - __custom objects created between 1.12.0 and 1.12.2 (from friday 28 nov PM > monday 1 dec PM)__ have "name" TEXT field with empty string default value set + NOT NULL constraint on column Command cleans data and updates table structure --- ...mpty-string-null-in-text-fields.command.ts | 200 ++++++++++++++++++ .../1-13-upgrade-version-command.module.ts | 22 ++ .../upgrade-version-command.module.ts | 2 + 3 files changed, 224 insertions(+) create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-clean-empty-string-null-in-text-fields.command.ts create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-upgrade-version-command.module.ts diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-clean-empty-string-null-in-text-fields.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-clean-empty-string-null-in-text-fields.command.ts new file mode 100644 index 0000000000..4455926a69 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-clean-empty-string-null-in-text-fields.command.ts @@ -0,0 +1,200 @@ +import { Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Command } from 'nest-commander'; +import { FieldMetadataType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { Repository } from 'typeorm'; + +import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; +import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; +import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util'; + +@Command({ + name: 'upgrade:1-13:clean-empty-string-null-in-text-fields', + description: + 'Clean up empty string defaults in TEXT fields and convert them to NULL', +}) +export class CleanEmptyStringNullInTextFieldsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { + protected readonly logger = new Logger( + CleanEmptyStringNullInTextFieldsCommand.name, + ); + + constructor( + @InjectRepository(WorkspaceEntity) + protected readonly workspaceRepository: Repository, + protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + @InjectRepository(ObjectMetadataEntity) + protected readonly objectMetadataRepository: Repository, + @InjectRepository(FieldMetadataEntity) + protected readonly fieldMetadataRepository: Repository, + protected readonly dataSourceService: DataSourceService, + ) { + super(workspaceRepository, twentyORMGlobalManager, dataSourceService); + } + + override async runOnWorkspace({ + workspaceId, + options, + dataSource, + }: RunOnWorkspaceArgs): Promise { + const isDryRun = options.dryRun || false; + + if (!isDefined(dataSource)) { + throw new Error( + `Could not find data source for workspace ${workspaceId}, should never occur`, + ); + } + + const workspace = await this.workspaceRepository.findOne({ + where: { id: workspaceId }, + }); + + if (!isDefined(workspace)) { + throw new Error( + `Could not find workspace ${workspaceId}, should never occur`, + ); + } + if ( + !isDefined(workspace.version) || + !['1.12.0', '1.12.1'].includes(workspace.version) + ) { + this.logger.log( + `Workspace ${workspaceId} is not a v1.12.0 or v1.12.1 workspace, skipping`, + ); + + return; + } + + const schemaName = getWorkspaceSchemaName(workspaceId); + + if (isDryRun) { + this.logger.log('Dry run mode: No changes will be applied'); + } + + const objectMetadataItems = await this.objectMetadataRepository.find({ + where: { workspaceId }, + relations: ['fields'], + }); + + for (const objectMetadataItem of objectMetadataItems) { + const tableName = computeObjectTargetTable(objectMetadataItem); + + if (!objectMetadataItem.isCustom) { + await this.cleanUpEmptyStringDefaultsInTextFieldsInStandardObjects( + objectMetadataItem, + tableName, + schemaName, + dataSource, + isDryRun, + ); + } + + if (objectMetadataItem.isCustom) { + await this.cleanUpEmptyStringDefaultsAndSetNullableInNameFieldInCustomObjects( + objectMetadataItem, + tableName, + schemaName, + dataSource, + isDryRun, + ); + } + } + } + + private async cleanUpEmptyStringDefaultsInTextFieldsInStandardObjects( + objectMetadataItem: ObjectMetadataEntity, + tableName: string, + schemaName: string, + dataSource: WorkspaceDataSource, + isDryRun: boolean, + ): Promise { + const textFields = objectMetadataItem.fields.filter( + (field) => + field.type === FieldMetadataType.TEXT && + field.isNullable === true && + field.defaultValue === null, + ); + + for (const field of textFields) { + this.logger.log( + `Checking field ${field.name} on standard object ${objectMetadataItem.nameSingular} (Table: ${tableName})`, + ); + + this.logger.log( + `Cleaning up empty string default for field ${field.name} on ${tableName}`, + ); + + if (!isDryRun) { + await dataSource.query( + `ALTER TABLE "${schemaName}"."${tableName}" ALTER COLUMN "${field.name}" DROP DEFAULT`, + [], + undefined, + { shouldBypassPermissionChecks: true }, + ); + + await dataSource.query( + `UPDATE "${schemaName}"."${tableName}" SET "${field.name}" = NULL WHERE "${field.name}" = ''`, + [], + undefined, + { shouldBypassPermissionChecks: true }, + ); + } + } + } + + private async cleanUpEmptyStringDefaultsAndSetNullableInNameFieldInCustomObjects( + objectMetadataItem: ObjectMetadataEntity, + tableName: string, + schemaName: string, + dataSource: WorkspaceDataSource, + isDryRun: boolean, + ): Promise { + const nameField = objectMetadataItem.fields.find( + (field) => + field.name === 'name' && + field.type === FieldMetadataType.TEXT && + field.defaultValue === "''", + ); + + if (!isDefined(nameField)) { + return; + } + + this.logger.log( + `Found "name" field with empty string default on ${objectMetadataItem.nameSingular} (Table: ${tableName})`, + ); + + if (!isDryRun) { + await dataSource.query( + `ALTER TABLE "${schemaName}"."${tableName}" ALTER COLUMN "name" DROP DEFAULT, ALTER COLUMN "name" DROP NOT NULL`, + [], + undefined, + { shouldBypassPermissionChecks: true }, + ); + + await dataSource.query( + `UPDATE "${schemaName}"."${tableName}" SET "name" = NULL WHERE "name" = ''`, + [], + undefined, + { shouldBypassPermissionChecks: true }, + ); + + await this.fieldMetadataRepository.update(nameField.id, { + defaultValue: null, + isNullable: true, + }); + + this.logger.log( + `Updated "name" field metadata and cleaned empty strings for ${objectMetadataItem.nameSingular}`, + ); + } + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-upgrade-version-command.module.ts new file mode 100644 index 0000000000..c067da04ef --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-13/1-13-upgrade-version-command.module.ts @@ -0,0 +1,22 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { CleanEmptyStringNullInTextFieldsCommand } from 'src/database/commands/upgrade-version-command/1-13/1-13-clean-empty-string-null-in-text-fields.command'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; +import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + WorkspaceEntity, + ObjectMetadataEntity, + FieldMetadataEntity, + ]), + DataSourceModule, + ], + providers: [CleanEmptyStringNullInTextFieldsCommand], + exports: [CleanEmptyStringNullInTextFieldsCommand], +}) +export class V1_13_UpgradeVersionCommandModule {} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade-version-command.module.ts index b70bfad665..8f8dfdc7c2 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade-version-command.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/upgrade-version-command.module.ts @@ -4,6 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { V1_10_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-10/1-10-upgrade-version-command.module'; import { V1_11_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-11/1-11-upgrade-version-command.module'; import { V1_12_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-12/1-12-upgrade-version-command.module'; +import { V1_13_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-13/1-13-upgrade-version-command.module'; import { V1_6_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-6/1-6-upgrade-version-command.module'; import { V1_7_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-7/1-7-upgrade-version-command.module'; import { V1_8_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-8/1-8-upgrade-version-command.module'; @@ -21,6 +22,7 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp V1_10_UpgradeVersionCommandModule, V1_11_UpgradeVersionCommandModule, V1_12_UpgradeVersionCommandModule, + V1_13_UpgradeVersionCommandModule, DataSourceModule, WorkspaceSyncMetadataModule, ],