Cascade delete Task targets when tasks deleted - logic + migration command (#17019)
Co-authored-by: prastoin <paul@twenty.com>
This commit is contained in:
+201
@@ -0,0 +1,201 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Command } from 'nest-commander';
|
||||
import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata';
|
||||
import {
|
||||
FieldMetadataRelationSettings,
|
||||
FieldMetadataType,
|
||||
RelationOnDeleteAction,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata.service';
|
||||
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { TASK_TARGET_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-migration/constant/standard-field-ids';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade:1-16:update-task-on-delete-action',
|
||||
description:
|
||||
'Update task relation onDelete action to CASCADE and delete orphaned taskTarget records',
|
||||
})
|
||||
export class UpdateTaskOnDeleteActionCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
protected readonly logger = new Logger(UpdateTaskOnDeleteActionCommand.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
protected readonly dataSourceService: DataSourceService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly fieldMetadataService: FieldMetadataService,
|
||||
) {
|
||||
super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService);
|
||||
}
|
||||
|
||||
override async runOnWorkspace({
|
||||
workspaceId,
|
||||
options,
|
||||
}: RunOnWorkspaceArgs): Promise<void> {
|
||||
const isDryRun = options.dryRun ?? false;
|
||||
|
||||
this.logger.log(
|
||||
`Running UpdateTaskOnDeleteActionCommand for workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
await this.updateTaskRelationOnDeleteAction(workspaceId, isDryRun);
|
||||
|
||||
await this.deleteOrphanedTaskTargets(workspaceId, isDryRun);
|
||||
}
|
||||
|
||||
private async updateTaskRelationOnDeleteAction(
|
||||
workspaceId: string,
|
||||
isDryRun: boolean,
|
||||
): Promise<void> {
|
||||
const { flatFieldMetadataMaps, flatObjectMetadataMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatFieldMetadataMaps',
|
||||
'flatObjectMetadataMaps',
|
||||
]);
|
||||
|
||||
const taskTargetObjectMetadata = Object.values(
|
||||
flatObjectMetadataMaps.byId,
|
||||
).find(
|
||||
(objectMetadata) =>
|
||||
objectMetadata?.standardId === STANDARD_OBJECT_IDS.taskTarget,
|
||||
);
|
||||
|
||||
if (!isDefined(taskTargetObjectMetadata)) {
|
||||
this.logger.warn(
|
||||
`TaskTarget object metadata not found in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const taskTargetFields = findManyFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityIds: taskTargetObjectMetadata.fieldMetadataIds,
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
const taskField = taskTargetFields.find(
|
||||
(field) => field.standardId === TASK_TARGET_STANDARD_FIELD_IDS.task,
|
||||
);
|
||||
|
||||
if (!isDefined(taskField)) {
|
||||
this.logger.warn(
|
||||
`Task field not found on taskTarget object in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (taskField.type !== FieldMetadataType.RELATION) {
|
||||
this.logger.warn(
|
||||
`Task field is not a relation field in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const taskFieldSettings =
|
||||
taskField.settings as FieldMetadataRelationSettings;
|
||||
|
||||
if (taskFieldSettings?.onDelete === RelationOnDeleteAction.CASCADE) {
|
||||
this.logger.log(
|
||||
`Task relation already has CASCADE onDelete in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Updating task relation onDelete from ${taskFieldSettings?.onDelete} to CASCADE in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
if (!isDryRun) {
|
||||
const updatedSettings: FieldMetadataRelationSettings = {
|
||||
...taskFieldSettings,
|
||||
onDelete: RelationOnDeleteAction.CASCADE,
|
||||
};
|
||||
|
||||
await this.fieldMetadataService.updateOneField({
|
||||
updateFieldInput: {
|
||||
id: taskField.id,
|
||||
settings: updatedSettings,
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: true,
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Successfully updated task relation onDelete to CASCADE in workspace ${workspaceId}`,
|
||||
);
|
||||
} else {
|
||||
this.logger.log(
|
||||
`DRY RUN: Would update task relation onDelete to CASCADE in workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteOrphanedTaskTargets(
|
||||
workspaceId: string,
|
||||
isDryRun: boolean,
|
||||
): Promise<void> {
|
||||
const taskTargetRepository =
|
||||
await this.globalWorkspaceOrmManager.getRepository(
|
||||
workspaceId,
|
||||
'taskTarget',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const orphanedTaskTargets = await taskTargetRepository.find({
|
||||
withDeleted: true,
|
||||
select: ['id'],
|
||||
where: {
|
||||
taskId: IsNull(),
|
||||
},
|
||||
});
|
||||
|
||||
const orphanedCount = orphanedTaskTargets.length;
|
||||
|
||||
this.logger.log(
|
||||
`Found ${orphanedCount} orphaned taskTarget record(s) in workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
if (orphanedCount === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDryRun) {
|
||||
const orphanedIds = orphanedTaskTargets.map((record) => record.id);
|
||||
|
||||
const batchSize = 100;
|
||||
|
||||
for (let i = 0; i < orphanedIds.length; i += batchSize) {
|
||||
const batch = orphanedIds.slice(i, i + batchSize);
|
||||
|
||||
await taskTargetRepository
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.whereInIds(batch)
|
||||
.execute();
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Deleted ${orphanedCount} orphaned taskTarget record(s) in workspace ${workspaceId}`,
|
||||
);
|
||||
} else {
|
||||
this.logger.log(
|
||||
`DRY RUN: Would delete ${orphanedCount} orphaned taskTarget record(s) in workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { UpdateTaskOnDeleteActionCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-update-task-on-delete-action.command';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.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 { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([WorkspaceEntity]),
|
||||
DataSourceModule,
|
||||
WorkspaceCacheModule,
|
||||
FieldMetadataModule,
|
||||
],
|
||||
providers: [UpdateTaskOnDeleteActionCommand],
|
||||
exports: [UpdateTaskOnDeleteActionCommand],
|
||||
})
|
||||
export class V1_16_UpgradeVersionCommandModule {}
|
||||
+2
@@ -4,6 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { V1_13_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-13/1-13-upgrade-version-command.module';
|
||||
import { V1_14_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-14/1-14-upgrade-version-command.module';
|
||||
import { V1_15_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-15/1-15-upgrade-version-command.module';
|
||||
import { V1_16_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-16/1-16-upgrade-version-command.module';
|
||||
import { UpgradeCommand } from 'src/database/commands/upgrade-version-command/upgrade.command';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
@@ -14,6 +15,7 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s
|
||||
V1_13_UpgradeVersionCommandModule,
|
||||
V1_14_UpgradeVersionCommandModule,
|
||||
V1_15_UpgradeVersionCommandModule,
|
||||
V1_16_UpgradeVersionCommandModule,
|
||||
DataSourceModule,
|
||||
],
|
||||
providers: [UpgradeCommand],
|
||||
|
||||
+9
@@ -22,6 +22,7 @@ import { AddWorkspaceForeignKeysMigrationCommand } from 'src/database/commands/u
|
||||
import { BackfillUpdatedByFieldCommand } from 'src/database/commands/upgrade-version-command/1-15/1-15-backfill-updated-by-field.command';
|
||||
import { FixNanPositionValuesInNotesCommand } from 'src/database/commands/upgrade-version-command/1-15/1-15-fix-nan-position-values-in-notes.command';
|
||||
import { MigratePageLayoutWidgetConfigurationCommand } from 'src/database/commands/upgrade-version-command/1-15/1-15-migrate-page-layout-widget-configuration.command';
|
||||
import { UpdateTaskOnDeleteActionCommand } from 'src/database/commands/upgrade-version-command/1-16/1-16-update-task-on-delete-action.command';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
@@ -59,6 +60,9 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
protected readonly fixNanPositionValuesInNotesCommand: FixNanPositionValuesInNotesCommand,
|
||||
protected readonly backfillUpdatedByFieldCommand: BackfillUpdatedByFieldCommand,
|
||||
protected readonly addWorkspaceForeignKeysMigrationCommand: AddWorkspaceForeignKeysMigrationCommand,
|
||||
|
||||
// 1.16 Commands
|
||||
protected readonly updateTaskOnDeleteActionCommand: UpdateTaskOnDeleteActionCommand,
|
||||
) {
|
||||
super(
|
||||
workspaceRepository,
|
||||
@@ -92,11 +96,16 @@ export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
this.addWorkspaceForeignKeysMigrationCommand,
|
||||
];
|
||||
|
||||
const commands_1160: VersionCommands = [
|
||||
this.updateTaskOnDeleteActionCommand,
|
||||
];
|
||||
|
||||
this.allCommands = {
|
||||
'1.12.0': commands_1120,
|
||||
'1.13.0': commands_1130,
|
||||
'1.14.0': commands_1140,
|
||||
'1.15.0': commands_1150,
|
||||
'1.16.0': commands_1160,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user