From b33b468679b964d02c25960455602e5986d442ed Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Fri, 5 Jul 2024 12:27:12 +0200 Subject: [PATCH] Add command to update boolean fields null values (#6113) We have recently decided that boolean fields should only accept truthy or falsy value, with users deciding of a default value at creation. This command helps cleaning the existing data, by 1. updating all boolean fields default values from null to false 2. updating all boolean fields values for records from null to false --------- Co-authored-by: Weiko --- ...-default-values-and-null-values.command.ts | 159 ++++++++++++++++++ .../commands/database-command.module.ts | 2 + .../calendar-event.workspace-entity.ts | 2 + 3 files changed, 163 insertions(+) create mode 100644 packages/twenty-server/src/database/commands/0-22-update-boolean-fields-null-default-values-and-null-values.command.ts diff --git a/packages/twenty-server/src/database/commands/0-22-update-boolean-fields-null-default-values-and-null-values.command.ts b/packages/twenty-server/src/database/commands/0-22-update-boolean-fields-null-default-values-and-null-values.command.ts new file mode 100644 index 000000000..89b18dde9 --- /dev/null +++ b/packages/twenty-server/src/database/commands/0-22-update-boolean-fields-null-default-values-and-null-values.command.ts @@ -0,0 +1,159 @@ +import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; +import { Logger } from '@nestjs/common'; + +import { Command, CommandRunner, Option } from 'nest-commander'; +import { Repository, IsNull, DataSource } from 'typeorm'; +import chalk from 'chalk'; + +import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { TypeORMService } from 'src/database/typeorm/typeorm.service'; +import { + FieldMetadataEntity, + FieldMetadataType, +} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service'; +import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; + +interface UpdateBooleanFieldsNullDefaultValuesAndNullValuesCommandOptions { + workspaceId?: string; +} + +@Command({ + name: 'migrate-0.22:update-boolean-field-null-default-values-and-null-values', + description: + 'Update boolean fields null default values and null values to false', +}) +export class UpdateBooleanFieldsNullDefaultValuesAndNullValuesCommand extends CommandRunner { + private readonly logger = new Logger( + UpdateBooleanFieldsNullDefaultValuesAndNullValuesCommand.name, + ); + constructor( + @InjectRepository(Workspace, 'core') + private readonly workspaceRepository: Repository, + private readonly typeORMService: TypeORMService, + private readonly dataSourceService: DataSourceService, + private readonly workspaceCacheVersionService: WorkspaceCacheVersionService, + @InjectDataSource('metadata') + private readonly metadataDataSource: DataSource, + ) { + super(); + } + + @Option({ + flags: '-w, --workspace-id [workspace_id]', + description: 'workspace id. Command runs on all workspaces if not provided', + required: false, + }) + parseWorkspaceId(value: string): string { + return value; + } + + async run( + _passedParam: string[], + options: UpdateBooleanFieldsNullDefaultValuesAndNullValuesCommandOptions, + ): Promise { + const workspaceIds = options.workspaceId + ? [options.workspaceId] + : (await this.workspaceRepository.find()).map( + (workspace) => workspace.id, + ); + + if (!workspaceIds.length) { + this.logger.log(chalk.yellow('No workspace found')); + + return; + } + + this.logger.log( + chalk.green(`Running command on ${workspaceIds.length} workspaces`), + ); + + for (const workspaceId of workspaceIds) { + const dataSourceMetadata = + await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceId( + workspaceId, + ); + + if (!dataSourceMetadata) { + throw new Error( + `Could not find dataSourceMetadata for workspace ${workspaceId}`, + ); + } + + const workspaceDataSource = + await this.typeORMService.connectToDataSource(dataSourceMetadata); + + if (!workspaceDataSource) { + throw new Error( + `Could not connect to dataSource for workspace ${workspaceId}`, + ); + } + + const workspaceQueryRunner = workspaceDataSource.createQueryRunner(); + const metadataQueryRunner = this.metadataDataSource.createQueryRunner(); + + await workspaceQueryRunner.connect(); + await metadataQueryRunner.connect(); + + await workspaceQueryRunner.startTransaction(); + await metadataQueryRunner.startTransaction(); + + try { + const fieldMetadataRepository = + metadataQueryRunner.manager.getRepository(FieldMetadataEntity); + + const booleanFieldsWithoutDefaultValue = + await fieldMetadataRepository.find({ + where: { + workspaceId, + type: FieldMetadataType.BOOLEAN, + defaultValue: IsNull(), + }, + relations: ['object'], + }); + + for (const booleanField of booleanFieldsWithoutDefaultValue) { + if (!booleanField.object) { + throw new Error( + `Could not find objectMetadataItem for field ${booleanField.id}`, + ); + } + + // Could be done via a batch update but it's safer in this context to run it sequentially with the ALTER TABLE + await fieldMetadataRepository.update(booleanField.id, { + defaultValue: false, + }); + + const fieldName = booleanField.name; + const tableName = computeObjectTargetTable(booleanField.object); + + await workspaceQueryRunner.query( + `ALTER TABLE "${dataSourceMetadata.schema}"."${tableName}" ALTER COLUMN "${fieldName}" SET DEFAULT false;`, + ); + } + + await workspaceQueryRunner.commitTransaction(); + await metadataQueryRunner.commitTransaction(); + } catch (error) { + await workspaceQueryRunner.rollbackTransaction(); + await metadataQueryRunner.rollbackTransaction(); + this.logger.log( + chalk.red(`Running command on workspace ${workspaceId} failed`), + ); + throw error; + } finally { + await workspaceQueryRunner.release(); + await metadataQueryRunner.release(); + } + + await this.workspaceCacheVersionService.incrementVersion(workspaceId); + + this.logger.log( + chalk.green(`Running command on workspace ${workspaceId} done`), + ); + } + + this.logger.log(chalk.green(`Command completed!`)); + } +} diff --git a/packages/twenty-server/src/database/commands/database-command.module.ts b/packages/twenty-server/src/database/commands/database-command.module.ts index d829bcf75..f673ca393 100644 --- a/packages/twenty-server/src/database/commands/database-command.module.ts +++ b/packages/twenty-server/src/database/commands/database-command.module.ts @@ -21,6 +21,7 @@ import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/ import { WorkspaceCacheVersionModule } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.module'; import { UpdateMessageChannelSyncStatusEnumCommand } from 'src/database/commands/0-20-update-message-channel-sync-status-enum.command'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { UpdateBooleanFieldsNullDefaultValuesAndNullValuesCommand } from 'src/database/commands/0-22-update-boolean-fields-null-default-values-and-null-values.command'; @Module({ imports: [ @@ -48,6 +49,7 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat StopDataSeedDemoWorkspaceCronCommand, UpdateMessageChannelVisibilityEnumCommand, UpdateMessageChannelSyncStatusEnumCommand, + UpdateBooleanFieldsNullDefaultValuesAndNullValuesCommand, ], }) export class DatabaseCommandModule {} diff --git a/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-event.workspace-entity.ts b/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-event.workspace-entity.ts index a4609608a..563c85391 100644 --- a/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-event.workspace-entity.ts +++ b/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-event.workspace-entity.ts @@ -44,6 +44,7 @@ export class CalendarEventWorkspaceEntity extends BaseWorkspaceEntity { label: 'Is canceled', description: 'Is canceled', icon: 'IconCalendarCancel', + defaultValue: false, }) isCanceled: boolean; @@ -53,6 +54,7 @@ export class CalendarEventWorkspaceEntity extends BaseWorkspaceEntity { label: 'Is Full Day', description: 'Is Full Day', icon: 'Icon24Hours', + defaultValue: false, }) isFullDay: boolean;