Skip to content

Commit f75cd5e

Browse files
committed
fix(workflow-edges): validate edges in BATCH_ADD_BLOCKS before persisting
Real gap Cursor's PR summary flagged ('BATCH_ADD_BLOCKS edge inserts... not fully covered by the new server pipeline'), confirmed by reading the code: this handler bulk-inserted the edges from a block-paste/duplicate/import payload directly into workflowEdges with zero validation — no missing-block, protected-target, annotation-only, trigger-target, scope-boundary, duplicate, or cycle check. A client sending edges through this operation instead of BATCH_ADD_EDGES could bypass every rule this PR otherwise enforces server-side, exactly the class of gap the PR exists to close. Routes it through the same filterEdgesForPersist used by the other two edge-add handlers. Runs after the block insert in this same handler, so the shared helper's blocksById lookup also sees the blocks this same batch just inserted (a transaction observes its own prior writes).
1 parent a7fdb6a commit f75cd5e

1 file changed

Lines changed: 46 additions & 19 deletions

File tree

apps/realtime/src/database/operations.ts

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1018,27 +1018,54 @@ async function handleBlocksOperationTx(
10181018
}
10191019

10201020
if (edges && edges.length > 0) {
1021-
const edgeValues = edges.map((edge: Record<string, unknown>) => ({
1022-
id: edge.id as string,
1023-
workflowId,
1024-
sourceBlockId: edge.source as string,
1025-
targetBlockId: edge.target as string,
1026-
sourceHandle: (edge.sourceHandle as string | null) || null,
1027-
targetHandle: (edge.targetHandle as string | null) || null,
1028-
}))
1021+
// Runs after the block insert above, so filterEdgesForPersist's
1022+
// blocksById lookup (a plain `tx.select` from `workflowBlocks`) also
1023+
// sees the blocks this same batch just inserted — reads observe a
1024+
// transaction's own prior writes.
1025+
const candidates: EdgeAddCandidate[] = (edges as Array<Record<string, unknown>>).map(
1026+
(e) => ({
1027+
id: e.id as string,
1028+
source: e.source as string,
1029+
target: e.target as string,
1030+
sourceHandle: (e.sourceHandle as string | null) ?? null,
1031+
targetHandle: (e.targetHandle as string | null) ?? null,
1032+
})
1033+
)
10291034

1030-
await tx
1031-
.insert(workflowEdges)
1032-
.values(edgeValues)
1033-
.onConflictDoUpdate({
1034-
target: workflowEdges.id,
1035-
set: {
1036-
sourceBlockId: sql`excluded.source_block_id`,
1037-
targetBlockId: sql`excluded.target_block_id`,
1038-
sourceHandle: sql`excluded.source_handle`,
1039-
targetHandle: sql`excluded.target_handle`,
1040-
},
1035+
const { safeEdges, droppedCounts, droppedDuplicates, droppedCyclic } =
1036+
await filterEdgesForPersist(tx, workflowId, candidates)
1037+
1038+
if (safeEdges.length < edges.length) {
1039+
logger.info(`Dropped ${edges.length - safeEdges.length} invalid edge(s)`, {
1040+
droppedCounts,
1041+
droppedDuplicates,
1042+
droppedCyclic,
10411043
})
1044+
}
1045+
1046+
if (safeEdges.length > 0) {
1047+
const edgeValues = safeEdges.map((edge) => ({
1048+
id: edge.id as string,
1049+
workflowId,
1050+
sourceBlockId: edge.source,
1051+
targetBlockId: edge.target,
1052+
sourceHandle: edge.sourceHandle || null,
1053+
targetHandle: edge.targetHandle || null,
1054+
}))
1055+
1056+
await tx
1057+
.insert(workflowEdges)
1058+
.values(edgeValues)
1059+
.onConflictDoUpdate({
1060+
target: workflowEdges.id,
1061+
set: {
1062+
sourceBlockId: sql`excluded.source_block_id`,
1063+
targetBlockId: sql`excluded.target_block_id`,
1064+
sourceHandle: sql`excluded.source_handle`,
1065+
targetHandle: sql`excluded.target_handle`,
1066+
},
1067+
})
1068+
}
10421069
}
10431070

10441071
if (loops && Object.keys(loops).length > 0) {

0 commit comments

Comments
 (0)