turbine-orm 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -2
- package/dist/cjs/cli/config.js +161 -0
- package/dist/cjs/cli/index.js +977 -0
- package/dist/cjs/cli/migrate.js +421 -0
- package/dist/cjs/cli/ui.js +237 -0
- package/dist/cjs/client.js +449 -0
- package/dist/cjs/generate.js +301 -0
- package/dist/cjs/index.js +75 -0
- package/dist/cjs/introspect.js +289 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/pipeline.js +71 -0
- package/dist/cjs/query.js +1558 -0
- package/dist/cjs/schema-builder.js +169 -0
- package/dist/cjs/schema-sql.js +371 -0
- package/dist/cjs/schema.js +137 -0
- package/dist/cjs/serverless.js +199 -0
- package/dist/cli/config.js +1 -1
- package/dist/cli/index.js +16 -8
- package/dist/cli/migrate.d.ts +29 -5
- package/dist/cli/migrate.js +58 -35
- package/dist/cli/ui.js +1 -1
- package/dist/client.d.ts +15 -4
- package/dist/client.js +28 -15
- package/dist/generate.d.ts +1 -1
- package/dist/generate.js +13 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/introspect.d.ts +1 -1
- package/dist/introspect.js +1 -1
- package/dist/pipeline.d.ts +1 -1
- package/dist/pipeline.js +1 -1
- package/dist/query.d.ts +55 -11
- package/dist/query.js +135 -140
- package/dist/schema-builder.d.ts +2 -2
- package/dist/schema-builder.js +2 -2
- package/dist/schema-sql.d.ts +1 -1
- package/dist/schema-sql.js +31 -15
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +1 -1
- package/dist/serverless.d.ts +3 -3
- package/dist/serverless.js +4 -4
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/package.json +17 -11
- package/dist/cli/config.d.ts.map +0 -1
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/migrate.d.ts.map +0 -1
- package/dist/cli/ui.d.ts.map +0 -1
- package/dist/client.d.ts.map +0 -1
- package/dist/generate.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/introspect.d.ts.map +0 -1
- package/dist/pipeline.d.ts.map +0 -1
- package/dist/query.d.ts.map +0 -1
- package/dist/schema-builder.d.ts.map +0 -1
- package/dist/schema-sql.d.ts.map +0 -1
- package/dist/schema.d.ts.map +0 -1
- package/dist/serverless.d.ts.map +0 -1
- package/dist/types.d.ts.map +0 -1
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @batadata/turbine — Code generator
|
|
4
|
+
*
|
|
5
|
+
* Takes an IntrospectedSchema and emits TypeScript files:
|
|
6
|
+
* - types.ts — Entity interfaces, Create/Update input types
|
|
7
|
+
* - metadata.ts — Runtime schema metadata (column maps, relations, etc.)
|
|
8
|
+
* - index.ts — Configured TurbineClient with typed table accessors
|
|
9
|
+
*
|
|
10
|
+
* Output goes to the specified directory (default: ./generated/turbine/).
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.generate = generate;
|
|
14
|
+
const node_fs_1 = require("node:fs");
|
|
15
|
+
const node_path_1 = require("node:path");
|
|
16
|
+
const schema_js_1 = require("./schema.js");
|
|
17
|
+
/** Get the TypeScript type name for a table (singularized PascalCase) */
|
|
18
|
+
function entityName(tableName) {
|
|
19
|
+
return (0, schema_js_1.snakeToPascal)((0, schema_js_1.singularize)(tableName));
|
|
20
|
+
}
|
|
21
|
+
/** Escape a value for embedding in a single-quoted TypeScript string literal */
|
|
22
|
+
function escSQ(value) {
|
|
23
|
+
return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
24
|
+
}
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Main generate function
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
function generate(options) {
|
|
29
|
+
const outDir = options.outDir ?? './generated/turbine';
|
|
30
|
+
// Path traversal protection — ensure output stays within project root
|
|
31
|
+
const resolved = (0, node_path_1.resolve)(outDir);
|
|
32
|
+
const rel = (0, node_path_1.relative)(process.cwd(), resolved);
|
|
33
|
+
if (rel.startsWith('..') || (0, node_path_1.resolve)(rel) !== resolved) {
|
|
34
|
+
throw new Error(`Output directory must be within the project root. Got: ${outDir}`);
|
|
35
|
+
}
|
|
36
|
+
(0, node_fs_1.mkdirSync)(outDir, { recursive: true });
|
|
37
|
+
const files = [];
|
|
38
|
+
// Generate types.ts
|
|
39
|
+
const typesContent = generateTypes(options.schema);
|
|
40
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(outDir, 'types.ts'), typesContent, 'utf-8');
|
|
41
|
+
files.push('types.ts');
|
|
42
|
+
// Generate metadata.ts
|
|
43
|
+
const metadataContent = generateMetadata(options.schema);
|
|
44
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(outDir, 'metadata.ts'), metadataContent, 'utf-8');
|
|
45
|
+
files.push('metadata.ts');
|
|
46
|
+
// Generate index.ts (configured client)
|
|
47
|
+
const indexContent = generateIndex(options.schema);
|
|
48
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(outDir, 'index.ts'), indexContent, 'utf-8');
|
|
49
|
+
files.push('index.ts');
|
|
50
|
+
return { outDir, files };
|
|
51
|
+
}
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// types.ts generator
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
function generatedFileHeader() {
|
|
56
|
+
return [
|
|
57
|
+
'/**',
|
|
58
|
+
' * Auto-generated by @batadata/turbine — DO NOT EDIT',
|
|
59
|
+
' *',
|
|
60
|
+
` * Generated at: ${new Date().toISOString()}`,
|
|
61
|
+
' * @see https://batadata.com/docs/turbine',
|
|
62
|
+
' */',
|
|
63
|
+
'',
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
function generateTypes(schema) {
|
|
67
|
+
const lines = [
|
|
68
|
+
...generatedFileHeader(),
|
|
69
|
+
];
|
|
70
|
+
// Generate enum types
|
|
71
|
+
for (const [enumName, labels] of Object.entries(schema.enums)) {
|
|
72
|
+
const typeName = (0, schema_js_1.snakeToPascal)(enumName);
|
|
73
|
+
lines.push(`/** Database enum: ${enumName} */`);
|
|
74
|
+
lines.push(`export type ${typeName} = ${labels.map((l) => `'${escSQ(l)}'`).join(' | ')};`);
|
|
75
|
+
lines.push('');
|
|
76
|
+
}
|
|
77
|
+
// Generate entity types for each table
|
|
78
|
+
for (const table of Object.values(schema.tables)) {
|
|
79
|
+
const typeName = entityName(table.name);
|
|
80
|
+
// --- Base entity interface ---
|
|
81
|
+
lines.push(`/** Row type for the \`${table.name}\` table */`);
|
|
82
|
+
lines.push(`export interface ${typeName} {`);
|
|
83
|
+
for (const col of table.columns) {
|
|
84
|
+
const pkNote = table.primaryKey.includes(col.name) ? ' (primary key)' : '';
|
|
85
|
+
const nullNote = col.nullable ? ' (nullable)' : '';
|
|
86
|
+
lines.push(` /** Column: ${col.name} — ${col.pgType}${pkNote}${nullNote} */`);
|
|
87
|
+
lines.push(` ${col.field}: ${col.tsType};`);
|
|
88
|
+
}
|
|
89
|
+
lines.push('}');
|
|
90
|
+
lines.push('');
|
|
91
|
+
// --- Create input type ---
|
|
92
|
+
// Required: non-nullable columns without defaults (except PK)
|
|
93
|
+
// Optional: nullable columns (default to NULL) or columns with explicit defaults
|
|
94
|
+
lines.push(`/** Input type for creating a row in \`${table.name}\` */`);
|
|
95
|
+
lines.push(`export type ${typeName}Create = {`);
|
|
96
|
+
for (const col of table.columns) {
|
|
97
|
+
const isPk = table.primaryKey.includes(col.name);
|
|
98
|
+
const isOptional = col.hasDefault || col.nullable || isPk;
|
|
99
|
+
if (isOptional) {
|
|
100
|
+
const reason = isPk ? 'auto-generated' : col.hasDefault ? 'has default' : 'nullable';
|
|
101
|
+
lines.push(` /** Optional: ${reason} */`);
|
|
102
|
+
lines.push(` ${col.field}?: ${col.tsType};`);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
lines.push(` ${col.field}: ${col.tsType};`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
lines.push('};');
|
|
109
|
+
lines.push('');
|
|
110
|
+
// --- Update input type (all fields optional except PK) ---
|
|
111
|
+
const nonPkCols = table.columns.filter((c) => !table.primaryKey.includes(c.name));
|
|
112
|
+
lines.push(`/** Input type for updating a row in \`${table.name}\` */`);
|
|
113
|
+
lines.push(`export type ${typeName}Update = {`);
|
|
114
|
+
for (const col of nonPkCols) {
|
|
115
|
+
lines.push(` ${col.field}?: ${col.tsType};`);
|
|
116
|
+
}
|
|
117
|
+
lines.push('};');
|
|
118
|
+
lines.push('');
|
|
119
|
+
// --- Relation types ---
|
|
120
|
+
const hasRelations = Object.keys(table.relations).length > 0;
|
|
121
|
+
if (hasRelations) {
|
|
122
|
+
for (const [relName, rel] of Object.entries(table.relations)) {
|
|
123
|
+
const targetType = entityName(rel.to);
|
|
124
|
+
if (rel.type === 'hasMany') {
|
|
125
|
+
lines.push(`/** ${typeName} with \`${relName}\` relation loaded (${rel.type}: ${rel.to}) */`);
|
|
126
|
+
lines.push(`export interface ${typeName}With${(0, schema_js_1.snakeToPascal)(relName)} extends ${typeName} {`);
|
|
127
|
+
lines.push(` ${relName}: ${targetType}[];`);
|
|
128
|
+
lines.push('}');
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
lines.push(`/** ${typeName} with \`${relName}\` relation loaded (${rel.type}: ${rel.to}) */`);
|
|
132
|
+
lines.push(`export interface ${typeName}With${(0, schema_js_1.snakeToPascal)(relName)} extends ${typeName} {`);
|
|
133
|
+
lines.push(` ${relName}: ${targetType} | null;`);
|
|
134
|
+
lines.push('}');
|
|
135
|
+
}
|
|
136
|
+
lines.push('');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return lines.join('\n');
|
|
141
|
+
}
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// metadata.ts generator
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
function generateMetadata(schema) {
|
|
146
|
+
const lines = [
|
|
147
|
+
...generatedFileHeader(),
|
|
148
|
+
"import type { SchemaMetadata } from '@batadata/turbine';",
|
|
149
|
+
'',
|
|
150
|
+
'export const SCHEMA: SchemaMetadata = {',
|
|
151
|
+
' tables: {',
|
|
152
|
+
];
|
|
153
|
+
for (const table of Object.values(schema.tables)) {
|
|
154
|
+
lines.push(` ${table.name}: {`);
|
|
155
|
+
lines.push(` name: '${escSQ(table.name)}',`);
|
|
156
|
+
// columns
|
|
157
|
+
lines.push(' columns: [');
|
|
158
|
+
for (const col of table.columns) {
|
|
159
|
+
lines.push(` ${serializeColumn(col)},`);
|
|
160
|
+
}
|
|
161
|
+
lines.push(' ],');
|
|
162
|
+
// columnMap
|
|
163
|
+
lines.push(' columnMap: {');
|
|
164
|
+
for (const [field, col] of Object.entries(table.columnMap)) {
|
|
165
|
+
lines.push(` ${field}: '${escSQ(col)}',`);
|
|
166
|
+
}
|
|
167
|
+
lines.push(' },');
|
|
168
|
+
// reverseColumnMap
|
|
169
|
+
lines.push(' reverseColumnMap: {');
|
|
170
|
+
for (const [col, field] of Object.entries(table.reverseColumnMap)) {
|
|
171
|
+
lines.push(` ${quoteIfNeeded(col)}: '${escSQ(field)}',`);
|
|
172
|
+
}
|
|
173
|
+
lines.push(' },');
|
|
174
|
+
// dateColumns
|
|
175
|
+
const dateCols = [...table.dateColumns];
|
|
176
|
+
lines.push(` dateColumns: new Set([${dateCols.map((c) => `'${escSQ(c)}'`).join(', ')}]),`);
|
|
177
|
+
// pgTypes
|
|
178
|
+
lines.push(' pgTypes: {');
|
|
179
|
+
for (const [col, pgType] of Object.entries(table.pgTypes)) {
|
|
180
|
+
lines.push(` ${quoteIfNeeded(col)}: '${escSQ(pgType)}',`);
|
|
181
|
+
}
|
|
182
|
+
lines.push(' },');
|
|
183
|
+
// allColumns
|
|
184
|
+
lines.push(` allColumns: [${table.allColumns.map((c) => `'${escSQ(c)}'`).join(', ')}],`);
|
|
185
|
+
// primaryKey
|
|
186
|
+
lines.push(` primaryKey: [${table.primaryKey.map((c) => `'${escSQ(c)}'`).join(', ')}],`);
|
|
187
|
+
// uniqueColumns
|
|
188
|
+
lines.push(` uniqueColumns: [${table.uniqueColumns.map((uc) => `[${uc.map((c) => `'${escSQ(c)}'`).join(', ')}]`).join(', ')}],`);
|
|
189
|
+
// relations
|
|
190
|
+
lines.push(' relations: {');
|
|
191
|
+
for (const [relName, rel] of Object.entries(table.relations)) {
|
|
192
|
+
lines.push(` ${relName}: { type: '${escSQ(rel.type)}', name: '${escSQ(rel.name)}', from: '${escSQ(rel.from)}', to: '${escSQ(rel.to)}', foreignKey: '${escSQ(rel.foreignKey)}', referenceKey: '${escSQ(rel.referenceKey)}' },`);
|
|
193
|
+
}
|
|
194
|
+
lines.push(' },');
|
|
195
|
+
// indexes
|
|
196
|
+
lines.push(' indexes: [');
|
|
197
|
+
for (const idx of table.indexes) {
|
|
198
|
+
lines.push(` { name: '${escSQ(idx.name)}', columns: [${idx.columns.map((c) => `'${escSQ(c)}'`).join(', ')}], unique: ${idx.unique}, definition: ${JSON.stringify(idx.definition)} },`);
|
|
199
|
+
}
|
|
200
|
+
lines.push(' ],');
|
|
201
|
+
lines.push(' },');
|
|
202
|
+
}
|
|
203
|
+
lines.push(' },');
|
|
204
|
+
// enums
|
|
205
|
+
lines.push(' enums: {');
|
|
206
|
+
for (const [enumName, labels] of Object.entries(schema.enums)) {
|
|
207
|
+
lines.push(` ${enumName}: [${labels.map((l) => `'${escSQ(l)}'`).join(', ')}],`);
|
|
208
|
+
}
|
|
209
|
+
lines.push(' },');
|
|
210
|
+
lines.push('};');
|
|
211
|
+
lines.push('');
|
|
212
|
+
return lines.join('\n');
|
|
213
|
+
}
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
// index.ts generator (configured client with typed table accessors)
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
function generateIndex(schema) {
|
|
218
|
+
const tableEntries = Object.values(schema.tables);
|
|
219
|
+
const lines = [
|
|
220
|
+
...generatedFileHeader(),
|
|
221
|
+
"import { TurbineClient as BaseTurbineClient, QueryInterface } from '@batadata/turbine';",
|
|
222
|
+
"import type { TurbineConfig } from '@batadata/turbine';",
|
|
223
|
+
"import { SCHEMA } from './metadata.js';",
|
|
224
|
+
];
|
|
225
|
+
// Import all entity types
|
|
226
|
+
const typeImports = tableEntries.map((t) => entityName(t.name));
|
|
227
|
+
lines.push(`import type { ${typeImports.join(', ')} } from './types.js';`);
|
|
228
|
+
lines.push('');
|
|
229
|
+
// Generate the client class with JSDoc
|
|
230
|
+
lines.push('/**');
|
|
231
|
+
lines.push(' * Generated Turbine client with typed table accessors.');
|
|
232
|
+
lines.push(' *');
|
|
233
|
+
lines.push(' * Tables:');
|
|
234
|
+
for (const table of tableEntries) {
|
|
235
|
+
lines.push(` * - \`${snakeToCamelStr(table.name)}\` (${table.name})`);
|
|
236
|
+
}
|
|
237
|
+
lines.push(' *');
|
|
238
|
+
lines.push(' * @example');
|
|
239
|
+
lines.push(' * ```ts');
|
|
240
|
+
lines.push(' * const db = turbine({ connectionString: process.env.DATABASE_URL });');
|
|
241
|
+
if (tableEntries.length > 0) {
|
|
242
|
+
const firstTable = tableEntries[0];
|
|
243
|
+
const accessor = snakeToCamelStr(firstTable.name);
|
|
244
|
+
lines.push(` * const rows = await db.${accessor}.findMany();`);
|
|
245
|
+
}
|
|
246
|
+
lines.push(' * ```');
|
|
247
|
+
lines.push(' */');
|
|
248
|
+
lines.push('export class TurbineClient extends BaseTurbineClient {');
|
|
249
|
+
for (const table of tableEntries) {
|
|
250
|
+
const typeName = entityName(table.name);
|
|
251
|
+
const accessor = snakeToCamelStr(table.name);
|
|
252
|
+
lines.push(` /** Query interface for the \`${table.name}\` table */`);
|
|
253
|
+
lines.push(` declare readonly ${accessor}: QueryInterface<${typeName}>;`);
|
|
254
|
+
}
|
|
255
|
+
lines.push('');
|
|
256
|
+
lines.push(' constructor(config?: TurbineConfig) {');
|
|
257
|
+
lines.push(' super(config, SCHEMA);');
|
|
258
|
+
lines.push(' }');
|
|
259
|
+
lines.push('}');
|
|
260
|
+
lines.push('');
|
|
261
|
+
// Factory function with JSDoc
|
|
262
|
+
lines.push('/**');
|
|
263
|
+
lines.push(' * Create a new Turbine client instance.');
|
|
264
|
+
lines.push(' *');
|
|
265
|
+
lines.push(' * @param config - Connection configuration. Falls back to DATABASE_URL env var.');
|
|
266
|
+
lines.push(' * @returns A fully-typed TurbineClient with table accessors.');
|
|
267
|
+
lines.push(' */');
|
|
268
|
+
lines.push('export function turbine(config?: TurbineConfig): TurbineClient {');
|
|
269
|
+
lines.push(' return new TurbineClient(config);');
|
|
270
|
+
lines.push('}');
|
|
271
|
+
lines.push('');
|
|
272
|
+
// Re-export everything
|
|
273
|
+
lines.push("export * from './types.js';");
|
|
274
|
+
lines.push("export { SCHEMA } from './metadata.js';");
|
|
275
|
+
lines.push('');
|
|
276
|
+
return lines.join('\n');
|
|
277
|
+
}
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
// Helpers
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
function serializeColumn(col) {
|
|
282
|
+
const parts = [
|
|
283
|
+
`name: '${escSQ(col.name)}'`,
|
|
284
|
+
`field: '${escSQ(col.field)}'`,
|
|
285
|
+
`pgType: '${escSQ(col.pgType)}'`,
|
|
286
|
+
`tsType: '${escSQ(col.tsType)}'`,
|
|
287
|
+
`nullable: ${col.nullable}`,
|
|
288
|
+
`hasDefault: ${col.hasDefault}`,
|
|
289
|
+
`isArray: ${col.isArray}`,
|
|
290
|
+
`pgArrayType: '${escSQ(col.pgArrayType)}'`,
|
|
291
|
+
];
|
|
292
|
+
if (col.maxLength !== undefined)
|
|
293
|
+
parts.push(`maxLength: ${col.maxLength}`);
|
|
294
|
+
return `{ ${parts.join(', ')} }`;
|
|
295
|
+
}
|
|
296
|
+
function quoteIfNeeded(s) {
|
|
297
|
+
return /[^a-zA-Z0-9_$]/.test(s) ? `'${s}'` : s;
|
|
298
|
+
}
|
|
299
|
+
function snakeToCamelStr(s) {
|
|
300
|
+
return s.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
301
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @batadata/turbine
|
|
4
|
+
*
|
|
5
|
+
* Turbine TypeScript SDK — type-safe Postgres queries with nested relations
|
|
6
|
+
* and pipeline batching. Feels like Prisma, runs at raw-SQL speed.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* // 1. Generate types from your database:
|
|
11
|
+
* // npx turbine generate
|
|
12
|
+
*
|
|
13
|
+
* // 2. Import the generated client:
|
|
14
|
+
* import { turbine } from './generated/turbine';
|
|
15
|
+
*
|
|
16
|
+
* const db = turbine({ connectionString: process.env.DATABASE_URL });
|
|
17
|
+
*
|
|
18
|
+
* // Type-safe queries with auto-complete
|
|
19
|
+
* const user = await db.users.findUnique({ where: { id: 1 } });
|
|
20
|
+
*
|
|
21
|
+
* // Nested relations in a single query (json_agg, no N+1)
|
|
22
|
+
* const userWithPosts = await db.users.findUnique({
|
|
23
|
+
* where: { id: 1 },
|
|
24
|
+
* with: { posts: { with: { comments: true } } },
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* // Pipeline: multiple queries in one round-trip
|
|
28
|
+
* const [user, count] = await db.pipeline(
|
|
29
|
+
* db.users.buildFindUnique({ where: { id: 1 } }),
|
|
30
|
+
* db.posts.buildCount({ where: { orgId: 1 } }),
|
|
31
|
+
* );
|
|
32
|
+
*
|
|
33
|
+
* await db.disconnect();
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.schemaPush = exports.schemaDiff = exports.schemaToSQLString = exports.schemaToSQL = exports.ColumnBuilder = exports.column = exports.table = exports.defineSchema = exports.generate = exports.introspect = exports.pgArrayType = exports.isDateType = exports.pgTypeToTs = exports.singularize = exports.snakeToPascal = exports.camelToSnake = exports.snakeToCamel = exports.executePipeline = exports.QueryInterface = exports.TransactionClient = exports.TurbineClient = void 0;
|
|
38
|
+
// Client
|
|
39
|
+
var client_js_1 = require("./client.js");
|
|
40
|
+
Object.defineProperty(exports, "TurbineClient", { enumerable: true, get: function () { return client_js_1.TurbineClient; } });
|
|
41
|
+
Object.defineProperty(exports, "TransactionClient", { enumerable: true, get: function () { return client_js_1.TransactionClient; } });
|
|
42
|
+
// Query builder
|
|
43
|
+
var query_js_1 = require("./query.js");
|
|
44
|
+
Object.defineProperty(exports, "QueryInterface", { enumerable: true, get: function () { return query_js_1.QueryInterface; } });
|
|
45
|
+
// Pipeline
|
|
46
|
+
var pipeline_js_1 = require("./pipeline.js");
|
|
47
|
+
Object.defineProperty(exports, "executePipeline", { enumerable: true, get: function () { return pipeline_js_1.executePipeline; } });
|
|
48
|
+
// Schema utilities
|
|
49
|
+
var schema_js_1 = require("./schema.js");
|
|
50
|
+
Object.defineProperty(exports, "snakeToCamel", { enumerable: true, get: function () { return schema_js_1.snakeToCamel; } });
|
|
51
|
+
Object.defineProperty(exports, "camelToSnake", { enumerable: true, get: function () { return schema_js_1.camelToSnake; } });
|
|
52
|
+
Object.defineProperty(exports, "snakeToPascal", { enumerable: true, get: function () { return schema_js_1.snakeToPascal; } });
|
|
53
|
+
Object.defineProperty(exports, "singularize", { enumerable: true, get: function () { return schema_js_1.singularize; } });
|
|
54
|
+
Object.defineProperty(exports, "pgTypeToTs", { enumerable: true, get: function () { return schema_js_1.pgTypeToTs; } });
|
|
55
|
+
Object.defineProperty(exports, "isDateType", { enumerable: true, get: function () { return schema_js_1.isDateType; } });
|
|
56
|
+
Object.defineProperty(exports, "pgArrayType", { enumerable: true, get: function () { return schema_js_1.pgArrayType; } });
|
|
57
|
+
// Introspection
|
|
58
|
+
var introspect_js_1 = require("./introspect.js");
|
|
59
|
+
Object.defineProperty(exports, "introspect", { enumerable: true, get: function () { return introspect_js_1.introspect; } });
|
|
60
|
+
// Code generation
|
|
61
|
+
var generate_js_1 = require("./generate.js");
|
|
62
|
+
Object.defineProperty(exports, "generate", { enumerable: true, get: function () { return generate_js_1.generate; } });
|
|
63
|
+
// Schema builder — define schemas in TypeScript
|
|
64
|
+
var schema_builder_js_1 = require("./schema-builder.js");
|
|
65
|
+
Object.defineProperty(exports, "defineSchema", { enumerable: true, get: function () { return schema_builder_js_1.defineSchema; } });
|
|
66
|
+
// Legacy compat (deprecated — use object format with defineSchema)
|
|
67
|
+
Object.defineProperty(exports, "table", { enumerable: true, get: function () { return schema_builder_js_1.table; } });
|
|
68
|
+
Object.defineProperty(exports, "column", { enumerable: true, get: function () { return schema_builder_js_1.column; } });
|
|
69
|
+
Object.defineProperty(exports, "ColumnBuilder", { enumerable: true, get: function () { return schema_builder_js_1.ColumnBuilder; } });
|
|
70
|
+
// Schema SQL — generate DDL, diff, and push
|
|
71
|
+
var schema_sql_js_1 = require("./schema-sql.js");
|
|
72
|
+
Object.defineProperty(exports, "schemaToSQL", { enumerable: true, get: function () { return schema_sql_js_1.schemaToSQL; } });
|
|
73
|
+
Object.defineProperty(exports, "schemaToSQLString", { enumerable: true, get: function () { return schema_sql_js_1.schemaToSQLString; } });
|
|
74
|
+
Object.defineProperty(exports, "schemaDiff", { enumerable: true, get: function () { return schema_sql_js_1.schemaDiff; } });
|
|
75
|
+
Object.defineProperty(exports, "schemaPush", { enumerable: true, get: function () { return schema_sql_js_1.schemaPush; } });
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @batadata/turbine — Schema introspection
|
|
4
|
+
*
|
|
5
|
+
* Connects to a live Postgres database, reads information_schema + pg_catalog,
|
|
6
|
+
* and produces a SchemaMetadata object describing every table, column, relation,
|
|
7
|
+
* and index in the target schema.
|
|
8
|
+
*
|
|
9
|
+
* This is the foundation of `npx turbine generate`.
|
|
10
|
+
*/
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.introspect = introspect;
|
|
16
|
+
const pg_1 = __importDefault(require("pg"));
|
|
17
|
+
const schema_js_1 = require("./schema.js");
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// SQL queries (all parameterized, no interpolation)
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
const SQL_TABLES = `
|
|
22
|
+
SELECT table_name
|
|
23
|
+
FROM information_schema.tables
|
|
24
|
+
WHERE table_schema = $1
|
|
25
|
+
AND table_type = 'BASE TABLE'
|
|
26
|
+
ORDER BY table_name
|
|
27
|
+
`;
|
|
28
|
+
const SQL_COLUMNS = `
|
|
29
|
+
SELECT
|
|
30
|
+
table_name,
|
|
31
|
+
column_name,
|
|
32
|
+
udt_name,
|
|
33
|
+
data_type,
|
|
34
|
+
is_nullable,
|
|
35
|
+
column_default,
|
|
36
|
+
ordinal_position,
|
|
37
|
+
character_maximum_length
|
|
38
|
+
FROM information_schema.columns
|
|
39
|
+
WHERE table_schema = $1
|
|
40
|
+
ORDER BY table_name, ordinal_position
|
|
41
|
+
`;
|
|
42
|
+
const SQL_PRIMARY_KEYS = `
|
|
43
|
+
SELECT
|
|
44
|
+
tc.table_name,
|
|
45
|
+
kcu.column_name,
|
|
46
|
+
kcu.ordinal_position
|
|
47
|
+
FROM information_schema.table_constraints tc
|
|
48
|
+
JOIN information_schema.key_column_usage kcu
|
|
49
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
50
|
+
AND tc.table_schema = kcu.table_schema
|
|
51
|
+
WHERE tc.constraint_type = 'PRIMARY KEY'
|
|
52
|
+
AND tc.table_schema = $1
|
|
53
|
+
ORDER BY tc.table_name, kcu.ordinal_position
|
|
54
|
+
`;
|
|
55
|
+
const SQL_FOREIGN_KEYS = `
|
|
56
|
+
SELECT
|
|
57
|
+
tc.table_name AS source_table,
|
|
58
|
+
kcu.column_name AS source_column,
|
|
59
|
+
ccu.table_name AS target_table,
|
|
60
|
+
ccu.column_name AS target_column,
|
|
61
|
+
tc.constraint_name
|
|
62
|
+
FROM information_schema.table_constraints tc
|
|
63
|
+
JOIN information_schema.key_column_usage kcu
|
|
64
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
65
|
+
AND tc.table_schema = kcu.table_schema
|
|
66
|
+
JOIN information_schema.constraint_column_usage ccu
|
|
67
|
+
ON tc.constraint_name = ccu.constraint_name
|
|
68
|
+
AND tc.table_schema = ccu.table_schema
|
|
69
|
+
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
70
|
+
AND tc.table_schema = $1
|
|
71
|
+
`;
|
|
72
|
+
const SQL_UNIQUE_CONSTRAINTS = `
|
|
73
|
+
SELECT
|
|
74
|
+
tc.table_name,
|
|
75
|
+
kcu.column_name
|
|
76
|
+
FROM information_schema.table_constraints tc
|
|
77
|
+
JOIN information_schema.key_column_usage kcu
|
|
78
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
79
|
+
AND tc.table_schema = kcu.table_schema
|
|
80
|
+
WHERE tc.constraint_type = 'UNIQUE'
|
|
81
|
+
AND tc.table_schema = $1
|
|
82
|
+
`;
|
|
83
|
+
const SQL_INDEXES = `
|
|
84
|
+
SELECT tablename, indexname, indexdef
|
|
85
|
+
FROM pg_indexes
|
|
86
|
+
WHERE schemaname = $1
|
|
87
|
+
`;
|
|
88
|
+
const SQL_ENUMS = `
|
|
89
|
+
SELECT t.typname, e.enumlabel
|
|
90
|
+
FROM pg_type t
|
|
91
|
+
JOIN pg_enum e ON t.oid = e.enumtypid
|
|
92
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
|
|
93
|
+
WHERE n.nspname = $1
|
|
94
|
+
ORDER BY t.typname, e.enumsortorder
|
|
95
|
+
`;
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Main introspection function
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
async function introspect(options) {
|
|
100
|
+
const schema = options.schema ?? 'public';
|
|
101
|
+
const pool = new pg_1.default.Pool({
|
|
102
|
+
connectionString: options.connectionString,
|
|
103
|
+
max: 1,
|
|
104
|
+
connectionTimeoutMillis: 10_000,
|
|
105
|
+
});
|
|
106
|
+
try {
|
|
107
|
+
// Run all information_schema queries in parallel
|
|
108
|
+
const [tablesResult, columnsResult, pkResult, fkResult, uniqueResult, indexResult, enumResult,] = await Promise.all([
|
|
109
|
+
pool.query(SQL_TABLES, [schema]),
|
|
110
|
+
pool.query(SQL_COLUMNS, [schema]),
|
|
111
|
+
pool.query(SQL_PRIMARY_KEYS, [schema]),
|
|
112
|
+
pool.query(SQL_FOREIGN_KEYS, [schema]),
|
|
113
|
+
pool.query(SQL_UNIQUE_CONSTRAINTS, [schema]),
|
|
114
|
+
pool.query(SQL_INDEXES, [schema]),
|
|
115
|
+
pool.query(SQL_ENUMS, [schema]),
|
|
116
|
+
]);
|
|
117
|
+
// Filter tables by include/exclude
|
|
118
|
+
let tableNames = tablesResult.rows.map((r) => r.table_name);
|
|
119
|
+
if (options.include?.length) {
|
|
120
|
+
const includeSet = new Set(options.include);
|
|
121
|
+
tableNames = tableNames.filter((t) => includeSet.has(t));
|
|
122
|
+
}
|
|
123
|
+
if (options.exclude?.length) {
|
|
124
|
+
const excludeSet = new Set(options.exclude);
|
|
125
|
+
tableNames = tableNames.filter((t) => !excludeSet.has(t));
|
|
126
|
+
}
|
|
127
|
+
const tableSet = new Set(tableNames);
|
|
128
|
+
// ----- Group columns by table -----
|
|
129
|
+
const columnsByTable = new Map();
|
|
130
|
+
for (const row of columnsResult.rows) {
|
|
131
|
+
const tableName = row.table_name;
|
|
132
|
+
if (!tableSet.has(tableName))
|
|
133
|
+
continue;
|
|
134
|
+
const isNullable = row.is_nullable === 'YES';
|
|
135
|
+
const isArray = row.data_type === 'ARRAY';
|
|
136
|
+
const baseType = isArray ? row.udt_name.slice(1) : row.udt_name;
|
|
137
|
+
const col = {
|
|
138
|
+
name: row.column_name,
|
|
139
|
+
field: (0, schema_js_1.snakeToCamel)(row.column_name),
|
|
140
|
+
pgType: row.udt_name,
|
|
141
|
+
tsType: (0, schema_js_1.pgTypeToTs)(isArray ? row.udt_name : baseType, isNullable),
|
|
142
|
+
nullable: isNullable,
|
|
143
|
+
hasDefault: row.column_default !== null,
|
|
144
|
+
isArray,
|
|
145
|
+
pgArrayType: (0, schema_js_1.pgArrayType)(baseType),
|
|
146
|
+
maxLength: row.character_maximum_length ?? undefined,
|
|
147
|
+
};
|
|
148
|
+
if (!columnsByTable.has(tableName))
|
|
149
|
+
columnsByTable.set(tableName, []);
|
|
150
|
+
columnsByTable.get(tableName).push(col);
|
|
151
|
+
}
|
|
152
|
+
// ----- Group primary keys by table -----
|
|
153
|
+
const pkByTable = new Map();
|
|
154
|
+
for (const row of pkResult.rows) {
|
|
155
|
+
if (!tableSet.has(row.table_name))
|
|
156
|
+
continue;
|
|
157
|
+
if (!pkByTable.has(row.table_name))
|
|
158
|
+
pkByTable.set(row.table_name, []);
|
|
159
|
+
pkByTable.get(row.table_name).push(row.column_name);
|
|
160
|
+
}
|
|
161
|
+
// ----- Group unique constraints by table -----
|
|
162
|
+
const uniqueByTable = new Map();
|
|
163
|
+
for (const row of uniqueResult.rows) {
|
|
164
|
+
if (!tableSet.has(row.table_name))
|
|
165
|
+
continue;
|
|
166
|
+
if (!uniqueByTable.has(row.table_name))
|
|
167
|
+
uniqueByTable.set(row.table_name, []);
|
|
168
|
+
// Each unique constraint may be multi-column; for simplicity, treat as single-col here
|
|
169
|
+
uniqueByTable.get(row.table_name).push([row.column_name]);
|
|
170
|
+
}
|
|
171
|
+
// ----- Group indexes by table -----
|
|
172
|
+
const indexesByTable = new Map();
|
|
173
|
+
for (const row of indexResult.rows) {
|
|
174
|
+
if (!tableSet.has(row.tablename))
|
|
175
|
+
continue;
|
|
176
|
+
if (!indexesByTable.has(row.tablename))
|
|
177
|
+
indexesByTable.set(row.tablename, []);
|
|
178
|
+
const isUnique = row.indexdef.includes('UNIQUE');
|
|
179
|
+
// Extract column names from indexdef (e.g. "CREATE INDEX idx ON tbl USING btree (col1, col2)")
|
|
180
|
+
const colMatch = row.indexdef.match(/\((.+)\)/);
|
|
181
|
+
const columns = colMatch
|
|
182
|
+
? colMatch[1].split(',').map((c) => c.trim().replace(/ (ASC|DESC)/i, ''))
|
|
183
|
+
: [];
|
|
184
|
+
indexesByTable.get(row.tablename).push({
|
|
185
|
+
name: row.indexname,
|
|
186
|
+
columns,
|
|
187
|
+
unique: isUnique,
|
|
188
|
+
definition: row.indexdef,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
// ----- Collect enums -----
|
|
192
|
+
const enums = {};
|
|
193
|
+
for (const row of enumResult.rows) {
|
|
194
|
+
if (!enums[row.typname])
|
|
195
|
+
enums[row.typname] = [];
|
|
196
|
+
enums[row.typname].push(row.enumlabel);
|
|
197
|
+
}
|
|
198
|
+
const foreignKeys = [];
|
|
199
|
+
for (const row of fkResult.rows) {
|
|
200
|
+
if (!tableSet.has(row.source_table) || !tableSet.has(row.target_table))
|
|
201
|
+
continue;
|
|
202
|
+
foreignKeys.push({
|
|
203
|
+
sourceTable: row.source_table,
|
|
204
|
+
sourceColumn: row.source_column,
|
|
205
|
+
targetTable: row.target_table,
|
|
206
|
+
targetColumn: row.target_column,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
// ----- Build relations from foreign keys -----
|
|
210
|
+
// Count FKs per (source, target) pair for disambiguation
|
|
211
|
+
const fkCounts = new Map();
|
|
212
|
+
for (const fk of foreignKeys) {
|
|
213
|
+
const key = `${fk.sourceTable}→${fk.targetTable}`;
|
|
214
|
+
fkCounts.set(key, (fkCounts.get(key) ?? 0) + 1);
|
|
215
|
+
}
|
|
216
|
+
const relationsByTable = new Map();
|
|
217
|
+
for (const fk of foreignKeys) {
|
|
218
|
+
const pairKey = `${fk.sourceTable}→${fk.targetTable}`;
|
|
219
|
+
const needsDisambiguation = (fkCounts.get(pairKey) ?? 0) > 1;
|
|
220
|
+
// --- belongsTo on the source (child) table ---
|
|
221
|
+
// e.g. posts.user_id → users.id creates posts.user (belongsTo)
|
|
222
|
+
const belongsToName = needsDisambiguation
|
|
223
|
+
? (0, schema_js_1.snakeToCamel)(fk.sourceColumn.replace(/_id$/, ''))
|
|
224
|
+
: (0, schema_js_1.singularize)((0, schema_js_1.snakeToCamel)(fk.targetTable));
|
|
225
|
+
if (!relationsByTable.has(fk.sourceTable))
|
|
226
|
+
relationsByTable.set(fk.sourceTable, {});
|
|
227
|
+
relationsByTable.get(fk.sourceTable)[belongsToName] = {
|
|
228
|
+
type: 'belongsTo',
|
|
229
|
+
name: belongsToName,
|
|
230
|
+
from: fk.sourceTable,
|
|
231
|
+
to: fk.targetTable,
|
|
232
|
+
foreignKey: fk.sourceColumn,
|
|
233
|
+
referenceKey: fk.targetColumn,
|
|
234
|
+
};
|
|
235
|
+
// --- hasMany on the target (parent) table ---
|
|
236
|
+
// e.g. posts.user_id → users.id creates users.posts (hasMany)
|
|
237
|
+
const hasManyName = needsDisambiguation
|
|
238
|
+
? (0, schema_js_1.snakeToCamel)(`${fk.sourceTable}_by_${fk.sourceColumn.replace(/_id$/, '')}`)
|
|
239
|
+
: (0, schema_js_1.snakeToCamel)(fk.sourceTable);
|
|
240
|
+
if (!relationsByTable.has(fk.targetTable))
|
|
241
|
+
relationsByTable.set(fk.targetTable, {});
|
|
242
|
+
relationsByTable.get(fk.targetTable)[hasManyName] = {
|
|
243
|
+
type: 'hasMany',
|
|
244
|
+
name: hasManyName,
|
|
245
|
+
from: fk.targetTable,
|
|
246
|
+
to: fk.sourceTable,
|
|
247
|
+
foreignKey: fk.sourceColumn,
|
|
248
|
+
referenceKey: fk.targetColumn,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
// ----- Assemble TableMetadata for each table -----
|
|
252
|
+
const tables = {};
|
|
253
|
+
for (const tableName of tableNames) {
|
|
254
|
+
const columns = columnsByTable.get(tableName) ?? [];
|
|
255
|
+
const columnMap = {};
|
|
256
|
+
const reverseColumnMap = {};
|
|
257
|
+
const dateColumns = new Set();
|
|
258
|
+
const pgTypes = {};
|
|
259
|
+
const allColumns = [];
|
|
260
|
+
for (const col of columns) {
|
|
261
|
+
columnMap[col.field] = col.name;
|
|
262
|
+
reverseColumnMap[col.name] = col.field;
|
|
263
|
+
allColumns.push(col.name);
|
|
264
|
+
pgTypes[col.name] = col.pgType;
|
|
265
|
+
const baseType = col.isArray ? col.pgType.slice(1) : col.pgType;
|
|
266
|
+
if ((0, schema_js_1.isDateType)(baseType)) {
|
|
267
|
+
dateColumns.add(col.name);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
tables[tableName] = {
|
|
271
|
+
name: tableName,
|
|
272
|
+
columns,
|
|
273
|
+
columnMap,
|
|
274
|
+
reverseColumnMap,
|
|
275
|
+
dateColumns,
|
|
276
|
+
pgTypes,
|
|
277
|
+
allColumns,
|
|
278
|
+
primaryKey: pkByTable.get(tableName) ?? [],
|
|
279
|
+
uniqueColumns: uniqueByTable.get(tableName) ?? [],
|
|
280
|
+
relations: relationsByTable.get(tableName) ?? {},
|
|
281
|
+
indexes: indexesByTable.get(tableName) ?? [],
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
return { tables, enums };
|
|
285
|
+
}
|
|
286
|
+
finally {
|
|
287
|
+
await pool.end();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|