(
trx: DbTransaction,
table: TableDefinition,
columns: { id?: string; name: string; type: string; required?: boolean; unique?: boolean }[],
requestId: string
)
| 413 | * inserts) that must succeed or roll back together. |
| 414 | */ |
| 415 | export async function addTableColumnsWithTx( |
| 416 | trx: DbTransaction, |
| 417 | table: TableDefinition, |
| 418 | columns: { id?: string; name: string; type: string; required?: boolean; unique?: boolean }[], |
| 419 | requestId: string |
| 420 | ): Promise<TableDefinition> { |
| 421 | if (columns.length === 0) return table |
| 422 | |
| 423 | const usedNames = new Set(table.schema.columns.map((c) => c.name.toLowerCase())) |
| 424 | const additions: TableSchema['columns'] = [] |
| 425 | |
| 426 | for (const column of columns) { |
| 427 | if (!NAME_PATTERN.test(column.name)) { |
| 428 | throw new Error( |
| 429 | `Invalid column name "${column.name}". Must start with a letter or underscore and contain only alphanumeric characters and underscores.` |
| 430 | ) |
| 431 | } |
| 432 | if (column.name.length > TABLE_LIMITS.MAX_COLUMN_NAME_LENGTH) { |
| 433 | throw new Error( |
| 434 | `Column name exceeds maximum length (${TABLE_LIMITS.MAX_COLUMN_NAME_LENGTH} characters)` |
| 435 | ) |
| 436 | } |
| 437 | if (!COLUMN_TYPES.includes(column.type as (typeof COLUMN_TYPES)[number])) { |
| 438 | throw new Error( |
| 439 | `Invalid column type "${column.type}". Must be one of: ${COLUMN_TYPES.join(', ')}` |
| 440 | ) |
| 441 | } |
| 442 | const lower = column.name.toLowerCase() |
| 443 | if (usedNames.has(lower)) { |
| 444 | throw new Error(`Column "${column.name}" already exists`) |
| 445 | } |
| 446 | usedNames.add(lower) |
| 447 | // Honor a caller-assigned id (the CSV append path pre-assigns so coercion |
| 448 | // and persistence agree); otherwise mint one. |
| 449 | const id = column.id ?? generateColumnId() |
| 450 | additions.push({ |
| 451 | id, |
| 452 | name: column.name, |
| 453 | type: column.type as TableSchema['columns'][number]['type'], |
| 454 | required: column.required ?? false, |
| 455 | unique: column.unique ?? false, |
| 456 | }) |
| 457 | } |
| 458 | |
| 459 | if (table.schema.columns.length + additions.length > TABLE_LIMITS.MAX_COLUMNS_PER_TABLE) { |
| 460 | throw new Error( |
| 461 | `Adding ${additions.length} column(s) would exceed maximum column limit (${TABLE_LIMITS.MAX_COLUMNS_PER_TABLE})` |
| 462 | ) |
| 463 | } |
| 464 | |
| 465 | // Spread `table.schema` first so workflow groups (and any future top-level |
| 466 | // schema fields) survive a CSV import that only adds plain columns. |
| 467 | const updatedSchema: TableSchema = { |
| 468 | ...table.schema, |
| 469 | columns: [...table.schema.columns, ...additions], |
| 470 | } |
| 471 | const now = new Date() |
| 472 |
no test coverage detected