feat(workflow): add universalIdentifier + applicationId to core workflowVersion (#22747)

## 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.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22747?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Thomas Trompette
2026-07-10 10:26:52 +02:00
committed by GitHub
parent 0786f9e793
commit b327ab09a7
6 changed files with 85 additions and 4 deletions
@@ -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<void> {
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<void> {
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"`,
);
}
}
@@ -0,0 +1,2 @@
export const ADD_WORKFLOW_VERSION_SYNCABLE_COLUMNS_UPGRADE_COMMAND_NAME =
'2.20.0_AddWorkflowVersionSyncableColumnsFastInstanceCommand_1783603454480';
@@ -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,
];
@@ -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;
@@ -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<WorkflowVersionEntity>,
@InjectRepository(WorkspaceEntity)
private readonly workspaceRepository: Repository<WorkspaceEntity>,
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<string> {
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[],
@@ -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: [