[Fix] Command to migrate operand values for workflows (#14849)

In [this PR](https://github.com/twentyhq/twenty/pull/14785) we got rid
of what we now call ViewFilterOperandDeprecated, a camelCase version of
ViewFilterOperand, which we thought we only used in the FE. We did not
notice that this enum was used to persist filters used in workflows,
reflected in workflowVersion and workflowRun.
As a result workflow runs were broken. [In this mitigation
PR](https://github.com/twentyhq/twenty/pull/14837) (and [this
one](https://github.com/twentyhq/twenty/pull/14841)) we updated the code
handle both enum values from ViewFilterOperandDeprecated and
ViewFilterOperand, but we still want to get rid of
ViewFilterOperandDeprecated.

the command in this PR replaces the occurences of enum values of
ViewFilterOperandDeprecated.
When this has been merged, deployed and run on the workspaces, we will
be able to remove ViewFilterOperandDeprecated altogether; that will have
to be done in 1.10 though not before.
This commit is contained in:
Marie
2025-10-09 11:45:44 +02:00
committed by GitHub
parent 920ad4c3f2
commit ebc6bbabaf
5 changed files with 236 additions and 1 deletions
@@ -0,0 +1,210 @@
import { InjectRepository } from '@nestjs/typeorm';
import { isString } from 'class-validator';
import { Command } from 'nest-commander';
import {
convertViewFilterOperandToCoreOperand,
isDefined,
} from 'twenty-shared/utils';
import { In, Raw, Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import {
WorkflowRunState,
WorkflowRunWorkspaceEntity,
} from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
import { WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
import { isWorkflowFilterAction } from 'src/modules/workflow/workflow-executor/workflow-actions/filter/guards/is-workflow-filter-action.guard';
import { isWorkflowFindRecordsAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/guards/is-workflow-find-records-action.guard';
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
@Command({
name: 'upgrade:1-10:migrate-workflow-step-filter-operand-value',
description:
'Migrate workflowVersion.steps[].settings.input.stepFilters[].operand to use new operand enum',
})
export class MigrateWorkflowStepFilterOperandValueCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(Workspace)
protected readonly workspaceRepository: Repository<Workspace>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {
super(workspaceRepository, twentyORMGlobalManager);
}
override async runOnWorkspace({
workspaceId,
index,
total,
options,
}: RunOnWorkspaceArgs): Promise<void> {
this.logger.log(
`[${index + 1}/${total}] Migrating workflow step filter operand values for workspace ${workspaceId}`,
);
// workflowVersions
const workflowVersionRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
const workflowVersionsToMigrate = await workflowVersionRepository.find({
where: [
{
steps: Raw(
(_alias) => `"workflowVersion"."steps"::text LIKE :search`,
{
search: '%operand%',
},
),
},
],
});
this.logger.log(
`Found ${workflowVersionsToMigrate.length} workflowVersions to migrate`,
);
for (const workflowVersion of workflowVersionsToMigrate) {
let steps: WorkflowAction[] | null | undefined;
steps = workflowVersion.steps;
let hasChanged = false;
for (const step of steps ?? []) {
if (isWorkflowFilterAction(step)) {
for (const filter of step.settings.input.stepFilters ?? []) {
if (isDefined(filter.operand) && isString(filter.operand)) {
const newOperand = convertViewFilterOperandToCoreOperand(
filter.operand,
);
if (newOperand && newOperand !== filter.operand) {
filter.operand = newOperand;
hasChanged = true;
}
}
}
}
if (isWorkflowFindRecordsAction(step)) {
for (const filter of step.settings.input.filter?.recordFilters ??
[]) {
if (isString(filter.operand)) {
const newOperand = convertViewFilterOperandToCoreOperand(
filter.operand,
);
if (newOperand && newOperand !== filter.operand) {
filter.operand = newOperand;
hasChanged = true;
}
}
}
}
}
if (hasChanged) {
this.logger.log(
`${options.dryRun ? 'DRY RUN - Would be' : ''}Updating workflowVersion ${workflowVersion.id} in workspace ${workspaceId}`,
);
if (!options.dryRun) {
await workflowVersionRepository.update(
{ id: workflowVersion.id },
{ steps },
);
}
this.logger.log(
`${options.dryRun ? 'DRY RUN - Would have' : ''} Updated workflowVersion ${workflowVersion.id} in workspace ${workspaceId}`,
);
}
}
//workflowRuns
const workflowRunRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowRunWorkspaceEntity>(
workspaceId,
'workflowRun',
{ shouldBypassPermissionChecks: true },
);
const workflowRunsToMigrate = await workflowRunRepository.find({
where: {
workflowVersionId: In(
workflowVersionsToMigrate.map(
(workflowVersion) => workflowVersion.id,
),
),
},
});
this.logger.log(
`Found ${workflowRunsToMigrate.length} workflowRuns to migrate`,
);
for (const workflowRun of workflowRunsToMigrate) {
let state: WorkflowRunState | null | undefined = workflowRun.state;
if (!isDefined(state)) {
continue;
}
let hasChanged = false;
for (const step of state.flow.steps) {
if (isWorkflowFindRecordsAction(step)) {
const filter = step.settings.input.filter;
for (const recordFilter of filter?.recordFilters ?? []) {
if (isString(recordFilter.operand)) {
const newOperand = convertViewFilterOperandToCoreOperand(
recordFilter.operand,
);
if (newOperand && newOperand !== recordFilter.operand) {
recordFilter.operand = newOperand;
hasChanged = true;
}
}
}
}
if (isWorkflowFilterAction(step)) {
for (const filter of step.settings.input.stepFilters ?? []) {
if (isString(filter.operand)) {
const newOperand = convertViewFilterOperandToCoreOperand(
filter.operand,
);
if (newOperand && newOperand !== filter.operand) {
filter.operand = newOperand;
hasChanged = true;
}
}
}
}
}
if (hasChanged) {
this.logger.log(
`${options.dryRun ? 'DRY RUN - Would be' : ''}Updating workflowRun ${workflowRun.id} in workspace ${workspaceId}`,
);
if (!options.dryRun) {
await workflowRunRepository.update({ id: workflowRun.id }, { state });
}
this.logger.log(
`${options.dryRun ? 'DRY RUN - Would have' : ''}Updated workflowRun ${workflowRun.id} in workspace ${workspaceId}`,
);
}
}
}
}
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { MigrateWorkflowStepFilterOperandValueCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-workflow-step-filter-operand-value';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
@Module({
imports: [TypeOrmModule.forFeature([Workspace]), WorkspaceDataSourceModule],
providers: [MigrateWorkflowStepFilterOperandValueCommand],
exports: [MigrateWorkflowStepFilterOperandValueCommand],
})
export class V1_10_UpgradeVersionCommandModule {}
@@ -4,6 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { V0_54_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/0-54/0-54-upgrade-version-command.module';
import { V0_55_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/0-55/0-55-upgrade-version-command.module';
import { V1_1_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-1/1-1-upgrade-version-command.module';
import { V1_10_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-10/1-10-upgrade-version-command.module';
import { V1_2_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-2/1-2-upgrade-version-command.module';
import { V1_3_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-3/1-3-upgrade-version-command.module';
import { V1_5_UpgradeVersionCommandModule } from 'src/database/commands/upgrade-version-command/1-5/1-5-upgrade-version-command.module';
@@ -24,6 +25,7 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp
V1_5_UpgradeVersionCommandModule,
V1_6_UpgradeVersionCommandModule,
V1_7_UpgradeVersionCommandModule,
V1_10_UpgradeVersionCommandModule,
WorkspaceSyncMetadataModule,
],
providers: [UpgradeCommand],
@@ -19,6 +19,7 @@ import { AddEnqueuedStatusToWorkflowRunCommand } from 'src/database/commands/upg
import { FixSchemaArrayTypeCommand } from 'src/database/commands/upgrade-version-command/1-1/1-1-fix-schema-array-type.command';
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 { MigrateWorkflowStepFilterOperandValueCommand } from 'src/database/commands/upgrade-version-command/1-10/1-10-migrate-workflow-step-filter-operand-value';
import { AddEnqueuedStatusToWorkflowRunV2Command } from 'src/database/commands/upgrade-version-command/1-2/1-2-add-enqueued-status-to-workflow-run-v2.command';
import { AddNextStepIdsToWorkflowVersionTriggers } from 'src/database/commands/upgrade-version-command/1-2/1-2-add-next-step-ids-to-workflow-version-triggers.command';
import { RemoveWorkflowRunsWithoutState } from 'src/database/commands/upgrade-version-command/1-2/1-2-remove-workflow-runs-without-state.command';
@@ -83,6 +84,9 @@ export class UpgradeCommand extends UpgradeCommandRunner {
// 1.7 Commands
protected readonly backfillWorkflowManualTriggerAvailabilityCommand: BackfillWorkflowManualTriggerAvailabilityCommand,
// 1.10 Commands
protected readonly migrateWorkflowStepFilterOperandValueCommand: MigrateWorkflowStepFilterOperandValueCommand,
) {
super(
workspaceRepository,
@@ -176,6 +180,11 @@ export class UpgradeCommand extends UpgradeCommandRunner {
afterSyncMetadata: [],
};
const commands_1100: VersionCommands = {
beforeSyncMetadata: [this.migrateWorkflowStepFilterOperandValueCommand],
afterSyncMetadata: [],
};
this.allCommands = {
'0.53.0': commands_053,
'0.54.0': commands_054,
@@ -189,6 +198,7 @@ export class UpgradeCommand extends UpgradeCommandRunner {
'1.5.0': commands_150,
'1.6.0': commands_160,
'1.7.0': commands_170,
'1.10.0': commands_1100,
};
}
@@ -14,5 +14,5 @@ export enum ViewFilterOperandDeprecated {
IsInPast = 'isInPast',
IsInFuture = 'isInFuture',
IsToday = 'isToday',
VectorSearch = 'vectorSearch',
VectorSearch = 'search',
}