From b327ab09a7af8a77ffcb4cb89a57d49bb503606c Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Fri, 10 Jul 2026 10:26:52 +0200 Subject: [PATCH] feat(workflow): add universalIdentifier + applicationId to core workflowVersion (#22747) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## workflowVersion core: syncable columns Adds `universalIdentifier` + `applicationId` (nullable) to `core.workflowVersion`, plus the FK to `core.application` and the `(workspaceId, universalIdentifier)` unique index, via a 2.20 add-columns fast command gated with `@WasIntroducedInUpgrade`. Nullable for now: the already-merged Phase A backfill (#22663) inserts version rows without these columns, so `applicationId` can't be NOT NULL yet. Flipping to NOT NULL + `extends SyncableEntity` comes once they're populated (backfill + dual-write follow-ups). Schema captured and verified via `migrate:generate` (zero drift). Independent of the core-workflow PR, but both add 2.20 upgrade commands, so this one (ts `…480`) must merge **after** the core-workflow PR (ts `…479`), or it gets re-timestamped on rebase. Review in cubic --- ...0-add-workflow-version-syncable-columns.ts | 49 +++++++++++++++++++ ...e-columns-upgrade-command-name.constant.ts | 2 + .../instance-commands.constant.ts | 2 + .../entities/workflow-version.entity.ts | 5 +- .../workflow-version-core-sync.service.ts | 28 ++++++++++- .../workflow/workflow-version-core.module.ts | 3 +- 6 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/2-20/2-20-instance-command-fast-1783603454480-add-workflow-version-syncable-columns.ts create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/2-20/add-workflow-version-syncable-columns-upgrade-command-name.constant.ts diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-20/2-20-instance-command-fast-1783603454480-add-workflow-version-syncable-columns.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-20/2-20-instance-command-fast-1783603454480-add-workflow-version-syncable-columns.ts new file mode 100644 index 0000000000..51404e7b43 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-20/2-20-instance-command-fast-1783603454480-add-workflow-version-syncable-columns.ts @@ -0,0 +1,49 @@ +import { QueryRunner } from 'typeorm'; + +import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator'; +import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface'; + +@RegisteredInstanceCommand('2.20.0', 1783603454480) +export class AddWorkflowVersionSyncableColumnsFastInstanceCommand + implements FastInstanceCommand +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."workflowVersion" ADD COLUMN IF NOT EXISTS "universalIdentifier" uuid NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workflowVersion" ADD COLUMN IF NOT EXISTS "applicationId" uuid NOT NULL`, + ); + await queryRunner.query( + `CREATE UNIQUE INDEX IF NOT EXISTS "IDX_c67a90cc29887078286205457e" + ON "core"."workflowVersion" ("workspaceId", "universalIdentifier")`, + ); + await queryRunner.query( + `DO $$ BEGIN + ALTER TABLE "core"."workflowVersion" ADD CONSTRAINT "FK_29f62766c5b109981244b97060d" FOREIGN KEY ("applicationId") REFERENCES "core"."application"("id") ON DELETE CASCADE; + EXCEPTION WHEN duplicate_object THEN null; END $$`, + ); + await queryRunner.query( + `CREATE INDEX IF NOT EXISTS "IDX_WORKFLOW_VERSION_APPLICATION_ID" + ON "core"."workflowVersion" ("applicationId")`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."workflowVersion" DROP CONSTRAINT IF EXISTS "FK_29f62766c5b109981244b97060d"`, + ); + await queryRunner.query( + `DROP INDEX IF EXISTS "core"."IDX_c67a90cc29887078286205457e"`, + ); + await queryRunner.query( + `DROP INDEX IF EXISTS "core"."IDX_WORKFLOW_VERSION_APPLICATION_ID"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workflowVersion" DROP COLUMN IF EXISTS "applicationId"`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workflowVersion" DROP COLUMN IF EXISTS "universalIdentifier"`, + ); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-20/add-workflow-version-syncable-columns-upgrade-command-name.constant.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-20/add-workflow-version-syncable-columns-upgrade-command-name.constant.ts new file mode 100644 index 0000000000..6eda2a681d --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-20/add-workflow-version-syncable-columns-upgrade-command-name.constant.ts @@ -0,0 +1,2 @@ +export const ADD_WORKFLOW_VERSION_SYNCABLE_COLUMNS_UPGRADE_COMMAND_NAME = + '2.20.0_AddWorkflowVersionSyncableColumnsFastInstanceCommand_1783603454480'; diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts index b07bbb81ac..d4ee164e97 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts @@ -105,6 +105,7 @@ import { BackfillNameFieldIsSystemSideEffectSlowInstanceCommand } from './2-20/2 import { RenameIsFeaturedToIsVettedOnApplicationRegistrationFastInstanceCommand } from './2-20/2-20-instance-command-fast-1783527064000-rename-is-featured-to-is-vetted-on-application-registration'; import { AddIsSystemSideEffectToSearchFieldMetadataFastInstanceCommand } from './2-20/2-20-instance-command-fast-1783580127637-add-is-system-side-effect-to-search-field-metadata'; import { CreateWorkflowCoreTableFastInstanceCommand } from './2-20/2-20-instance-command-fast-1783603454479-create-workflow-core-table'; +import { AddWorkflowVersionSyncableColumnsFastInstanceCommand } from './2-20/2-20-instance-command-fast-1783603454480-add-workflow-version-syncable-columns'; export const INSTANCE_COMMANDS = [ AddViewFieldGroupIdIndexOnViewFieldFastInstanceCommand, @@ -212,4 +213,5 @@ export const INSTANCE_COMMANDS = [ RenameIsFeaturedToIsVettedOnApplicationRegistrationFastInstanceCommand, AddIsSystemSideEffectToSearchFieldMetadataFastInstanceCommand, CreateWorkflowCoreTableFastInstanceCommand, + AddWorkflowVersionSyncableColumnsFastInstanceCommand, ]; diff --git a/packages/twenty-server/src/engine/core-modules/workflow/entities/workflow-version.entity.ts b/packages/twenty-server/src/engine/core-modules/workflow/entities/workflow-version.entity.ts index 6a856dbcdf..00265e725e 100644 --- a/packages/twenty-server/src/engine/core-modules/workflow/entities/workflow-version.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/workflow/entities/workflow-version.entity.ts @@ -9,7 +9,7 @@ import { import { CREATE_WORKFLOW_VERSION_CORE_TABLE_UPGRADE_COMMAND_NAME } from 'src/database/commands/upgrade-version-command/2-20/create-workflow-version-core-table-upgrade-command-name.constant'; import { WasIntroducedInUpgrade } from 'src/engine/core-modules/upgrade/decorators/was-introduced-in-upgrade.decorator'; -import { WorkspaceRelatedEntity } from 'src/engine/workspace-manager/types/workspace-related-entity'; +import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface'; import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type'; import { type WorkflowTrigger } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type'; @@ -29,7 +29,8 @@ export enum WorkflowVersionStatus { unique: true, where: `"status" = 'ACTIVE'`, }) -export class WorkflowVersionEntity extends WorkspaceRelatedEntity { +@Index('IDX_WORKFLOW_VERSION_APPLICATION_ID', ['applicationId']) +export class WorkflowVersionEntity extends SyncableEntity { @PrimaryGeneratedColumn('uuid') id: string; diff --git a/packages/twenty-server/src/engine/core-modules/workflow/services/workflow-version-core-sync.service.ts b/packages/twenty-server/src/engine/core-modules/workflow/services/workflow-version-core-sync.service.ts index 04a91e2a84..0a98df8a64 100644 --- a/packages/twenty-server/src/engine/core-modules/workflow/services/workflow-version-core-sync.service.ts +++ b/packages/twenty-server/src/engine/core-modules/workflow/services/workflow-version-core-sync.service.ts @@ -1,12 +1,15 @@ import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; import { isDefined } from 'twenty-shared/utils'; -import { In } from 'typeorm'; +import { In, Repository } from 'typeorm'; +import { v4 as uuidv4 } from 'uuid'; import { WorkflowVersionEntity, WorkflowVersionStatus, } from 'src/engine/core-modules/workflow/entities/workflow-version.entity'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { InjectWorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/inject-workspace-scoped-repository.decorator'; import { WorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/workspace-scoped-repository'; import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; @@ -17,6 +20,8 @@ export class WorkflowVersionCoreSyncService { constructor( @InjectWorkspaceScopedRepository(WorkflowVersionEntity) private readonly workflowVersionRepository: WorkspaceScopedRepository, + @InjectRepository(WorkspaceEntity) + private readonly workspaceRepository: Repository, private readonly workspaceCacheService: WorkspaceCacheService, ) {} @@ -28,6 +33,8 @@ export class WorkflowVersionCoreSyncService { return; } + const applicationId = await this.getCustomApplicationIdOrThrow(workspaceId); + await this.workflowVersionRepository.upsert( workspaceId, workflowVersions.map((workflowVersion) => ({ @@ -38,6 +45,8 @@ export class WorkflowVersionCoreSyncService { : null, steps: workflowVersion.steps ?? null, status: workflowVersion.status as unknown as WorkflowVersionStatus, + universalIdentifier: uuidv4(), + applicationId, })), ['id'], ); @@ -45,6 +54,23 @@ export class WorkflowVersionCoreSyncService { await this.invalidateAutomatedTriggerMaps(workspaceId); } + private async getCustomApplicationIdOrThrow( + workspaceId: string, + ): Promise { + const workspace = await this.workspaceRepository.findOne({ + where: { id: workspaceId }, + select: ['id', 'workspaceCustomApplicationId'], + }); + + if (!isDefined(workspace?.workspaceCustomApplicationId)) { + throw new Error( + `Workspace custom application not found for workspace ${workspaceId}`, + ); + } + + return workspace.workspaceCustomApplicationId; + } + async deleteFromCore( workspaceId: string, workflowVersionIds: string[], diff --git a/packages/twenty-server/src/engine/core-modules/workflow/workflow-version-core.module.ts b/packages/twenty-server/src/engine/core-modules/workflow/workflow-version-core.module.ts index 9849724cad..4830d33771 100644 --- a/packages/twenty-server/src/engine/core-modules/workflow/workflow-version-core.module.ts +++ b/packages/twenty-server/src/engine/core-modules/workflow/workflow-version-core.module.ts @@ -4,12 +4,13 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { WorkflowVersionEntity } from 'src/engine/core-modules/workflow/entities/workflow-version.entity'; import { WorkflowVersionCoreSyncService } from 'src/engine/core-modules/workflow/services/workflow-version-core-sync.service'; import { WorkspaceWorkflowAutomatedTriggerMapCacheService } from 'src/engine/core-modules/workflow/services/workspace-workflow-automated-trigger-map-cache.service'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { provideWorkspaceScopedRepository } from 'src/engine/twenty-orm/workspace-scoped-repository/provide-workspace-scoped-repository'; import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; @Module({ imports: [ - TypeOrmModule.forFeature([WorkflowVersionEntity]), + TypeOrmModule.forFeature([WorkflowVersionEntity, WorkspaceEntity]), WorkspaceCacheModule, ], providers: [