( data: CreateTableData, requestId: string )
| 237 | * @throws Error if validation fails or limits exceeded |
| 238 | */ |
| 239 | export async function createTable( |
| 240 | data: CreateTableData, |
| 241 | requestId: string |
| 242 | ): Promise<TableDefinition> { |
| 243 | // Validate table name |
| 244 | const nameValidation = validateTableName(data.name) |
| 245 | if (!nameValidation.valid) { |
| 246 | throw new Error(`Invalid table name: ${nameValidation.errors.join(', ')}`) |
| 247 | } |
| 248 | |
| 249 | // Validate schema |
| 250 | const schemaValidation = validateTableSchema(data.schema) |
| 251 | if (!schemaValidation.valid) { |
| 252 | throw new Error(`Invalid schema: ${schemaValidation.errors.join(', ')}`) |
| 253 | } |
| 254 | |
| 255 | const tableId = `tbl_${generateId().replace(/-/g, '')}` |
| 256 | const now = new Date() |
| 257 | |
| 258 | // Stamp stable ids so the table is id-keyed from its first row write. |
| 259 | const schema = withGeneratedColumnIds(data.schema) |
| 260 | |
| 261 | // Row limits are enforced per-write against the current plan (see assertRowCapacity); the stored |
| 262 | // column is vestigial, so it just takes the caller's value (if any) or the default. |
| 263 | const maxRows = data.maxRows ?? TABLE_LIMITS.MAX_ROWS_PER_TABLE |
| 264 | const maxTables = data.maxTables ?? TABLE_LIMITS.MAX_TABLES_PER_WORKSPACE |
| 265 | |
| 266 | const newTable = { |
| 267 | id: tableId, |
| 268 | name: data.name, |
| 269 | description: data.description ?? null, |
| 270 | schema, |
| 271 | workspaceId: data.workspaceId, |
| 272 | createdBy: data.userId, |
| 273 | maxRows, |
| 274 | archivedAt: null, |
| 275 | createdAt: now, |
| 276 | updatedAt: now, |
| 277 | } |
| 278 | |
| 279 | // Create-mode CSV import is born with a running job so its rows stay hidden until ready. |
| 280 | const initialJob = |
| 281 | data.jobStatus === 'running' && data.jobId |
| 282 | ? { id: data.jobId, type: data.jobType ?? 'import', startedAt: now } |
| 283 | : null |
| 284 | |
| 285 | // Starter rows count against the plan too. Checked before the tx (the lookup is a |
| 286 | // separate pool read) — a new table starts empty, so the footprint is just these. |
| 287 | const initialRowCount = data.initialRowCount ?? 0 |
| 288 | let rowLimit: number | undefined |
| 289 | if (initialRowCount > 0) { |
| 290 | rowLimit = await assertRowCapacity({ |
| 291 | workspaceId: data.workspaceId, |
| 292 | currentRowCount: 0, |
| 293 | addedRows: initialRowCount, |
| 294 | }) |
| 295 | } |
| 296 |
no test coverage detected