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;