feat(workflow): backfill workspace workflowVersion into core (phase A) (#22663)
## workflowVersion -> core, Phase A Follows #21674 (Phase 0, merged). Base: `main`. Populates core `workflowVersion` and keeps it in sync with the workspace object, so a later phase can switch reads to core. Reads stay on the workspace object in this PR. ### 1. Backfill (upgrade command) `BackfillWorkflowVersionToCoreCommand`, a `@RegisteredWorkspaceCommand('2.20.0', ...)`. Per workspace, reads all workspace `workflowVersion` records and upserts them into core, preserving ids (idempotent), dry-run aware. ### 2. Dual-write (always on, not flag-gated) `WorkflowVersionCoreDualWriteListener` hooks `@OnDatabaseBatchEvent('workflowVersion', CREATED/UPDATED/DELETED)` (same mechanism as the existing workflow-version status listener) and mirrors every mutation into core. Sync failures are logged, never break the user's write; drift is repaired by re-running the backfill command. Dual-write is deliberately not behind a flag: reading from core (next phase) is only safe if core has been continuously in sync since the backfill. An always-on mirror makes "core is fresh" an invariant, so the read switch becomes a plain flag flip. The cost is one extra upsert on infrequent workflowVersion writes. `IS_WORKFLOW_VERSION_IN_CORE_ENABLED` is reserved for the read switch (Phase B): dispatch from the `workflowAutomatedTriggerMaps` cache, runner and builder reading trigger/steps from core. Until then it gates nothing. Both the backfill and the listener go through a single `WorkflowVersionCoreSyncService` (`upsertToCore`/`deleteFromCore`): the workspace-to-core mapping (`trigger` -> `triggers[]`, plus `steps`, `status`, `workflowId`) and `workflowAutomatedTriggerMaps` invalidation live in one place. ### Rollout plan (following phases) - **B, read switch (flag per workspace):** reads move to core; writes keep flowing workspace -> listener -> core. Rollback = flip the flag back, workspace never stopped being source of truth. - **C, contract (code change):** write paths write trigger/steps to core directly; workspace `workflowVersion` stays as a thin shell (nav/relations/search) but drops the trigger/steps columns; listener and flag removed. ### Not in this PR - Reconciliation tooling beyond re-running the backfill. - The read switch (Phase B).
This commit is contained in:
+70
@@ -0,0 +1,70 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { In } from 'typeorm';
|
||||
|
||||
import {
|
||||
WorkflowVersionEntity,
|
||||
WorkflowVersionStatus,
|
||||
} from 'src/engine/core-modules/workflow/entities/workflow-version.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';
|
||||
import { type WorkflowVersionWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class WorkflowVersionCoreSyncService {
|
||||
constructor(
|
||||
@InjectWorkspaceScopedRepository(WorkflowVersionEntity)
|
||||
private readonly workflowVersionRepository: WorkspaceScopedRepository<WorkflowVersionEntity>,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
) {}
|
||||
|
||||
async upsertToCore(
|
||||
workspaceId: string,
|
||||
workflowVersions: WorkflowVersionWorkspaceEntity[],
|
||||
): Promise<void> {
|
||||
if (workflowVersions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.workflowVersionRepository.upsert(
|
||||
workspaceId,
|
||||
workflowVersions.map((workflowVersion) => ({
|
||||
id: workflowVersion.id,
|
||||
workflowId: workflowVersion.workflowId,
|
||||
triggers: isDefined(workflowVersion.trigger)
|
||||
? [workflowVersion.trigger]
|
||||
: null,
|
||||
steps: workflowVersion.steps ?? null,
|
||||
status: workflowVersion.status as unknown as WorkflowVersionStatus,
|
||||
})),
|
||||
['id'],
|
||||
);
|
||||
|
||||
await this.invalidateAutomatedTriggerMaps(workspaceId);
|
||||
}
|
||||
|
||||
async deleteFromCore(
|
||||
workspaceId: string,
|
||||
workflowVersionIds: string[],
|
||||
): Promise<void> {
|
||||
if (workflowVersionIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.workflowVersionRepository.delete(workspaceId, {
|
||||
id: In(workflowVersionIds),
|
||||
});
|
||||
|
||||
await this.invalidateAutomatedTriggerMaps(workspaceId);
|
||||
}
|
||||
|
||||
private async invalidateAutomatedTriggerMaps(
|
||||
workspaceId: string,
|
||||
): Promise<void> {
|
||||
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
|
||||
'workflowAutomatedTriggerMaps',
|
||||
]);
|
||||
}
|
||||
}
|
||||
+12
-2
@@ -2,15 +2,25 @@ import { Module } from '@nestjs/common';
|
||||
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 { 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])],
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([WorkflowVersionEntity]),
|
||||
WorkspaceCacheModule,
|
||||
],
|
||||
providers: [
|
||||
WorkspaceWorkflowAutomatedTriggerMapCacheService,
|
||||
WorkflowVersionCoreSyncService,
|
||||
provideWorkspaceScopedRepository(WorkflowVersionEntity),
|
||||
],
|
||||
exports: [TypeOrmModule, WorkspaceWorkflowAutomatedTriggerMapCacheService],
|
||||
exports: [
|
||||
TypeOrmModule,
|
||||
WorkspaceWorkflowAutomatedTriggerMapCacheService,
|
||||
WorkflowVersionCoreSyncService,
|
||||
],
|
||||
})
|
||||
export class WorkflowVersionCoreModule {}
|
||||
|
||||
Reference in New Issue
Block a user