( data: AddWorkflowGroupData, requestId: string )
| 116 | * mid-mutation (e.g. columns referencing a group that doesn't yet exist). |
| 117 | */ |
| 118 | export async function addWorkflowGroup( |
| 119 | data: AddWorkflowGroupData, |
| 120 | requestId: string |
| 121 | ): Promise<TableDefinition> { |
| 122 | const updatedTable = await withLockedTable(data.tableId, async (table, trx) => { |
| 123 | const schema = table.schema |
| 124 | const groups = schema.workflowGroups ?? [] |
| 125 | if (groups.some((g) => g.id === data.group.id)) { |
| 126 | throw new Error(`Workflow group "${data.group.id}" already exists`) |
| 127 | } |
| 128 | |
| 129 | const existingNames = new Set(schema.columns.map((c) => c.name.toLowerCase())) |
| 130 | for (const col of data.outputColumns) { |
| 131 | if (!NAME_PATTERN.test(col.name)) { |
| 132 | throw new Error( |
| 133 | `Invalid output column name "${col.name}". Must satisfy ${NAME_PATTERN.source}.` |
| 134 | ) |
| 135 | } |
| 136 | if (existingNames.has(col.name.toLowerCase())) { |
| 137 | throw new Error(`Column "${col.name}" already exists`) |
| 138 | } |
| 139 | } |
| 140 | |
| 141 | if (schema.columns.length + data.outputColumns.length > TABLE_LIMITS.MAX_COLUMNS_PER_TABLE) { |
| 142 | throw new Error( |
| 143 | `Adding ${data.outputColumns.length} columns would exceed the maximum (${TABLE_LIMITS.MAX_COLUMNS_PER_TABLE}).` |
| 144 | ) |
| 145 | } |
| 146 | |
| 147 | // Assign stable ids to the new output columns, then rewrite the group's |
| 148 | // column refs from name → id so outputs/deps/inputMappings key on ids — |
| 149 | // matching the row-data storage key and surviving future renames. |
| 150 | const outputColumns = data.outputColumns.map((col) => |
| 151 | col.id ? col : { ...col, id: generateColumnId() } |
| 152 | ) |
| 153 | const updatedColumns = [...schema.columns, ...outputColumns] |
| 154 | const idByName = new Map(updatedColumns.map((c) => [c.name, getColumnId(c)])) |
| 155 | const group = remapGroupColumnRefs(data.group, idByName) |
| 156 | |
| 157 | const updatedSchema: TableSchema = { |
| 158 | ...schema, |
| 159 | columns: updatedColumns, |
| 160 | workflowGroups: [...groups, group], |
| 161 | } |
| 162 | |
| 163 | // Keep `metadata.columnOrder` (column ids) in sync — see `addTableColumn`. |
| 164 | // New output columns get appended in the order the caller supplied. |
| 165 | const existingOrder = table.metadata?.columnOrder |
| 166 | let updatedMetadata = table.metadata |
| 167 | if (existingOrder && existingOrder.length > 0) { |
| 168 | const known = new Set(existingOrder) |
| 169 | const append = outputColumns.map(getColumnId).filter((id) => !known.has(id)) |
| 170 | if (append.length > 0) { |
| 171 | updatedMetadata = { ...table.metadata, columnOrder: [...existingOrder, ...append] } |
| 172 | } |
| 173 | } |
| 174 | |
| 175 | assertValidSchema(updatedSchema, updatedMetadata?.columnOrder) |
no test coverage detected