CleanOrphanedKanbanAggregateOperationFieldMetadataIdCommand (#15344)

## Context

We want to introduce a FK between view and fieldMetadata through
KanbanAggregateOperationFieldMetadataId so we need to clean up orphan
ones
This commit is contained in:
Weiko
2025-10-24 17:18:02 +02:00
committed by GitHub
parent f7a80d250b
commit 822b4d75a4
3 changed files with 122 additions and 0 deletions
@@ -0,0 +1,114 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { isDefined } from 'twenty-shared/utils';
import { In, IsNull, Not, 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 { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { ViewType } from 'src/engine/metadata-modules/view/enums/view-type.enum';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
@Command({
name: 'upgrade:1-10:clean-orphaned-kanban-aggregate-operation-field-metadata-id',
description:
"Delete all kanbanAggregateOperationFieldMetadataId in views that don't map to a real fieldMetadata (this is because we want to introduce a FK later)",
})
export class CleanOrphanedKanbanAggregateOperationFieldMetadataIdCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
@InjectRepository(ViewEntity)
private readonly viewRepository: Repository<ViewEntity>,
@InjectRepository(FieldMetadataEntity)
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
) {
super(workspaceRepository, twentyORMGlobalManager);
}
override async runOnWorkspace({
workspaceId,
index,
total,
options,
}: RunOnWorkspaceArgs): Promise<void> {
this.logger.log(
`[${index + 1}/${total}] Cleaning orphaned kanbanAggregateOperationFieldMetadataId for workspace ${workspaceId}`,
);
const existingFieldMetadataIds = await this.fieldMetadataRepository.find({
where: { workspaceId },
select: ['id'],
});
const existingFieldMetadataIdSet = new Set(
existingFieldMetadataIds.map((fm) => fm.id),
);
const viewsWithOrphanedFieldMetadata = await this.viewRepository.find({
where: {
workspaceId,
kanbanAggregateOperationFieldMetadataId: Not(IsNull()),
},
select: ['id', 'kanbanAggregateOperationFieldMetadataId', 'type'],
});
const orphanedViews = viewsWithOrphanedFieldMetadata.filter(
(view) =>
isDefined(view.kanbanAggregateOperationFieldMetadataId) &&
!existingFieldMetadataIdSet.has(
view.kanbanAggregateOperationFieldMetadataId,
),
);
if (orphanedViews.length === 0) {
this.logger.log(
'No orphaned kanbanAggregateOperationFieldMetadataId references found',
);
return;
}
const kanbanViewsToDelete = orphanedViews.filter(
(view) => view.type === ViewType.KANBAN,
);
const otherViewsToUpdate = orphanedViews.filter(
(view) => view.type !== ViewType.KANBAN,
);
let deletedCount = 0;
let updatedCount = 0;
if (!options.dryRun) {
if (kanbanViewsToDelete.length > 0) {
const kanbanViewIds = kanbanViewsToDelete.map((view) => view.id);
await this.viewRepository.delete({ id: In(kanbanViewIds) });
deletedCount = kanbanViewsToDelete.length;
}
if (otherViewsToUpdate.length > 0) {
const otherViewIds = otherViewsToUpdate.map((view) => view.id);
await this.viewRepository.update(
{ id: In(otherViewIds) },
{ kanbanAggregateOperationFieldMetadataId: null },
);
updatedCount = otherViewsToUpdate.length;
}
} else {
deletedCount = kanbanViewsToDelete.length;
updatedCount = otherViewsToUpdate.length;
}
this.logger.log(
`${options.dryRun ? 'DRY RUN - Would have' : ''}Deleted ${deletedCount} KANBAN views and updated ${updatedCount} other views with orphaned kanbanAggregateOperationFieldMetadataId references`,
);
}
}
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
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 { 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 { RegenerateSearchVectorsCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-regenerate-search-vectors.command';
@@ -9,6 +10,7 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-schema-manager/workspace-schema-manager.module';
@Module({
@@ -18,6 +20,7 @@ import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-sc
ObjectMetadataEntity,
FieldMetadataEntity,
IndexMetadataEntity,
ViewEntity,
]),
WorkspaceSchemaManagerModule,
],
@@ -26,12 +29,14 @@ import { WorkspaceSchemaManagerModule } from 'src/engine/twenty-orm/workspace-sc
MigrateAttachmentTypeToFileCategoryCommand,
RegenerateSearchVectorsCommand,
AddWorkflowRunStopStatusesCommand,
CleanOrphanedKanbanAggregateOperationFieldMetadataIdCommand,
],
exports: [
MigrateAttachmentAuthorToCreatedByCommand,
MigrateAttachmentTypeToFileCategoryCommand,
RegenerateSearchVectorsCommand,
AddWorkflowRunStopStatusesCommand,
CleanOrphanedKanbanAggregateOperationFieldMetadataIdCommand,
],
})
export class V1_10_UpgradeVersionCommandModule {}
@@ -20,6 +20,7 @@ import { FixSchemaArrayTypeCommand } from 'src/database/commands/upgrade-version
import { FixUpdateStandardFieldsIsLabelSyncedWithName } from 'src/database/commands/upgrade-version-command/1-1/1-1-fix-update-standard-field-is-label-synced-with-name.command';
import { MigrateWorkflowRunStatesCommand } from 'src/database/commands/upgrade-version-command/1-1/1-1-migrate-workflow-run-state.command';
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 { 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 { RegenerateSearchVectorsCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-regenerate-search-vectors.command';
@@ -105,6 +106,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
protected readonly migrateAttachmentTypeToFileCategoryCommand: MigrateAttachmentTypeToFileCategoryCommand,
protected readonly regenerateSearchVectorsCommand: RegenerateSearchVectorsCommand,
protected readonly addWorkflowRunStopStatusesCommand: AddWorkflowRunStopStatusesCommand,
protected readonly cleanOrphanedKanbanAggregateOperationFieldMetadataIdCommand: CleanOrphanedKanbanAggregateOperationFieldMetadataIdCommand,
) {
super(
workspaceRepository,
@@ -213,6 +215,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
beforeSyncMetadata: [
this.regenerateSearchVectorsCommand,
this.addWorkflowRunStopStatusesCommand,
this.cleanOrphanedKanbanAggregateOperationFieldMetadataIdCommand,
],
afterSyncMetadata: [
this.migrateAttachmentAuthorToCreatedByCommand,