feat(workflow): soft-ref core workflow/version (backfill + dual-write) (#22821)
Replaces the shared-UUID model (core row reuses the workspace record id) with a **soft-ref**: the workspace `workflow`/`workflowVersion` records carry a nullable `coreWorkflowId`/`coreWorkflowVersionId` pointing to their **own-id** core rows. This removes the assumption that workspace record ids are globally unique - which is false, since prefilled/seeded workflows share ids across workspaces. Supersedes #22776. ## In this PR **Soft-ref columns (foundation):** - **twenty-shared** `STANDARD_OBJECTS`: `workflowVersion.coreWorkflowVersionId` + `workflow.coreWorkflowId` (+ snapshot test). - **compute utils**: both as system, nullable UUID fields. - **entity classes**: the bare fields. **Version soft-ref sync:** - Core `workflowVersion` rows get their own id, derived deterministically from `workspaceId + record id` (uuidv5). Deterministic so the upsert is idempotent: a failed write-back re-derives the same id and self-heals instead of orphaning rows or colliding on the one-active-per-workflow index. - Sync = find-or-create keyed on the workspace record's `coreWorkflowVersionId`, then write the core id back onto the workspace record. - Migrating over pre-soft-ref data: purges any core row whose id equals the workspace record id before recreating, so old shared-UUID rows aren't orphaned. - Version dual-write listener reworked: delete is keyed by the core id read off `before.coreWorkflowVersionId`. Verified on a fresh `database:reset` (columns materialize, backfill produces deterministic own-id rows linked back, idempotent re-run), a simulated old shared-UUID state (stale rows purged, records re-linked), and a simulated write-back failure (retry re-links to the same id, no orphan, active-version index intact). ## Next steps (follow-up work, not in this PR) 1. Workflow-side soft-ref sync mirroring the version side (service, module, dual-write listener, backfill command). 2. Workspace command to add the two columns to existing workspaces. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22821?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:
+1
@@ -17,6 +17,7 @@ export class WorkflowVersionWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
name: string | null;
|
||||
trigger: WorkflowTrigger | null;
|
||||
steps: WorkflowAction[] | null;
|
||||
coreWorkflowVersionId: string | null;
|
||||
status: WorkflowVersionStatus;
|
||||
position: number;
|
||||
searchVector: string;
|
||||
|
||||
+1
@@ -17,6 +17,7 @@ export enum WorkflowStatus {
|
||||
export class WorkflowWorkspaceEntity extends BaseWorkspaceEntity {
|
||||
name: string | null;
|
||||
lastPublishedVersionId: string | null;
|
||||
coreWorkflowId: string | null;
|
||||
statuses: WorkflowStatus[] | null;
|
||||
position: number;
|
||||
searchVector: string;
|
||||
|
||||
+8
-4
@@ -67,7 +67,9 @@ export class WorkflowVersionCoreDualWriteListener {
|
||||
): Promise<void> {
|
||||
await this.deleteFromCore(
|
||||
batchEvent.workspaceId,
|
||||
batchEvent.events.map((event) => event.properties.before.id),
|
||||
batchEvent.events
|
||||
.map((event) => event.properties.before.coreWorkflowVersionId)
|
||||
.filter(isDefined),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -79,7 +81,9 @@ export class WorkflowVersionCoreDualWriteListener {
|
||||
): Promise<void> {
|
||||
await this.deleteFromCore(
|
||||
batchEvent.workspaceId,
|
||||
batchEvent.events.map((event) => event.properties.before.id),
|
||||
batchEvent.events
|
||||
.map((event) => event.properties.before.coreWorkflowVersionId)
|
||||
.filter(isDefined),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -105,7 +109,7 @@ export class WorkflowVersionCoreDualWriteListener {
|
||||
|
||||
private async deleteFromCore(
|
||||
workspaceId: string | undefined,
|
||||
workflowVersionIds: string[],
|
||||
coreWorkflowVersionIds: string[],
|
||||
): Promise<void> {
|
||||
if (!isDefined(workspaceId)) {
|
||||
return;
|
||||
@@ -114,7 +118,7 @@ export class WorkflowVersionCoreDualWriteListener {
|
||||
try {
|
||||
await this.workflowVersionCoreSyncService.deleteFromCore(
|
||||
workspaceId,
|
||||
workflowVersionIds,
|
||||
coreWorkflowVersionIds,
|
||||
);
|
||||
} catch (error) {
|
||||
this.exceptionHandlerService.captureExceptions([error], {
|
||||
|
||||
Reference in New Issue
Block a user