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
@@ -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: [