schema-export.ts97 lines · main
1import { 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
18interface ExportColumn {
19 name: string;
20 sqlType: string;
21 nullable: boolean;
22 primaryKey: boolean;
23}
24
25interface ExportTable {
26 name: string;
27 columns: ExportColumn[];
28}
29
30export interface ExportInput {
31 tables: ExportTable[];
32}
33
34const 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
47function 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
61export 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
78export 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}