[REQUIRED_FOR_1_10] Fix kanban foreign key migration (#15557)

# Introduction
We introduced a foreign key addition that will fail in production due to
orphan views targetting non existing fields

## Migration
The migration will be run for any new workspace successfully or any
twenty instance without corrupted data

## Upgrade command
The upgrade command will at some point allow the migration to be run
manually after removing any corrupted data

## Release note
We should remove the migration we've manually set as being run in
production
This commit is contained in:
Paul Rastoin
2025-11-03 16:08:35 +01:00
committed by GitHub
parent 20576f2ee7
commit 17acfe1d2a
4 changed files with 75 additions and 3 deletions
@@ -0,0 +1,57 @@
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { DataSource, Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@Command({
name: '1-10-create-view-kanban-field-metadata-id-foreign-key-migration',
description:
'Create FK_b3cc95732479f7a1337350c398f foreign key on view kanban field metadata id column',
})
export class CreateViewKanbanFieldMetadataIdForeignKeyMigrationCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
private hasRunOnce = false;
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
@InjectDataSource()
private readonly coreDataSource: DataSource,
) {
super(workspaceRepository, twentyORMGlobalManager);
}
override async runOnWorkspace({
options,
}: RunOnWorkspaceArgs): Promise<void> {
if (this.hasRunOnce) {
this.logger.log('Skipping kanban field metadata id foreign key creation');
return;
}
if (!options.dryRun) {
try {
await this.coreDataSource.query(
`ALTER TABLE "core"."view" ADD CONSTRAINT "FK_b3cc95732479f7a1337350c398f" FOREIGN KEY ("kanbanAggregateOperationFieldMetadataId") REFERENCES "core"."fieldMetadata"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
this.logger.log(
'Successfully added foreign key constraint for kanbanAggregateOperationFieldMetadataId',
);
} catch (error) {
this.logger.log(
`Foreign key constraint already exists or could not be created (this is expected for subsequent workspaces): ${error.message}`,
);
}
}
this.hasRunOnce = true;
}
}
@@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { AddWorkflowRunStopStatusesCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-add-workflow-run-stop-statuses.command';
import { CleanOrphanedKanbanAggregateOperationFieldMetadataIdCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-clean-orphaned-kanban-aggregate-operation-field-metadata-id.command';
import { CreateViewKanbanFieldMetadataIdForeignKeyMigrationCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-create-view-kanban-field-metadata-id-foreign-key-migration.command';
import { MigrateAttachmentAuthorToCreatedByCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-author-to-created-by.command';
import { MigrateAttachmentTypeToFileCategoryCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-type-to-file-category.command';
import { MigrateChannelPartialFullSyncStagesCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-channel-partial-full-sync-stages.command';
@@ -32,6 +33,7 @@ import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-sc
RegenerateSearchVectorsCommand,
AddWorkflowRunStopStatusesCommand,
CleanOrphanedKanbanAggregateOperationFieldMetadataIdCommand,
CreateViewKanbanFieldMetadataIdForeignKeyMigrationCommand,
],
exports: [
MigrateAttachmentAuthorToCreatedByCommand,
@@ -40,6 +42,7 @@ import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-sc
AddWorkflowRunStopStatusesCommand,
CleanOrphanedKanbanAggregateOperationFieldMetadataIdCommand,
MigrateChannelPartialFullSyncStagesCommand,
CreateViewKanbanFieldMetadataIdForeignKeyMigrationCommand,
],
})
export class V1_10_UpgradeVersionCommandModule {}
@@ -11,6 +11,7 @@ import {
} from 'src/database/commands/command-runners/upgrade.command-runner';
import { AddWorkflowRunStopStatusesCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-add-workflow-run-stop-statuses.command';
import { CleanOrphanedKanbanAggregateOperationFieldMetadataIdCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-clean-orphaned-kanban-aggregate-operation-field-metadata-id.command';
import { CreateViewKanbanFieldMetadataIdForeignKeyMigrationCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-create-view-kanban-field-metadata-id-foreign-key-migration.command';
import { MigrateAttachmentAuthorToCreatedByCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-author-to-created-by.command';
import { MigrateAttachmentTypeToFileCategoryCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-attachment-type-to-file-category.command';
import { MigrateChannelPartialFullSyncStagesCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-channel-partial-full-sync-stages.command';
@@ -61,6 +62,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
protected readonly addWorkflowRunStopStatusesCommand: AddWorkflowRunStopStatusesCommand,
protected readonly cleanOrphanedKanbanAggregateOperationFieldMetadataIdCommand: CleanOrphanedKanbanAggregateOperationFieldMetadataIdCommand,
protected readonly migrateChannelPartialFullSyncStagesCommand: MigrateChannelPartialFullSyncStagesCommand,
protected readonly createViewKanbanFieldMetadataIdForeignKeyMigrationCommand: CreateViewKanbanFieldMetadataIdForeignKeyMigrationCommand,
) {
super(
workspaceRepository,
@@ -97,6 +99,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
this.regenerateSearchVectorsCommand,
this.addWorkflowRunStopStatusesCommand,
this.cleanOrphanedKanbanAggregateOperationFieldMetadataIdCommand,
this.createViewKanbanFieldMetadataIdForeignKeyMigrationCommand,
this.migrateChannelPartialFullSyncStagesCommand,
],
afterSyncMetadata: [
@@ -6,9 +6,18 @@ export class KanbanFieldMetadataIdentifierView1760965667836
name = 'KanbanFieldMetadataIdentifierView1760965667836';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."view" ADD CONSTRAINT "FK_b3cc95732479f7a1337350c398f" FOREIGN KEY ("kanbanAggregateOperationFieldMetadataId") REFERENCES "core"."fieldMetadata"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
// Swallowing any exceptions here
// 1-10-clean-orphaned-kanban-aggregate-operation-field-metadata-id.command.ts upgrade command handles fallback for existing workspaces
try {
await queryRunner.query(
`ALTER TABLE "core"."view" ADD CONSTRAINT "FK_b3cc95732479f7a1337350c398f" FOREIGN KEY ("kanbanAggregateOperationFieldMetadataId") REFERENCES "core"."fieldMetadata"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
} catch (error) {
// eslint-disable-next-line no-console
console.error(
`Swallowing KanbanFieldMetadataIdentifierView1760965667836 error: ${error.message}. Upgrade:1-10:clean-orphaned-kanban-aggregate-operation-field-metadata-id upgrade command will handle fallback`,
);
}
}
public async down(queryRunner: QueryRunner): Promise<void> {