schema-export.ts97 lines · main
| 1 | import { getTableColumns, listProjectTables } from './studio.js'; |
| 2 | |
| 3 | /** |
| 4 | * Postgres-schema → briven DSL transcriber. |
| 5 | * |
| 6 | * Used by the schema-export route and consumed by the CLI's `existingBranch` |
| 7 | * flow to pre-populate `briven/schema.ts` from a database the user built by |
| 8 | * clicking around in studio. The intent is "good-enough first draft" — the |
| 9 | * generated file is meant to be committed and then hand-edited where the |
| 10 | * transcribed types are wrong. |
| 11 | * |
| 12 | * The pure `exportSchemaToDsl` function takes a plain `ExportInput` shape so |
| 13 | * tests can exercise the rendering logic without spinning up a live data |
| 14 | * plane. The DB-coupled wrapper `exportProjectSchema(projectId)` adapts the |
| 15 | * studio `ColumnInfo` rows into that shape. |
| 16 | */ |
| 17 | |
| 18 | interface ExportColumn { |
| 19 | name: string; |
| 20 | sqlType: string; |
| 21 | nullable: boolean; |
| 22 | primaryKey: boolean; |
| 23 | } |
| 24 | |
| 25 | interface ExportTable { |
| 26 | name: string; |
| 27 | columns: ExportColumn[]; |
| 28 | } |
| 29 | |
| 30 | export interface ExportInput { |
| 31 | tables: ExportTable[]; |
| 32 | } |
| 33 | |
| 34 | const TYPE_MAP: Record<string, string> = { |
| 35 | text: 'text()', |
| 36 | varchar: 'text()', |
| 37 | uuid: 'text()', |
| 38 | int4: 'integer()', |
| 39 | int8: 'bigint()', |
| 40 | bool: 'boolean()', |
| 41 | jsonb: 'json()', |
| 42 | timestamp: 'timestamp()', |
| 43 | timestamptz: 'timestamp()', |
| 44 | date: 'date()', |
| 45 | }; |
| 46 | |
| 47 | function renderColumn(c: ExportColumn): string { |
| 48 | const base = TYPE_MAP[c.sqlType] ?? null; |
| 49 | if (!base) { |
| 50 | return [ |
| 51 | ` // TODO: unsupported type '${c.sqlType}' — confirm`, |
| 52 | ` ${c.name}: text(),`, |
| 53 | ].join('\n'); |
| 54 | } |
| 55 | let chain = base; |
| 56 | if (c.primaryKey) chain += '.primaryKey()'; |
| 57 | else if (!c.nullable) chain += '.notNull()'; |
| 58 | return ` ${c.name}: ${chain},`; |
| 59 | } |
| 60 | |
| 61 | export function exportSchemaToDsl(input: ExportInput): string { |
| 62 | const importedSymbols = new Set<string>(['schema', 'table']); |
| 63 | for (const t of input.tables) { |
| 64 | for (const col of t.columns) { |
| 65 | const mapped = TYPE_MAP[col.sqlType]; |
| 66 | if (mapped) importedSymbols.add(mapped.replace(/\(\)$/, '')); |
| 67 | else importedSymbols.add('text'); |
| 68 | } |
| 69 | } |
| 70 | const importLine = `import { ${Array.from(importedSymbols).sort().join(', ')} } from '@briven/cli/schema';`; |
| 71 | const tableLines = input.tables.map((t) => { |
| 72 | const cols = t.columns.map(renderColumn).join('\n'); |
| 73 | return ` ${t.name}: table({\n${cols}\n }),`; |
| 74 | }); |
| 75 | return `${importLine}\n\nexport default schema({\n${tableLines.join('\n')}\n});\n`; |
| 76 | } |
| 77 | |
| 78 | export async function exportProjectSchema(projectId: string): Promise<string> { |
| 79 | const tables = await listProjectTables(projectId); |
| 80 | const expanded: ExportTable[] = []; |
| 81 | for (const t of tables) { |
| 82 | const columns = await getTableColumns(projectId, t.name); |
| 83 | expanded.push({ |
| 84 | name: t.name, |
| 85 | // Studio's ColumnInfo uses `dataType` / `nullable` / `isPrimaryKey`. |
| 86 | // Map onto our DB-free ExportColumn so the pure renderer stays |
| 87 | // decoupled from the live-DB row shape. |
| 88 | columns: columns.map((c) => ({ |
| 89 | name: c.name, |
| 90 | sqlType: c.dataType, |
| 91 | nullable: c.nullable, |
| 92 | primaryKey: c.isPrimaryKey, |
| 93 | })), |
| 94 | }); |
| 95 | } |
| 96 | return exportSchemaToDsl({ tables: expanded }); |
| 97 | } |