turbine-orm 0.27.0 → 0.28.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 +17 -13
- package/dist/cjs/cli/config.js +20 -3
- package/dist/cjs/cli/destructive.js +47 -31
- package/dist/cjs/cli/index.js +273 -71
- package/dist/cjs/cli/mcp.js +788 -0
- package/dist/cjs/cli/migrate.js +95 -20
- package/dist/cjs/cli/studio.js +3 -2
- package/dist/cjs/client.js +267 -34
- package/dist/cjs/dialect.js +2 -0
- package/dist/cjs/generate.js +171 -7
- package/dist/cjs/index.js +4 -1
- package/dist/cjs/introspect.js +177 -4
- package/dist/cjs/query/batched-loader.js +148 -0
- package/dist/cjs/query/builder.js +714 -133
- package/dist/cjs/schema-builder.js +59 -4
- package/dist/cjs/schema-sql.js +315 -6
- package/dist/cjs/seed.js +66 -0
- package/dist/cli/config.d.ts +9 -2
- package/dist/cli/config.js +19 -3
- package/dist/cli/destructive.js +47 -31
- package/dist/cli/index.d.ts +52 -1
- package/dist/cli/index.js +272 -74
- package/dist/cli/mcp.d.ts +17 -0
- package/dist/cli/mcp.js +781 -0
- package/dist/cli/migrate.d.ts +37 -0
- package/dist/cli/migrate.js +92 -20
- package/dist/cli/studio.d.ts +3 -2
- package/dist/cli/studio.js +3 -2
- package/dist/client.d.ts +136 -1
- package/dist/client.js +267 -34
- package/dist/dialect.d.ts +17 -0
- package/dist/dialect.js +2 -0
- package/dist/generate.d.ts +17 -0
- package/dist/generate.js +171 -10
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -0
- package/dist/introspect.d.ts +20 -1
- package/dist/introspect.js +175 -4
- package/dist/query/batched-loader.d.ts +29 -2
- package/dist/query/batched-loader.js +148 -1
- package/dist/query/builder.d.ts +156 -8
- package/dist/query/builder.js +715 -134
- package/dist/query/index.d.ts +1 -1
- package/dist/query/types.d.ts +113 -8
- package/dist/schema-builder.d.ts +73 -8
- package/dist/schema-builder.js +59 -4
- package/dist/schema-sql.d.ts +67 -0
- package/dist/schema-sql.js +310 -6
- package/dist/schema.d.ts +53 -0
- package/dist/seed.d.ts +4 -0
- package/dist/seed.js +63 -0
- package/package.json +2 -3
package/dist/cjs/generate.js
CHANGED
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
13
|
exports.generate = generate;
|
|
14
14
|
exports.generateTypes = generateTypes;
|
|
15
|
+
exports.generateZod = generateZod;
|
|
16
|
+
exports.generateMetadata = generateMetadata;
|
|
17
|
+
exports.generateIndex = generateIndex;
|
|
15
18
|
const node_fs_1 = require("node:fs");
|
|
16
19
|
const node_path_1 = require("node:path");
|
|
17
20
|
const schema_js_1 = require("./schema.js");
|
|
@@ -19,6 +22,24 @@ const schema_js_1 = require("./schema.js");
|
|
|
19
22
|
function entityName(tableName) {
|
|
20
23
|
return (0, schema_js_1.snakeToPascal)((0, schema_js_1.singularize)(tableName));
|
|
21
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Resolve the TypeScript type for a column, mapping enum-typed columns to their
|
|
27
|
+
* generated string-literal union (PascalCase enum name) instead of the
|
|
28
|
+
* `unknown` that {@link pgTypeToTs} yields for user-defined types. Falls back to
|
|
29
|
+
* the introspected `col.tsType` for every non-enum column.
|
|
30
|
+
*/
|
|
31
|
+
function columnTsType(col, enums) {
|
|
32
|
+
const dt = col.dialectType ?? col.pgType;
|
|
33
|
+
const isArray = col.isArray || dt.startsWith('_');
|
|
34
|
+
const base = isArray && dt.startsWith('_') ? dt.slice(1) : dt;
|
|
35
|
+
if (Object.hasOwn(enums, base)) {
|
|
36
|
+
let t = (0, schema_js_1.snakeToPascal)(base);
|
|
37
|
+
if (isArray)
|
|
38
|
+
t += '[]';
|
|
39
|
+
return col.nullable ? `${t} | null` : t;
|
|
40
|
+
}
|
|
41
|
+
return col.tsType;
|
|
42
|
+
}
|
|
22
43
|
/** Escape a value for embedding in a single-quoted TypeScript string literal */
|
|
23
44
|
function escSQ(value) {
|
|
24
45
|
return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
@@ -48,6 +69,12 @@ function generate(options) {
|
|
|
48
69
|
const indexContent = generateIndex(options.schema);
|
|
49
70
|
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(outDir, 'index.ts'), indexContent, 'utf-8');
|
|
50
71
|
files.push('index.ts');
|
|
72
|
+
// Generate zod.ts (optional — --zod flag)
|
|
73
|
+
if (options.zod) {
|
|
74
|
+
const zodContent = generateZod(options.schema);
|
|
75
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(outDir, 'zod.ts'), zodContent, 'utf-8');
|
|
76
|
+
files.push('zod.ts');
|
|
77
|
+
}
|
|
51
78
|
return { outDir, files };
|
|
52
79
|
}
|
|
53
80
|
// ---------------------------------------------------------------------------
|
|
@@ -108,7 +135,7 @@ function generateTypes(schema) {
|
|
|
108
135
|
const pkNote = table.primaryKey.includes(col.name) ? ' (primary key)' : '';
|
|
109
136
|
const nullNote = col.nullable ? ' (nullable)' : '';
|
|
110
137
|
lines.push(` /** Column: ${col.name} — ${col.pgType}${pkNote}${nullNote} */`);
|
|
111
|
-
lines.push(` ${col.field}: ${col.
|
|
138
|
+
lines.push(` ${col.field}: ${columnTsType(col, schema.enums)};`);
|
|
112
139
|
}
|
|
113
140
|
lines.push('}');
|
|
114
141
|
lines.push('');
|
|
@@ -118,15 +145,18 @@ function generateTypes(schema) {
|
|
|
118
145
|
lines.push(`/** Input type for creating a row in \`${table.name}\` */`);
|
|
119
146
|
lines.push(`export type ${typeName}Create = {`);
|
|
120
147
|
for (const col of table.columns) {
|
|
148
|
+
// STORED generated columns are computed by the database — never writable.
|
|
149
|
+
if (col.isGeneratedStored)
|
|
150
|
+
continue;
|
|
121
151
|
const isPk = table.primaryKey.includes(col.name);
|
|
122
152
|
const isOptional = col.hasDefault || col.nullable || isPk;
|
|
123
153
|
if (isOptional) {
|
|
124
154
|
const reason = isPk ? 'auto-generated' : col.hasDefault ? 'has default' : 'nullable';
|
|
125
155
|
lines.push(` /** Optional: ${reason} */`);
|
|
126
|
-
lines.push(` ${col.field}?: ${col.
|
|
156
|
+
lines.push(` ${col.field}?: ${columnTsType(col, schema.enums)};`);
|
|
127
157
|
}
|
|
128
158
|
else {
|
|
129
|
-
lines.push(` ${col.field}: ${col.
|
|
159
|
+
lines.push(` ${col.field}: ${columnTsType(col, schema.enums)};`);
|
|
130
160
|
}
|
|
131
161
|
}
|
|
132
162
|
lines.push('};');
|
|
@@ -134,11 +164,11 @@ function generateTypes(schema) {
|
|
|
134
164
|
// --- Update input type (all fields optional except PK) ---
|
|
135
165
|
// Numeric columns additionally accept `UpdateOperatorInput<number>` so
|
|
136
166
|
// users can write `{ viewCount: { increment: 1 } }` without an `as any`.
|
|
137
|
-
const nonPkCols = table.columns.filter((c) => !table.primaryKey.includes(c.name));
|
|
167
|
+
const nonPkCols = table.columns.filter((c) => !table.primaryKey.includes(c.name) && !c.isGeneratedStored);
|
|
138
168
|
lines.push(`/** Input type for updating a row in \`${table.name}\` */`);
|
|
139
169
|
lines.push(`export type ${typeName}Update = {`);
|
|
140
170
|
for (const col of nonPkCols) {
|
|
141
|
-
lines.push(` ${col.field}?: ${updateFieldType(col.
|
|
171
|
+
lines.push(` ${col.field}?: ${updateFieldType(columnTsType(col, schema.enums))};`);
|
|
142
172
|
}
|
|
143
173
|
lines.push('};');
|
|
144
174
|
lines.push('');
|
|
@@ -271,6 +301,119 @@ function generateTypes(schema) {
|
|
|
271
301
|
return lines.join('\n');
|
|
272
302
|
}
|
|
273
303
|
// ---------------------------------------------------------------------------
|
|
304
|
+
// zod.ts generator (H1 — `turbine generate --zod`)
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
/**
|
|
307
|
+
* Map a TypeScript primitive (as produced by {@link pgTypeToTs}) to its Zod
|
|
308
|
+
* expression. `Date` uses `z.coerce.date()` — the generated schemas double as
|
|
309
|
+
* request-body validators where dates arrive as ISO strings, and coercion keeps
|
|
310
|
+
* both a `Date` and a valid date-string acceptable (documented decision).
|
|
311
|
+
*/
|
|
312
|
+
function zodScalar(ts) {
|
|
313
|
+
switch (ts) {
|
|
314
|
+
case 'number':
|
|
315
|
+
return 'z.number()';
|
|
316
|
+
case 'string':
|
|
317
|
+
return 'z.string()';
|
|
318
|
+
case 'boolean':
|
|
319
|
+
return 'z.boolean()';
|
|
320
|
+
case 'Date':
|
|
321
|
+
return 'z.coerce.date()';
|
|
322
|
+
case 'bigint':
|
|
323
|
+
return 'z.bigint()';
|
|
324
|
+
case 'Buffer':
|
|
325
|
+
return 'z.instanceof(Uint8Array)';
|
|
326
|
+
case 'number[]':
|
|
327
|
+
// pgvector — `pgTypeToTs('vector')` yields `number[]`.
|
|
328
|
+
return 'z.array(z.number())';
|
|
329
|
+
default:
|
|
330
|
+
// json/jsonb and any unmapped user-defined type.
|
|
331
|
+
return 'z.unknown()';
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Base Zod expression for a column, resolving enums → `z.enum([...])`, arrays →
|
|
336
|
+
* `.array()`, and vectors → `z.array(z.number())`. Does NOT append
|
|
337
|
+
* `.nullable()` / `.optional()` — callers layer those on per-schema.
|
|
338
|
+
*/
|
|
339
|
+
function zodBaseType(col, enums) {
|
|
340
|
+
const dt = col.dialectType ?? col.pgType;
|
|
341
|
+
const isArray = col.isArray || dt.startsWith('_');
|
|
342
|
+
const base = isArray && dt.startsWith('_') ? dt.slice(1) : dt;
|
|
343
|
+
let expr;
|
|
344
|
+
if (Object.hasOwn(enums, base)) {
|
|
345
|
+
expr = `z.enum([${enums[base].map((l) => `'${escSQ(l)}'`).join(', ')}])`;
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
expr = zodScalar((0, schema_js_1.pgTypeToTs)(base, false));
|
|
349
|
+
}
|
|
350
|
+
if (isArray)
|
|
351
|
+
expr += '.array()';
|
|
352
|
+
return expr;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Generate the contents of `zod.ts`. Emits, per table, `XSchema` (the full
|
|
356
|
+
* row), `XCreateSchema` (PK/defaulted/nullable columns optional, STORED
|
|
357
|
+
* generated columns omitted), and `XUpdateSchema` (PK + STORED generated
|
|
358
|
+
* columns omitted, every remaining column optional). Exported so tests can pin
|
|
359
|
+
* the output without writing files.
|
|
360
|
+
*/
|
|
361
|
+
function generateZod(schema) {
|
|
362
|
+
const lines = [...generatedFileHeader()];
|
|
363
|
+
// `zod` is a USER dependency — this generated file imports it, but the Turbine
|
|
364
|
+
// library runtime never does, so Zod stays out of the package's dep graph.
|
|
365
|
+
lines.push("import { z } from 'zod';");
|
|
366
|
+
lines.push('');
|
|
367
|
+
for (const table of Object.values(schema.tables)) {
|
|
368
|
+
const typeName = entityName(table.name);
|
|
369
|
+
// Full-row schema.
|
|
370
|
+
lines.push(`/** Zod schema for a \`${table.name}\` row */`);
|
|
371
|
+
lines.push(`export const ${typeName}Schema = z.object({`);
|
|
372
|
+
for (const col of table.columns) {
|
|
373
|
+
let expr = zodBaseType(col, schema.enums);
|
|
374
|
+
if (col.nullable)
|
|
375
|
+
expr += '.nullable()';
|
|
376
|
+
lines.push(` ${col.field}: ${expr},`);
|
|
377
|
+
}
|
|
378
|
+
lines.push('});');
|
|
379
|
+
lines.push('');
|
|
380
|
+
// Create schema — STORED generated columns can never be written; PK,
|
|
381
|
+
// defaulted, and nullable columns are optional.
|
|
382
|
+
lines.push(`/** Zod schema for creating a \`${table.name}\` row */`);
|
|
383
|
+
lines.push(`export const ${typeName}CreateSchema = z.object({`);
|
|
384
|
+
for (const col of table.columns) {
|
|
385
|
+
if (col.isGeneratedStored)
|
|
386
|
+
continue;
|
|
387
|
+
const isPk = table.primaryKey.includes(col.name);
|
|
388
|
+
let expr = zodBaseType(col, schema.enums);
|
|
389
|
+
if (col.nullable)
|
|
390
|
+
expr += '.nullable()';
|
|
391
|
+
if (col.hasDefault || col.nullable || isPk)
|
|
392
|
+
expr += '.optional()';
|
|
393
|
+
lines.push(` ${col.field}: ${expr},`);
|
|
394
|
+
}
|
|
395
|
+
lines.push('});');
|
|
396
|
+
lines.push('');
|
|
397
|
+
// Update schema — PK and STORED generated columns omitted; all else optional.
|
|
398
|
+
lines.push(`/** Zod schema for updating a \`${table.name}\` row */`);
|
|
399
|
+
lines.push(`export const ${typeName}UpdateSchema = z.object({`);
|
|
400
|
+
for (const col of table.columns) {
|
|
401
|
+
if (col.isGeneratedStored)
|
|
402
|
+
continue;
|
|
403
|
+
if (table.primaryKey.includes(col.name))
|
|
404
|
+
continue;
|
|
405
|
+
let expr = zodBaseType(col, schema.enums);
|
|
406
|
+
if (col.nullable)
|
|
407
|
+
expr += '.nullable()';
|
|
408
|
+
expr += '.optional()';
|
|
409
|
+
lines.push(` ${col.field}: ${expr},`);
|
|
410
|
+
}
|
|
411
|
+
lines.push('});');
|
|
412
|
+
lines.push('');
|
|
413
|
+
}
|
|
414
|
+
return lines.join('\n');
|
|
415
|
+
}
|
|
416
|
+
// ---------------------------------------------------------------------------
|
|
274
417
|
// metadata.ts generator
|
|
275
418
|
// ---------------------------------------------------------------------------
|
|
276
419
|
function generateMetadata(schema) {
|
|
@@ -352,6 +495,9 @@ function generateMetadata(schema) {
|
|
|
352
495
|
lines.push(` { name: '${escSQ(idx.name)}', columns: [${idx.columns.map((c) => `'${escSQ(c)}'`).join(', ')}], unique: ${idx.unique}, definition: ${JSON.stringify(idx.definition)} },`);
|
|
353
496
|
}
|
|
354
497
|
lines.push(' ],');
|
|
498
|
+
// isView — read-only marker; the runtime write guard reads it.
|
|
499
|
+
if (table.isView)
|
|
500
|
+
lines.push(' isView: true,');
|
|
355
501
|
lines.push(' },');
|
|
356
502
|
}
|
|
357
503
|
lines.push(' },');
|
|
@@ -409,7 +555,7 @@ function generateIndex(schema) {
|
|
|
409
555
|
const hasRelations = Object.keys(table.relations).length > 0;
|
|
410
556
|
const genericArgs = hasRelations ? `${typeName}, ${typeName}Relations` : typeName;
|
|
411
557
|
lines.push(` /** Query interface for the \`${table.name}\` table (transaction-scoped) */`);
|
|
412
|
-
lines.push(` declare readonly ${accessor}:
|
|
558
|
+
lines.push(` declare readonly ${accessor}: ${accessorType(table, genericArgs)};`);
|
|
413
559
|
}
|
|
414
560
|
lines.push('}');
|
|
415
561
|
lines.push('');
|
|
@@ -451,7 +597,7 @@ function generateIndex(schema) {
|
|
|
451
597
|
const hasRelations = Object.keys(table.relations).length > 0;
|
|
452
598
|
const genericArgs = hasRelations ? `${typeName}, ${typeName}Relations` : typeName;
|
|
453
599
|
lines.push(` /** Query interface for the \`${table.name}\` table */`);
|
|
454
|
-
lines.push(` declare readonly ${accessor}:
|
|
600
|
+
lines.push(` declare readonly ${accessor}: ${accessorType(table, genericArgs)};`);
|
|
455
601
|
}
|
|
456
602
|
lines.push('');
|
|
457
603
|
lines.push(' constructor(config?: TurbineConfig) {');
|
|
@@ -495,6 +641,18 @@ function generateIndex(schema) {
|
|
|
495
641
|
// ---------------------------------------------------------------------------
|
|
496
642
|
// Helpers
|
|
497
643
|
// ---------------------------------------------------------------------------
|
|
644
|
+
/**
|
|
645
|
+
* The generated table-accessor type. A view (`isView`) without a primary key
|
|
646
|
+
* cannot be looked up by unique key, so its `findUnique`-family methods are
|
|
647
|
+
* excluded via `Omit`. Everything else is a plain `QueryInterface<…>`.
|
|
648
|
+
*/
|
|
649
|
+
function accessorType(table, genericArgs) {
|
|
650
|
+
const base = `QueryInterface<${genericArgs}>`;
|
|
651
|
+
if (table.isView && table.primaryKey.length === 0) {
|
|
652
|
+
return `Omit<${base}, 'findUnique' | 'findUniqueOrThrow'>`;
|
|
653
|
+
}
|
|
654
|
+
return base;
|
|
655
|
+
}
|
|
498
656
|
function serializeColumn(col) {
|
|
499
657
|
const parts = [
|
|
500
658
|
`name: '${escSQ(col.name)}'`,
|
|
@@ -512,6 +670,12 @@ function serializeColumn(col) {
|
|
|
512
670
|
// output stays byte-identical for the common client-default columns.
|
|
513
671
|
if (col.isGenerated)
|
|
514
672
|
parts.push(`isGenerated: true`);
|
|
673
|
+
// STORED generated columns — the runtime write guard reads isGeneratedStored.
|
|
674
|
+
if (col.isGeneratedStored)
|
|
675
|
+
parts.push(`isGeneratedStored: true`);
|
|
676
|
+
if (col.generationExpression !== undefined) {
|
|
677
|
+
parts.push(`generationExpression: '${escSQ(col.generationExpression)}'`);
|
|
678
|
+
}
|
|
515
679
|
if (col.maxLength !== undefined)
|
|
516
680
|
parts.push(`maxLength: ${col.maxLength}`);
|
|
517
681
|
return `{ ${parts.join(', ')} }`;
|
package/dist/cjs/index.js
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
*/
|
|
36
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
37
|
exports.ColumnBuilder = exports.applyManyToManyRelations = exports.snakeToPascal = exports.snakeToCamel = exports.singularize = exports.pgTypeToTs = exports.pgArrayType = exports.normalizeKeyColumns = exports.isDateType = exports.camelToSnake = exports.validateChannel = exports.QueryInterface = exports.pipelineSupported = exports.executePipeline = exports.hasRelationFields = exports.executeNestedUpdate = exports.executeNestedCreate = exports.introspect = exports.generate = exports.wrapPgError = exports.ValidationError = exports.UnsupportedFeatureError = exports.UniqueConstraintError = exports.TurbineErrorCode = exports.TurbineError = exports.TimeoutError = exports.setErrorMessageMode = exports.SerializationFailureError = exports.RelationError = exports.PipelineError = exports.OptimisticLockError = exports.NotNullViolationError = exports.NotFoundError = exports.MigrationError = exports.getErrorMessageMode = exports.ForeignKeyError = exports.ExclusionConstraintError = exports.DeadlockError = exports.ConnectionError = exports.CircularRelationError = exports.CheckConstraintError = exports.postgresDialect = exports.withRetry = exports.TurbineClient = exports.TransactionClient = exports.yugabytedb = exports.timescale = exports.postgresql = exports.cockroachdb = exports.alloydb = void 0;
|
|
38
|
-
exports.TypedSqlQuery = exports.buildTypedSql = exports.turbineHttp = exports.schemaToSQLString = exports.schemaToSQL = exports.schemaPush = exports.schemaDiff = exports.table = exports.defineSchema = exports.column = void 0;
|
|
38
|
+
exports.TypedSqlQuery = exports.buildTypedSql = exports.turbineHttp = exports.defineSeed = exports.schemaToSQLString = exports.schemaToSQL = exports.schemaPush = exports.schemaDiff = exports.table = exports.defineSchema = exports.column = void 0;
|
|
39
39
|
var index_js_1 = require("./adapters/index.js");
|
|
40
40
|
Object.defineProperty(exports, "alloydb", { enumerable: true, get: function () { return index_js_1.alloydb; } });
|
|
41
41
|
Object.defineProperty(exports, "cockroachdb", { enumerable: true, get: function () { return index_js_1.cockroachdb; } });
|
|
@@ -118,6 +118,9 @@ Object.defineProperty(exports, "schemaDiff", { enumerable: true, get: function (
|
|
|
118
118
|
Object.defineProperty(exports, "schemaPush", { enumerable: true, get: function () { return schema_sql_js_1.schemaPush; } });
|
|
119
119
|
Object.defineProperty(exports, "schemaToSQL", { enumerable: true, get: function () { return schema_sql_js_1.schemaToSQL; } });
|
|
120
120
|
Object.defineProperty(exports, "schemaToSQLString", { enumerable: true, get: function () { return schema_sql_js_1.schemaToSQLString; } });
|
|
121
|
+
// Seed helper
|
|
122
|
+
var seed_js_1 = require("./seed.js");
|
|
123
|
+
Object.defineProperty(exports, "defineSeed", { enumerable: true, get: function () { return seed_js_1.defineSeed; } });
|
|
121
124
|
// Serverless / edge factory
|
|
122
125
|
var serverless_js_1 = require("./serverless.js");
|
|
123
126
|
Object.defineProperty(exports, "turbineHttp", { enumerable: true, get: function () { return serverless_js_1.turbineHttp; } });
|
package/dist/cjs/introspect.js
CHANGED
|
@@ -12,11 +12,32 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
12
12
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
13
|
};
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.pgConfActionToReferential = pgConfActionToReferential;
|
|
15
16
|
exports.introspect = introspect;
|
|
16
17
|
exports.introspectPostgresCatalog = introspectPostgresCatalog;
|
|
18
|
+
exports.stripCheckWrapper = stripCheckWrapper;
|
|
17
19
|
const pg_1 = __importDefault(require("pg"));
|
|
18
20
|
const dialect_js_1 = require("./dialect.js");
|
|
19
21
|
const schema_js_1 = require("./schema.js");
|
|
22
|
+
/**
|
|
23
|
+
* Map a `pg_constraint.confdeltype` / `confupdtype` character to a
|
|
24
|
+
* {@link ReferentialAction}. Postgres encodes: `a` = NO ACTION, `r` = RESTRICT,
|
|
25
|
+
* `c` = CASCADE, `n` = SET NULL, `d` = SET DEFAULT.
|
|
26
|
+
*/
|
|
27
|
+
function pgConfActionToReferential(ch) {
|
|
28
|
+
switch (ch) {
|
|
29
|
+
case 'c':
|
|
30
|
+
return 'cascade';
|
|
31
|
+
case 'r':
|
|
32
|
+
return 'restrict';
|
|
33
|
+
case 'n':
|
|
34
|
+
return 'set null';
|
|
35
|
+
case 'd':
|
|
36
|
+
return 'set default';
|
|
37
|
+
default:
|
|
38
|
+
return 'no action';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
20
41
|
// ---------------------------------------------------------------------------
|
|
21
42
|
// SQL queries (all parameterized, no interpolation)
|
|
22
43
|
// ---------------------------------------------------------------------------
|
|
@@ -36,6 +57,8 @@ const SQL_COLUMNS = `
|
|
|
36
57
|
is_nullable,
|
|
37
58
|
column_default,
|
|
38
59
|
is_identity,
|
|
60
|
+
is_generated,
|
|
61
|
+
generation_expression,
|
|
39
62
|
ordinal_position,
|
|
40
63
|
character_maximum_length
|
|
41
64
|
FROM information_schema.columns
|
|
@@ -91,6 +114,65 @@ const SQL_INDEXES = `
|
|
|
91
114
|
FROM pg_indexes
|
|
92
115
|
WHERE schemaname = $1
|
|
93
116
|
`;
|
|
117
|
+
// Foreign-key referential actions (ON DELETE / ON UPDATE) live in pg_catalog,
|
|
118
|
+
// not information_schema. Keyed by constraint name for join with SQL_FOREIGN_KEYS.
|
|
119
|
+
const SQL_FK_ACTIONS = `
|
|
120
|
+
SELECT con.conname, con.confdeltype, con.confupdtype
|
|
121
|
+
FROM pg_constraint con
|
|
122
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = con.connamespace
|
|
123
|
+
WHERE con.contype = 'f'
|
|
124
|
+
AND n.nspname = $1
|
|
125
|
+
`;
|
|
126
|
+
// CHECK constraints (contype = 'c'). NOT NULL is stored as attnotnull, not a
|
|
127
|
+
// check constraint, so it never appears here.
|
|
128
|
+
const SQL_CHECKS = `
|
|
129
|
+
SELECT rel.relname AS table_name, con.conname, pg_get_constraintdef(con.oid) AS definition
|
|
130
|
+
FROM pg_constraint con
|
|
131
|
+
JOIN pg_catalog.pg_class rel ON rel.oid = con.conrelid
|
|
132
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = con.connamespace
|
|
133
|
+
WHERE con.contype = 'c'
|
|
134
|
+
AND n.nspname = $1
|
|
135
|
+
`;
|
|
136
|
+
// Views (relkind 'v') — column metadata comes free from information_schema.columns.
|
|
137
|
+
const SQL_VIEWS = `
|
|
138
|
+
SELECT table_name
|
|
139
|
+
FROM information_schema.views
|
|
140
|
+
WHERE table_schema = $1
|
|
141
|
+
ORDER BY table_name
|
|
142
|
+
`;
|
|
143
|
+
// Materialized views (relkind 'm') — NOT in information_schema; read from pg_catalog.
|
|
144
|
+
const SQL_MATVIEWS = `
|
|
145
|
+
SELECT matviewname AS table_name
|
|
146
|
+
FROM pg_matviews
|
|
147
|
+
WHERE schemaname = $1
|
|
148
|
+
ORDER BY matviewname
|
|
149
|
+
`;
|
|
150
|
+
// Materialized-view columns — information_schema.columns omits matviews, so pull
|
|
151
|
+
// them from pg_attribute. Aliased to mirror SQL_COLUMNS so the same row-mapping
|
|
152
|
+
// applies (array types surface as data_type 'ARRAY' + a '_'-prefixed udt_name).
|
|
153
|
+
const SQL_MATVIEW_COLUMNS = `
|
|
154
|
+
SELECT
|
|
155
|
+
c.relname AS table_name,
|
|
156
|
+
a.attname AS column_name,
|
|
157
|
+
t.typname AS udt_name,
|
|
158
|
+
CASE WHEN t.typcategory = 'A' THEN 'ARRAY' ELSE 'base' END AS data_type,
|
|
159
|
+
CASE WHEN a.attnotnull THEN 'NO' ELSE 'YES' END AS is_nullable,
|
|
160
|
+
NULL AS column_default,
|
|
161
|
+
'NO' AS is_identity,
|
|
162
|
+
'NEVER' AS is_generated,
|
|
163
|
+
NULL AS generation_expression,
|
|
164
|
+
a.attnum AS ordinal_position,
|
|
165
|
+
NULL::int AS character_maximum_length
|
|
166
|
+
FROM pg_catalog.pg_attribute a
|
|
167
|
+
JOIN pg_catalog.pg_class c ON c.oid = a.attrelid
|
|
168
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
169
|
+
JOIN pg_catalog.pg_type t ON t.oid = a.atttypid
|
|
170
|
+
WHERE n.nspname = $1
|
|
171
|
+
AND c.relkind = 'm'
|
|
172
|
+
AND a.attnum > 0
|
|
173
|
+
AND NOT a.attisdropped
|
|
174
|
+
ORDER BY c.relname, a.attnum
|
|
175
|
+
`;
|
|
94
176
|
const SQL_ENUMS = `
|
|
95
177
|
SELECT t.typname, e.enumlabel
|
|
96
178
|
FROM pg_type t
|
|
@@ -131,17 +213,45 @@ async function introspectPostgresCatalog(options) {
|
|
|
131
213
|
});
|
|
132
214
|
try {
|
|
133
215
|
// Run all information_schema queries in parallel
|
|
134
|
-
const [tablesResult, columnsResult, pkResult, fkResult, uniqueResult, indexResult, enumResult] = await Promise.all([
|
|
216
|
+
const [tablesResult, columnsResult, pkResult, fkResult, fkActionsResult, uniqueResult, indexResult, checkResult, enumResult,] = await Promise.all([
|
|
135
217
|
pool.query(SQL_TABLES, [schema]),
|
|
136
218
|
pool.query(SQL_COLUMNS, [schema]),
|
|
137
219
|
pool.query(SQL_PRIMARY_KEYS, [schema]),
|
|
138
220
|
pool.query(SQL_FOREIGN_KEYS, [schema]),
|
|
221
|
+
pool.query(SQL_FK_ACTIONS, [schema]),
|
|
139
222
|
pool.query(SQL_UNIQUE_CONSTRAINTS, [schema]),
|
|
140
223
|
pool.query(SQL_INDEXES, [schema]),
|
|
224
|
+
pool.query(SQL_CHECKS, [schema]),
|
|
141
225
|
pool.query(SQL_ENUMS, [schema]),
|
|
142
226
|
]);
|
|
143
|
-
//
|
|
144
|
-
|
|
227
|
+
// Views + materialized views (opt-in). Regular-view columns are already in
|
|
228
|
+
// columnsResult (information_schema.columns); matview columns need a separate
|
|
229
|
+
// pg_catalog read, which we splice into the column rows below.
|
|
230
|
+
const viewNameSet = new Set();
|
|
231
|
+
const matviewColumnRows = [];
|
|
232
|
+
if (options.includeViews) {
|
|
233
|
+
const [viewsResult, matviewsResult, matviewColsResult] = await Promise.all([
|
|
234
|
+
pool.query(SQL_VIEWS, [schema]),
|
|
235
|
+
pool.query(SQL_MATVIEWS, [schema]),
|
|
236
|
+
pool.query(SQL_MATVIEW_COLUMNS, [schema]),
|
|
237
|
+
]);
|
|
238
|
+
for (const r of viewsResult.rows)
|
|
239
|
+
viewNameSet.add(r.table_name);
|
|
240
|
+
for (const r of matviewsResult.rows)
|
|
241
|
+
viewNameSet.add(r.table_name);
|
|
242
|
+
matviewColumnRows.push(...matviewColsResult.rows);
|
|
243
|
+
}
|
|
244
|
+
// constraint_name → { onDelete, onUpdate } referential actions.
|
|
245
|
+
const fkActions = new Map();
|
|
246
|
+
for (const row of fkActionsResult.rows) {
|
|
247
|
+
fkActions.set(row.conname, {
|
|
248
|
+
onDelete: pgConfActionToReferential(row.confdeltype),
|
|
249
|
+
onUpdate: pgConfActionToReferential(row.confupdtype),
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
// Filter tables by include/exclude. Views/matviews join the base tables as
|
|
253
|
+
// candidates so include/exclude apply uniformly.
|
|
254
|
+
let tableNames = [...tablesResult.rows.map((r) => r.table_name), ...viewNameSet];
|
|
145
255
|
if (options.include?.length) {
|
|
146
256
|
const includeSet = new Set(options.include);
|
|
147
257
|
tableNames = tableNames.filter((t) => includeSet.has(t));
|
|
@@ -152,8 +262,10 @@ async function introspectPostgresCatalog(options) {
|
|
|
152
262
|
}
|
|
153
263
|
const tableSet = new Set(tableNames);
|
|
154
264
|
// ----- Group columns by table -----
|
|
265
|
+
// Base-table + regular-view columns come from information_schema.columns;
|
|
266
|
+
// materialized-view columns are appended from the pg_catalog read.
|
|
155
267
|
const columnsByTable = new Map();
|
|
156
|
-
for (const row of columnsResult.rows) {
|
|
268
|
+
for (const row of [...columnsResult.rows, ...matviewColumnRows]) {
|
|
157
269
|
const tableName = row.table_name;
|
|
158
270
|
if (!tableSet.has(tableName))
|
|
159
271
|
continue;
|
|
@@ -176,6 +288,11 @@ async function introspectPostgresCatalog(options) {
|
|
|
176
288
|
// (gen_random_uuid(), now()), which Turbine must still synthesize.
|
|
177
289
|
isGenerated: (typeof row.column_default === 'string' && row.column_default.includes('nextval(')) ||
|
|
178
290
|
row.is_identity === 'YES',
|
|
291
|
+
// GENERATED ALWAYS AS (expr) STORED — computed by the database, never
|
|
292
|
+
// writable. Distinct from isGenerated (serial/identity, which a client
|
|
293
|
+
// MAY override). is_generated is 'ALWAYS' for STORED columns, else 'NEVER'.
|
|
294
|
+
isGeneratedStored: row.is_generated === 'ALWAYS',
|
|
295
|
+
generationExpression: row.is_generated === 'ALWAYS' && row.generation_expression ? row.generation_expression : undefined,
|
|
179
296
|
isArray,
|
|
180
297
|
arrayType,
|
|
181
298
|
pgArrayType: arrayType,
|
|
@@ -230,6 +347,20 @@ async function introspectPostgresCatalog(options) {
|
|
|
230
347
|
definition: row.indexdef,
|
|
231
348
|
});
|
|
232
349
|
}
|
|
350
|
+
// ----- Group check constraints by table -----
|
|
351
|
+
// pg_get_constraintdef yields e.g. `CHECK ((price >= 0))`; strip the leading
|
|
352
|
+
// `CHECK ` and the outermost paren pair to recover the raw expression.
|
|
353
|
+
const checksByTable = new Map();
|
|
354
|
+
for (const row of checkResult.rows) {
|
|
355
|
+
if (!tableSet.has(row.table_name))
|
|
356
|
+
continue;
|
|
357
|
+
if (!checksByTable.has(row.table_name))
|
|
358
|
+
checksByTable.set(row.table_name, []);
|
|
359
|
+
checksByTable.get(row.table_name).push({
|
|
360
|
+
name: row.conname,
|
|
361
|
+
expression: stripCheckWrapper(row.definition),
|
|
362
|
+
});
|
|
363
|
+
}
|
|
233
364
|
// ----- Collect enums -----
|
|
234
365
|
const enums = {};
|
|
235
366
|
for (const row of enumResult.rows) {
|
|
@@ -279,6 +410,13 @@ async function introspectPostgresCatalog(options) {
|
|
|
279
410
|
? (0, schema_js_1.snakeToCamel)(fk.sourceColumns[0].replace(/_id$/, ''))
|
|
280
411
|
: (0, schema_js_1.snakeToCamel)(fk.constraintName.replace(/^fk_/, '').replace(/_fkey$/, ''))
|
|
281
412
|
: (0, schema_js_1.singularize)((0, schema_js_1.snakeToCamel)(fk.targetTable));
|
|
413
|
+
// Referential actions (omit the 'no action' default to keep metadata lean).
|
|
414
|
+
const actions = fkActions.get(fk.constraintName);
|
|
415
|
+
const actionFields = {};
|
|
416
|
+
if (actions?.onDelete && actions.onDelete !== 'no action')
|
|
417
|
+
actionFields.onDelete = actions.onDelete;
|
|
418
|
+
if (actions?.onUpdate && actions.onUpdate !== 'no action')
|
|
419
|
+
actionFields.onUpdate = actions.onUpdate;
|
|
282
420
|
if (!relationsByTable.has(fk.sourceTable))
|
|
283
421
|
relationsByTable.set(fk.sourceTable, {});
|
|
284
422
|
relationsByTable.get(fk.sourceTable)[belongsToName] = {
|
|
@@ -288,6 +426,7 @@ async function introspectPostgresCatalog(options) {
|
|
|
288
426
|
to: fk.targetTable,
|
|
289
427
|
foreignKey,
|
|
290
428
|
referenceKey,
|
|
429
|
+
...actionFields,
|
|
291
430
|
};
|
|
292
431
|
// --- hasMany on the target (parent) table ---
|
|
293
432
|
// e.g. posts.user_id → users.id creates users.posts (hasMany)
|
|
@@ -305,6 +444,7 @@ async function introspectPostgresCatalog(options) {
|
|
|
305
444
|
to: fk.sourceTable,
|
|
306
445
|
foreignKey,
|
|
307
446
|
referenceKey,
|
|
447
|
+
...actionFields,
|
|
308
448
|
};
|
|
309
449
|
}
|
|
310
450
|
// ----- Conservative many-to-many auto-detection (PURELY ADDITIVE) -----
|
|
@@ -422,6 +562,8 @@ async function introspectPostgresCatalog(options) {
|
|
|
422
562
|
uniqueColumns: uniqueByTable.get(tableName) ?? [],
|
|
423
563
|
relations: relationsByTable.get(tableName) ?? {},
|
|
424
564
|
indexes: indexesByTable.get(tableName) ?? [],
|
|
565
|
+
checks: checksByTable.get(tableName) ?? [],
|
|
566
|
+
...(viewNameSet.has(tableName) ? { isView: true } : {}),
|
|
425
567
|
};
|
|
426
568
|
}
|
|
427
569
|
return { tables, enums };
|
|
@@ -430,3 +572,34 @@ async function introspectPostgresCatalog(options) {
|
|
|
430
572
|
await pool.end();
|
|
431
573
|
}
|
|
432
574
|
}
|
|
575
|
+
/**
|
|
576
|
+
* Recover the raw check expression from `pg_get_constraintdef` output, which
|
|
577
|
+
* wraps it as `CHECK ((expr))`. Strips the leading `CHECK ` keyword and one
|
|
578
|
+
* balanced outer paren pair; leaves anything unexpected untouched.
|
|
579
|
+
*/
|
|
580
|
+
function stripCheckWrapper(def) {
|
|
581
|
+
let s = def.trim();
|
|
582
|
+
const m = /^CHECK\s*\((.*)\)$/is.exec(s);
|
|
583
|
+
if (m)
|
|
584
|
+
s = m[1].trim();
|
|
585
|
+
// pg double-wraps single expressions: `(price >= 0)` → unwrap one more pair
|
|
586
|
+
// only when the parens are balanced across the whole string.
|
|
587
|
+
if (s.startsWith('(') && s.endsWith(')')) {
|
|
588
|
+
let depth = 0;
|
|
589
|
+
let balanced = true;
|
|
590
|
+
for (let i = 0; i < s.length; i++) {
|
|
591
|
+
if (s[i] === '(')
|
|
592
|
+
depth++;
|
|
593
|
+
else if (s[i] === ')') {
|
|
594
|
+
depth--;
|
|
595
|
+
if (depth === 0 && i < s.length - 1) {
|
|
596
|
+
balanced = false;
|
|
597
|
+
break;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
if (balanced)
|
|
602
|
+
s = s.slice(1, -1).trim();
|
|
603
|
+
}
|
|
604
|
+
return s;
|
|
605
|
+
}
|