(input: {
tableId: string
groups: Array<{ id: string; outputs: Array<{ columnName: string }> }>
rowIds?: string[]
/** Select-all scope: deselected rows whose outputs must NOT be wiped. */
excludeRowIds?: string[]
mode: DispatchMode
})
| 93 | * walks to them. For `mode: 'incomplete'` we skip rows whose outputs are |
| 94 | * already filled, mirroring the eligibility predicate. */ |
| 95 | export async function bulkClearWorkflowGroupCells(input: { |
| 96 | tableId: string |
| 97 | groups: Array<{ id: string; outputs: Array<{ columnName: string }> }> |
| 98 | rowIds?: string[] |
| 99 | /** Select-all scope: deselected rows whose outputs must NOT be wiped. */ |
| 100 | excludeRowIds?: string[] |
| 101 | mode: DispatchMode |
| 102 | }): Promise<void> { |
| 103 | const { tableId, groups, rowIds, excludeRowIds, mode } = input |
| 104 | if (groups.length === 0) return |
| 105 | // `'new'` mode targets only rows with no prior attempt — nothing to clear. |
| 106 | // Pre-existing outputs on any other row must not be wiped by an auto-fire. |
| 107 | if (mode === 'new') return |
| 108 | |
| 109 | const groupIds = groups.map((g) => g.id) |
| 110 | const rowScope = rowIds && rowIds.length > 0 ? rowIds : null |
| 111 | const excluded = !rowScope && excludeRowIds && excludeRowIds.length > 0 ? excludeRowIds : null |
| 112 | |
| 113 | if (mode === 'all') { |
| 114 | // Run-all re-runs every targeted group: wipe all their output columns + |
| 115 | // executions for the rows in scope. (Prior in-flight runs were already |
| 116 | // cancelled by the caller.) |
| 117 | const outputCols = Array.from( |
| 118 | new Set(groups.flatMap((g) => g.outputs.map((o) => o.columnName))) |
| 119 | ) |
| 120 | let dataExpr: SQL = sql`coalesce(${userTableRows.data}, '{}'::jsonb)` |
| 121 | for (const col of outputCols) dataExpr = sql`(${dataExpr}) - ${col}::text` |
| 122 | const filters: SQL[] = [eq(userTableRows.tableId, tableId)] |
| 123 | if (rowScope) filters.push(inArray(userTableRows.id, rowScope)) |
| 124 | if (excluded) filters.push(notInArray(userTableRows.id, excluded)) |
| 125 | |
| 126 | await db.transaction(async (trx) => { |
| 127 | await trx |
| 128 | .update(userTableRows) |
| 129 | .set({ data: dataExpr, updatedAt: new Date() }) |
| 130 | .where(and(...filters)) |
| 131 | const execFilters: SQL[] = [ |
| 132 | eq(tableRowExecutions.tableId, tableId), |
| 133 | inArray(tableRowExecutions.groupId, groupIds), |
| 134 | ] |
| 135 | if (rowScope) execFilters.push(inArray(tableRowExecutions.rowId, rowScope)) |
| 136 | if (excluded) execFilters.push(notInArray(tableRowExecutions.rowId, excluded)) |
| 137 | await trx.delete(tableRowExecutions).where(and(...execFilters)) |
| 138 | }) |
| 139 | return |
| 140 | } |
| 141 | |
| 142 | // `incomplete`: clear per-group, not per-row. Only groups that are |
| 143 | // re-runnable (`error` / `cancelled`) get their output columns + exec wiped; |
| 144 | // `completed` and in-flight groups are left fully intact. A row-level "all |
| 145 | // filled" check would otherwise wipe a completed group's data + exec just |
| 146 | // because a *sibling* group on the same row is incomplete, re-running the |
| 147 | // completed one. (`never-run` groups have no exec/output to clear — the |
| 148 | // dispatcher runs them via eligibility.) |
| 149 | await db.transaction(async (trx) => { |
| 150 | for (const group of groups) { |
| 151 | const reRunnable = sql`EXISTS ( |
| 152 | SELECT 1 FROM ${tableRowExecutions} re |
no test coverage detected