diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-upgrade-version-command.module.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-upgrade-version-command.module.ts index ef27428f0a..74171fbf86 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-upgrade-version-command.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-upgrade-version-command.module.ts @@ -22,6 +22,7 @@ import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-chan import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity'; import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; +import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module'; import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity'; import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity'; import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module'; @@ -40,6 +41,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace UserWorkspaceEntity, ]), DataSourceModule, + FieldMetadataModule, WorkspaceCacheModule, ApplicationModule, WorkspaceMigrationModule, diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-backfill-message-thread-subject.command.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-backfill-message-thread-subject.command.ts index 922d5eb6b7..e0a0830100 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-backfill-message-thread-subject.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/1-21/1-21-workspace-command-backfill-message-thread-subject.command.ts @@ -1,18 +1,29 @@ import { Command } from 'nest-commander'; +import { STANDARD_OBJECTS } from 'twenty-shared/metadata'; +import { FieldMetadataType } from 'twenty-shared/types'; import { ActiveOrSuspendedWorkspaceCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspace.command-runner'; import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service'; import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner'; +import { ApplicationService } from 'src/engine/core-modules/application/application.service'; +import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier.util'; +import { getDefaultFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/get-default-flat-field-metadata-from-create-field-input.util'; +import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { getWorkspaceSchemaName } from 'src/engine/workspace-datasource/utils/get-workspace-schema-name.util'; +import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service'; @Command({ name: 'upgrade:1-21:backfill-message-thread-subject', description: - 'Backfill messageThread.subject from the most recently received message in each thread', + 'Create the messageThread.subject standard field if missing and backfill it from the most recently received message in each thread', }) export class BackfillMessageThreadSubjectCommand extends ActiveOrSuspendedWorkspaceCommandRunner { constructor( protected readonly workspaceIteratorService: WorkspaceIteratorService, + private readonly applicationService: ApplicationService, + private readonly workspaceCacheService: WorkspaceCacheService, + private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, ) { super(workspaceIteratorService); } @@ -28,7 +39,10 @@ export class BackfillMessageThreadSubjectCommand extends ActiveOrSuspendedWorksp return; } - const schemaName = getWorkspaceSchemaName(workspaceId); + await this.ensureSubjectFieldExists({ + workspaceId, + isDryRun: !!options.dryRun, + }); if (options.dryRun) { this.logger.log( @@ -38,23 +52,7 @@ export class BackfillMessageThreadSubjectCommand extends ActiveOrSuspendedWorksp return; } - const columnExists = await dataSource.query( - `SELECT 1 FROM information_schema.columns - WHERE table_schema = $1 - AND table_name = 'messageThread' - AND column_name = 'subject'`, - [schemaName], - undefined, - { shouldBypassPermissionChecks: true }, - ); - - if (columnExists.length === 0) { - this.logger.log( - `Column "subject" does not exist yet on messageThread for workspace ${workspaceId}, skipping (will be created by sync-metadata)`, - ); - - return; - } + const schemaName = getWorkspaceSchemaName(workspaceId); const result = await dataSource.query( `UPDATE "${schemaName}"."messageThread" mt @@ -75,4 +73,105 @@ export class BackfillMessageThreadSubjectCommand extends ActiveOrSuspendedWorksp `Backfilled subject for ${result?.[1] ?? 0} message threads in workspace ${workspaceId}`, ); } + + private async ensureSubjectFieldExists({ + workspaceId, + isDryRun, + }: { + workspaceId: string; + isDryRun: boolean; + }): Promise { + const { flatObjectMetadataMaps, flatFieldMetadataMaps } = + await this.workspaceCacheService.getOrRecompute(workspaceId, [ + 'flatObjectMetadataMaps', + 'flatFieldMetadataMaps', + ]); + + const messageThreadObjectMetadata = + findFlatEntityByUniversalIdentifier({ + flatEntityMaps: flatObjectMetadataMaps, + universalIdentifier: STANDARD_OBJECTS.messageThread.universalIdentifier, + }); + + if (!messageThreadObjectMetadata) { + this.logger.log( + `messageThread object metadata not found for workspace ${workspaceId}, skipping`, + ); + + return; + } + + const existingField = findFlatEntityByUniversalIdentifier({ + flatEntityMaps: flatFieldMetadataMaps, + universalIdentifier: + STANDARD_OBJECTS.messageThread.fields.subject.universalIdentifier, + }); + + if (existingField) { + return; + } + + if (isDryRun) { + this.logger.log( + `[DRY RUN] Would create messageThread.subject field for workspace ${workspaceId}`, + ); + + return; + } + + const { twentyStandardFlatApplication } = + await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( + { workspaceId }, + ); + + const flatFieldMetadataToCreate = { + ...getDefaultFlatFieldMetadata({ + createFieldInput: { + name: 'subject', + type: FieldMetadataType.TEXT, + label: 'Subject', + description: 'Subject', + icon: 'IconMessage', + isNullable: true, + isUIReadOnly: true, + universalIdentifier: + STANDARD_OBJECTS.messageThread.fields.subject.universalIdentifier, + }, + flatApplication: twentyStandardFlatApplication, + objectMetadataUniversalIdentifier: + messageThreadObjectMetadata.universalIdentifier, + }), + isCustom: false, + }; + + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + allFlatEntityOperationByMetadataName: { + fieldMetadata: { + flatEntityToCreate: [flatFieldMetadataToCreate], + flatEntityToDelete: [], + flatEntityToUpdate: [], + }, + }, + workspaceId, + applicationUniversalIdentifier: + twentyStandardFlatApplication.universalIdentifier, + }, + ); + + if (validateAndBuildResult.status === 'fail') { + this.logger.error( + `Failed to create messageThread.subject field for workspace ${workspaceId}:\n${JSON.stringify(validateAndBuildResult, null, 2)}`, + ); + + throw new Error( + `Failed to create messageThread.subject field for workspace ${workspaceId}`, + ); + } + + this.logger.log( + `Created messageThread.subject field for workspace ${workspaceId}`, + ); + } }