turbine-orm 0.29.0 → 0.30.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 +1 -1
- package/dist/cjs/cli/index.js +5 -0
- package/dist/cjs/cli/mcp.js +22 -92
- package/dist/cjs/client.js +33 -3
- package/dist/cjs/generate.js +71 -25
- package/dist/cjs/index.js +4 -1
- package/dist/cjs/introspect.js +350 -120
- package/dist/cjs/mssql.js +18 -133
- package/dist/cjs/mysql.js +16 -129
- package/dist/cjs/optional-peer-import.cjs +122 -0
- package/dist/cjs/powdb.js +424 -81
- package/dist/cjs/powql.js +49 -25
- package/dist/cjs/query/builder.js +290 -23
- package/dist/cjs/query/filters.js +32 -1
- package/dist/cjs/schema-metadata.js +316 -0
- package/dist/cjs/sqlite.js +8 -89
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +5 -0
- package/dist/cli/mcp.d.ts +18 -0
- package/dist/cli/mcp.js +22 -93
- package/dist/client.d.ts +6 -2
- package/dist/client.js +33 -3
- package/dist/generate.d.ts +16 -4
- package/dist/generate.js +71 -25
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/introspect.d.ts +94 -1
- package/dist/introspect.js +345 -120
- package/dist/mssql.js +16 -101
- package/dist/mysql.js +14 -97
- package/dist/optional-peer-import.cjs +89 -0
- package/dist/optional-peer-import.d.cts +53 -0
- package/dist/powdb.d.ts +87 -25
- package/dist/powdb.js +419 -80
- package/dist/powql.d.ts +6 -0
- package/dist/powql.js +51 -27
- package/dist/query/builder.d.ts +60 -3
- package/dist/query/builder.js +291 -24
- package/dist/query/deferred.d.ts +7 -2
- package/dist/query/filters.d.ts +18 -0
- package/dist/query/filters.js +30 -0
- package/dist/query/types.d.ts +19 -0
- package/dist/schema-metadata.d.ts +77 -0
- package/dist/schema-metadata.js +313 -0
- package/dist/schema.d.ts +10 -0
- package/dist/sqlite.js +9 -90
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -882,7 +882,7 @@ Everything is honest about what ports and what doesn't. Features marked **PG-onl
|
|
|
882
882
|
|
|
883
883
|
**Engine notes:** SQLite uses `RETURNING` (≥ 3.35) just like Postgres. MySQL has no `RETURNING`, so writes re-`SELECT` the affected row and **`createMany` returns `[]`** (the rows ARE inserted — re-query if you need them). SQL Server returns rows via `OUTPUT`/`MERGE`; `DISTINCT ON` is Postgres-only. Only Postgres streams via a true cursor (constant memory); the other engines' `findManyStream` materializes the result then yields it in batches. Optimistic locking throws `OptimisticLockError` on all engines (on MySQL the conflict is detected from the version-checked UPDATE's affected-row count). The `turbine` CLI (`generate`, `migrate`) is currently PostgreSQL-only — point the engine factories at a hand-written or programmatically introspected `SCHEMA`.
|
|
884
884
|
|
|
885
|
-
**PowDB** speaks its own non-SQL query language (PowQL), so it sits outside the SQL matrix above. Writes use a trailing **`returning`** keyword (upsert reselects by PK). PKs are server-assigned `auto` ints **or** client UUIDs. Nested relations load client-side (N+1, including many-to-many via the junction — no `json_agg`). Nested writes cover hasMany/hasOne/belongsTo; many-to-many nested writes are not supported. Transactions are single-writer (
|
|
885
|
+
**PowDB** speaks its own non-SQL query language (PowQL), so it sits outside the SQL matrix above. Writes use a trailing **`returning`** keyword (upsert reselects by PK). PKs are server-assigned `auto` ints **or** client UUIDs. Nested relations load client-side (N+1, including many-to-many via the junction — no `json_agg`). Nested writes cover hasMany/hasOne/belongsTo; many-to-many nested writes are not supported. Transactions are single-writer: concurrent `$transaction` calls queue FIFO (bounded by `transactionQueueTimeoutMs`); nested/re-entrant transactions throw typed errors (no savepoints). Schema is code-first via `defineSchema` (no wire introspection) — `schemaDefToMetadata()` bridges it to any engine that needs runtime metadata. Embedded `syncMode: 'normal'` moves fsync off the commit path; the networked transport runs the same data over a socket. Cursor streaming and the Postgres-only trio (pgvector / LISTEN/NOTIFY / RLS session GUCs) throw `UnsupportedFeatureError`. Full details: **[turbineorm.dev/engines#powdb](https://turbineorm.dev/engines#powdb)**.
|
|
886
886
|
|
|
887
887
|
Full setup, signatures, and the complete support matrix: **[turbineorm.dev/engines](https://turbineorm.dev/engines)**.
|
|
888
888
|
|
package/dist/cjs/cli/index.js
CHANGED
|
@@ -141,6 +141,9 @@ function parseArgs(argv = process.argv.slice(2)) {
|
|
|
141
141
|
case '--include-views':
|
|
142
142
|
result.includeViews = true;
|
|
143
143
|
break;
|
|
144
|
+
case '--no-timestamp':
|
|
145
|
+
result.noTimestamp = true;
|
|
146
|
+
break;
|
|
144
147
|
case '--allow-destructive':
|
|
145
148
|
result.allowDestructive = true;
|
|
146
149
|
break;
|
|
@@ -557,6 +560,7 @@ async function cmdGenerate(args, config) {
|
|
|
557
560
|
outDir: config.out,
|
|
558
561
|
connectionString: url,
|
|
559
562
|
zod: args.zod,
|
|
563
|
+
noTimestamp: args.noTimestamp,
|
|
560
564
|
});
|
|
561
565
|
genSpinner.succeed(`Generated ${(0, ui_js_1.bold)(String(result.files.length))} files in ${(0, ui_js_1.elapsed)(startTime)}`);
|
|
562
566
|
// List files
|
|
@@ -1503,6 +1507,7 @@ function showGenerateHelp() {
|
|
|
1503
1507
|
console.log(` ${(0, ui_js_1.cyan)('--exclude')} ${(0, ui_js_1.dim)('<tables>')} Comma-separated tables to exclude`);
|
|
1504
1508
|
console.log(` ${(0, ui_js_1.cyan)('--zod')} Also emit ${(0, ui_js_1.cyan)('zod.ts')} validation schemas ${(0, ui_js_1.dim)('(needs the zod dep)')}`);
|
|
1505
1509
|
console.log(` ${(0, ui_js_1.cyan)('--include-views')} Include views + materialized views as read-only entities`);
|
|
1510
|
+
console.log(` ${(0, ui_js_1.cyan)('--no-timestamp')} Omit the ${(0, ui_js_1.dim)('Generated at:')} header line ${(0, ui_js_1.dim)('(reproducible, diff-stable output)')}`);
|
|
1506
1511
|
console.log(` ${(0, ui_js_1.cyan)('--allow-empty')} Generate even when introspection matches 0 tables`);
|
|
1507
1512
|
(0, ui_js_1.newline)();
|
|
1508
1513
|
}
|
package/dist/cjs/cli/mcp.js
CHANGED
|
@@ -4,12 +4,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.startMcpServer = startMcpServer;
|
|
7
|
+
exports.buildRelations = buildRelations;
|
|
7
8
|
exports.runMcpServer = runMcpServer;
|
|
8
9
|
const node_crypto_1 = require("node:crypto");
|
|
9
10
|
const node_fs_1 = require("node:fs");
|
|
10
11
|
const node_path_1 = require("node:path");
|
|
11
12
|
const pg_1 = __importDefault(require("pg"));
|
|
12
13
|
const index_advisor_js_1 = require("../index-advisor.js");
|
|
14
|
+
const introspect_js_1 = require("../introspect.js");
|
|
13
15
|
const index_js_1 = require("../query/index.js");
|
|
14
16
|
const schema_js_1 = require("../schema.js");
|
|
15
17
|
const migrate_js_1 = require("./migrate.js");
|
|
@@ -557,7 +559,7 @@ async function loadSchemaMetadata(client, options) {
|
|
|
557
559
|
labels.push(row.enumlabel);
|
|
558
560
|
enums[row.typname] = labels;
|
|
559
561
|
}
|
|
560
|
-
const relationsByTable = buildRelations(tableNames, columnsByTable, pkByTable, fkResult.rows);
|
|
562
|
+
const relationsByTable = buildRelations(tableNames, columnsByTable, pkByTable, fkResult.rows, enums);
|
|
561
563
|
const tables = {};
|
|
562
564
|
for (const tableName of tableNames) {
|
|
563
565
|
const columns = columnsByTable.get(tableName) ?? [];
|
|
@@ -596,7 +598,15 @@ async function loadSchemaMetadata(client, options) {
|
|
|
596
598
|
}
|
|
597
599
|
return { tables, enums };
|
|
598
600
|
}
|
|
599
|
-
|
|
601
|
+
/**
|
|
602
|
+
* Group raw FK rows into constraint-level entries and delegate relation
|
|
603
|
+
* naming to the SHARED introspection builder (`buildRelationsFromForeignKeys`
|
|
604
|
+
* + `addAutoManyToManyRelations` in ../introspect.ts). MCP previously carried
|
|
605
|
+
* a stale copy of a retired naming scheme, so `turbine mcp` and `turbine
|
|
606
|
+
* generate` derived DIFFERENT relation names from the same database (N-3).
|
|
607
|
+
* Exported for the parity unit test.
|
|
608
|
+
*/
|
|
609
|
+
function buildRelations(tableNames, columnsByTable, pkByTable, rows, enums = {}) {
|
|
600
610
|
const tableSet = new Set(tableNames);
|
|
601
611
|
const groups = new Map();
|
|
602
612
|
for (const row of rows) {
|
|
@@ -614,95 +624,18 @@ function buildRelations(tableNames, columnsByTable, pkByTable, rows) {
|
|
|
614
624
|
groups.set(row.constraint_name, group);
|
|
615
625
|
}
|
|
616
626
|
const foreignKeys = [...groups.values()];
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
const referenceKey = oneOrMany(fk.targetColumns);
|
|
628
|
-
const belongsToName = needsDisambiguation
|
|
629
|
-
? fk.sourceColumns.length === 1
|
|
630
|
-
? (0, schema_js_1.snakeToCamel)(fk.sourceColumns[0].replace(/_id$/, ''))
|
|
631
|
-
: (0, schema_js_1.snakeToCamel)(fk.constraintName.replace(/^fk_/, '').replace(/_fkey$/, ''))
|
|
632
|
-
: (0, schema_js_1.singularize)((0, schema_js_1.snakeToCamel)(fk.targetTable));
|
|
633
|
-
const hasManyName = needsDisambiguation
|
|
634
|
-
? fk.sourceColumns.length === 1
|
|
635
|
-
? (0, schema_js_1.snakeToCamel)(`${fk.sourceTable}_by_${fk.sourceColumns[0].replace(/_id$/, '')}`)
|
|
636
|
-
: (0, schema_js_1.snakeToCamel)(`${fk.sourceTable}_by_${fk.constraintName.replace(/^fk_/, '').replace(/_fkey$/, '')}`)
|
|
637
|
-
: (0, schema_js_1.snakeToCamel)(fk.sourceTable);
|
|
638
|
-
const sourceRels = relations.get(fk.sourceTable) ?? {};
|
|
639
|
-
sourceRels[belongsToName] = {
|
|
640
|
-
type: 'belongsTo',
|
|
641
|
-
name: belongsToName,
|
|
642
|
-
from: fk.sourceTable,
|
|
643
|
-
to: fk.targetTable,
|
|
644
|
-
foreignKey,
|
|
645
|
-
referenceKey,
|
|
646
|
-
};
|
|
647
|
-
relations.set(fk.sourceTable, sourceRels);
|
|
648
|
-
const targetRels = relations.get(fk.targetTable) ?? {};
|
|
649
|
-
targetRels[hasManyName] = {
|
|
650
|
-
type: 'hasMany',
|
|
651
|
-
name: hasManyName,
|
|
652
|
-
from: fk.targetTable,
|
|
653
|
-
to: fk.sourceTable,
|
|
654
|
-
foreignKey,
|
|
655
|
-
referenceKey,
|
|
656
|
-
};
|
|
657
|
-
relations.set(fk.targetTable, targetRels);
|
|
658
|
-
}
|
|
659
|
-
addManyToManyRelations(tableNames, columnsByTable, pkByTable, foreignKeys, relations);
|
|
627
|
+
const columnFieldsByTable = new Map();
|
|
628
|
+
const unknownTypedFieldsByTable = new Map();
|
|
629
|
+
for (const [tbl, cols] of columnsByTable) {
|
|
630
|
+
columnFieldsByTable.set(tbl, new Set(cols.map((c) => c.field)));
|
|
631
|
+
// Enum-typed columns also report tsType 'unknown', but the generated type
|
|
632
|
+
// layer gives them a concrete union — only json/jsonb qualify as shadows.
|
|
633
|
+
unknownTypedFieldsByTable.set(tbl, new Set(cols.filter((c) => (0, introspect_js_1.isUnknownTsType)(c.tsType) && !Object.hasOwn(enums, c.pgType)).map((c) => c.field)));
|
|
634
|
+
}
|
|
635
|
+
const relations = (0, introspect_js_1.buildRelationsFromForeignKeys)(foreignKeys, columnFieldsByTable, undefined, unknownTypedFieldsByTable);
|
|
636
|
+
(0, introspect_js_1.addAutoManyToManyRelations)(tableNames, foreignKeys, pkByTable, new Map(Array.from(columnsByTable, ([tbl, cols]) => [tbl, cols.map((c) => c.name)])), relations, columnFieldsByTable, unknownTypedFieldsByTable);
|
|
660
637
|
return relations;
|
|
661
638
|
}
|
|
662
|
-
function addManyToManyRelations(tableNames, columnsByTable, pkByTable, foreignKeys, relations) {
|
|
663
|
-
for (const tableName of tableNames) {
|
|
664
|
-
const pk = pkByTable.get(tableName) ?? [];
|
|
665
|
-
if (pk.length !== 2)
|
|
666
|
-
continue;
|
|
667
|
-
const tableFks = foreignKeys.filter((fk) => fk.sourceTable === tableName);
|
|
668
|
-
if (tableFks.length !== 2 || tableFks.some((fk) => fk.sourceColumns.length !== 1))
|
|
669
|
-
continue;
|
|
670
|
-
const fkCols = tableFks.map((fk) => fk.sourceColumns[0]);
|
|
671
|
-
const pkSet = new Set(pk);
|
|
672
|
-
if (!fkCols.every((column) => pkSet.has(column)) || new Set(fkCols).size !== 2)
|
|
673
|
-
continue;
|
|
674
|
-
const [fkA, fkB] = tableFks;
|
|
675
|
-
if (fkA.targetTable === fkB.targetTable)
|
|
676
|
-
continue;
|
|
677
|
-
const junctionColumns = (columnsByTable.get(tableName) ?? []).map((column) => column.name);
|
|
678
|
-
if (junctionColumns.length !== 2)
|
|
679
|
-
continue;
|
|
680
|
-
addManyToManyDirection(relations, tableName, fkA, fkB);
|
|
681
|
-
addManyToManyDirection(relations, tableName, fkB, fkA);
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
function addManyToManyDirection(relations, junctionTable, self, other) {
|
|
685
|
-
const sourceTable = self.targetTable;
|
|
686
|
-
const targetTable = other.targetTable;
|
|
687
|
-
const relName = (0, schema_js_1.snakeToCamel)(targetTable);
|
|
688
|
-
const tableRelations = relations.get(sourceTable) ?? {};
|
|
689
|
-
if (tableRelations[relName])
|
|
690
|
-
return;
|
|
691
|
-
tableRelations[relName] = {
|
|
692
|
-
type: 'manyToMany',
|
|
693
|
-
name: relName,
|
|
694
|
-
from: sourceTable,
|
|
695
|
-
to: targetTable,
|
|
696
|
-
referenceKey: oneOrMany(self.targetColumns),
|
|
697
|
-
foreignKey: oneOrMany(self.targetColumns),
|
|
698
|
-
through: {
|
|
699
|
-
table: junctionTable,
|
|
700
|
-
sourceKey: self.sourceColumns[0],
|
|
701
|
-
targetKey: other.sourceColumns[0],
|
|
702
|
-
},
|
|
703
|
-
};
|
|
704
|
-
relations.set(sourceTable, tableRelations);
|
|
705
|
-
}
|
|
706
639
|
async function estimateRows(client, schema) {
|
|
707
640
|
const result = await client.query(`SELECT c.relname, c.reltuples::bigint::text AS reltuples
|
|
708
641
|
FROM pg_class c
|
|
@@ -730,9 +663,6 @@ function extractIndexColumns(indexdef) {
|
|
|
730
663
|
.replace(/ (ASC|DESC)$/i, '')
|
|
731
664
|
.replace(/^"|"$/g, ''));
|
|
732
665
|
}
|
|
733
|
-
function oneOrMany(columns) {
|
|
734
|
-
return columns.length === 1 ? columns[0] : columns;
|
|
735
|
-
}
|
|
736
666
|
function optionalLimit(value) {
|
|
737
667
|
if (value === undefined)
|
|
738
668
|
return 50;
|
package/dist/cjs/client.js
CHANGED
|
@@ -771,14 +771,31 @@ class TurbineClient {
|
|
|
771
771
|
*/
|
|
772
772
|
async transaction(fn) {
|
|
773
773
|
const client = await this.pool.connect();
|
|
774
|
+
/**
|
|
775
|
+
* Only true once BEGIN has actually succeeded. If BEGIN itself throws
|
|
776
|
+
* (e.g. a single-writer engine's transaction gate times out or rejects a
|
|
777
|
+
* re-entrant begin), issuing a "best-effort" ROLLBACK would be a stray
|
|
778
|
+
* statement from a context that never opened a transaction — on a driver
|
|
779
|
+
* with one shared engine handle (PowDB embedded) it would roll back a
|
|
780
|
+
* DIFFERENT caller's open transaction.
|
|
781
|
+
*/
|
|
782
|
+
let began = false;
|
|
774
783
|
try {
|
|
775
784
|
await client.query(this.dialect.beginStatement());
|
|
785
|
+
began = true;
|
|
776
786
|
const result = await fn(client);
|
|
777
787
|
await client.query(this.dialect.commitStatement());
|
|
778
788
|
return result;
|
|
779
789
|
}
|
|
780
790
|
catch (err) {
|
|
781
|
-
|
|
791
|
+
if (began) {
|
|
792
|
+
try {
|
|
793
|
+
await client.query(this.dialect.rollbackStatement());
|
|
794
|
+
}
|
|
795
|
+
catch {
|
|
796
|
+
// Best-effort rollback — the connection may have died mid-query.
|
|
797
|
+
}
|
|
798
|
+
}
|
|
782
799
|
throw err;
|
|
783
800
|
}
|
|
784
801
|
finally {
|
|
@@ -812,11 +829,21 @@ class TurbineClient {
|
|
|
812
829
|
}
|
|
813
830
|
};
|
|
814
831
|
let timedOut = false;
|
|
832
|
+
/**
|
|
833
|
+
* Only true once BEGIN has actually succeeded. If BEGIN itself throws —
|
|
834
|
+
* e.g. a single-writer engine's transaction gate times out in its FIFO
|
|
835
|
+
* queue or rejects a re-entrant begin (PowDB, E002/E017) — this context
|
|
836
|
+
* never opened a transaction, so the catch below must NOT issue its
|
|
837
|
+
* best-effort ROLLBACK: on a driver with one shared engine handle that
|
|
838
|
+
* stray ROLLBACK would tear down a DIFFERENT caller's open transaction.
|
|
839
|
+
*/
|
|
840
|
+
let began = false;
|
|
815
841
|
try {
|
|
816
842
|
// BEGIN with optional isolation level — the dialect owns the keyword and
|
|
817
843
|
// BEGIN+isolation composition (Postgres appends ` ISOLATION LEVEL …`).
|
|
818
844
|
const isolationSql = options?.isolationLevel ? ISOLATION_LEVELS[options.isolationLevel] : undefined;
|
|
819
845
|
await client.query(this.dialect.beginStatement(isolationSql));
|
|
846
|
+
began = true;
|
|
820
847
|
// Apply transaction-local session context (RLS / multi-tenant GUCs).
|
|
821
848
|
// Order matters: BEGIN -> isolation level (above) -> set_config loop ->
|
|
822
849
|
// user fn. Any error here propagates to the catch below and rolls back
|
|
@@ -889,8 +916,11 @@ class TurbineClient {
|
|
|
889
916
|
// If the timeout fired we already destroyed the connection — issuing a
|
|
890
917
|
// ROLLBACK on a released client would throw "Client has already been
|
|
891
918
|
// released". Skip the rollback in that case (the backend rolled back
|
|
892
|
-
// when its socket was closed).
|
|
893
|
-
|
|
919
|
+
// when its socket was closed). Likewise skip it when BEGIN never
|
|
920
|
+
// succeeded (`began` false) — there is no transaction to roll back and
|
|
921
|
+
// the stray statement could hit another caller's transaction on a
|
|
922
|
+
// shared-handle engine.
|
|
923
|
+
if (began && !timedOut && !released) {
|
|
894
924
|
try {
|
|
895
925
|
await client.query(this.dialect.rollbackStatement());
|
|
896
926
|
}
|
package/dist/cjs/generate.js
CHANGED
|
@@ -57,21 +57,22 @@ function generate(options) {
|
|
|
57
57
|
}
|
|
58
58
|
(0, node_fs_1.mkdirSync)(outDir, { recursive: true });
|
|
59
59
|
const files = [];
|
|
60
|
+
const fileOptions = { noTimestamp: options.noTimestamp };
|
|
60
61
|
// Generate types.ts
|
|
61
|
-
const typesContent = generateTypes(options.schema);
|
|
62
|
+
const typesContent = generateTypes(options.schema, fileOptions);
|
|
62
63
|
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(outDir, 'types.ts'), typesContent, 'utf-8');
|
|
63
64
|
files.push('types.ts');
|
|
64
65
|
// Generate metadata.ts
|
|
65
|
-
const metadataContent = generateMetadata(options.schema);
|
|
66
|
+
const metadataContent = generateMetadata(options.schema, fileOptions);
|
|
66
67
|
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(outDir, 'metadata.ts'), metadataContent, 'utf-8');
|
|
67
68
|
files.push('metadata.ts');
|
|
68
69
|
// Generate index.ts (configured client)
|
|
69
|
-
const indexContent = generateIndex(options.schema);
|
|
70
|
+
const indexContent = generateIndex(options.schema, fileOptions);
|
|
70
71
|
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(outDir, 'index.ts'), indexContent, 'utf-8');
|
|
71
72
|
files.push('index.ts');
|
|
72
73
|
// Generate zod.ts (optional — --zod flag)
|
|
73
74
|
if (options.zod) {
|
|
74
|
-
const zodContent = generateZod(options.schema);
|
|
75
|
+
const zodContent = generateZod(options.schema, fileOptions);
|
|
75
76
|
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(outDir, 'zod.ts'), zodContent, 'utf-8');
|
|
76
77
|
files.push('zod.ts');
|
|
77
78
|
}
|
|
@@ -80,24 +81,51 @@ function generate(options) {
|
|
|
80
81
|
// ---------------------------------------------------------------------------
|
|
81
82
|
// types.ts generator
|
|
82
83
|
// ---------------------------------------------------------------------------
|
|
83
|
-
function generatedFileHeader() {
|
|
84
|
+
function generatedFileHeader(options) {
|
|
85
|
+
// `noTimestamp` omits the volatile line entirely (T-8b) so regenerating an
|
|
86
|
+
// unchanged schema produces byte-identical files.
|
|
84
87
|
return [
|
|
85
88
|
'/**',
|
|
86
89
|
' * Auto-generated by turbine-orm — DO NOT EDIT',
|
|
87
90
|
' *',
|
|
88
|
-
` * Generated at: ${new Date().toISOString()}
|
|
91
|
+
...(options?.noTimestamp ? [] : [` * Generated at: ${new Date().toISOString()}`]),
|
|
89
92
|
' * @see https://turbineorm.dev',
|
|
90
93
|
' */',
|
|
91
94
|
'',
|
|
92
95
|
];
|
|
93
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* The relations of a table that are safe to surface in the generated TYPE
|
|
99
|
+
* layer. A relation whose name equals a scalar column field would shadow the
|
|
100
|
+
* column: `interface XWithY extends X` becomes TS2430, the `XCreate & { y?: … }`
|
|
101
|
+
* intersection collapses (TS2322), and neither the column nor the relation is
|
|
102
|
+
* targetable. Introspection no longer produces such names (they are
|
|
103
|
+
* disambiguated at the source), but hand-written or legacy metadata may —
|
|
104
|
+
* skip those relations here with a warning instead of emitting broken types.
|
|
105
|
+
* The runtime metadata (metadata.ts) still carries every relation.
|
|
106
|
+
*/
|
|
107
|
+
function typeSafeRelations(table, warn = true) {
|
|
108
|
+
const columnFields = new Set(table.columns.map((c) => c.field));
|
|
109
|
+
const usable = [];
|
|
110
|
+
for (const [relName, rel] of Object.entries(table.relations)) {
|
|
111
|
+
if (columnFields.has(relName)) {
|
|
112
|
+
if (warn) {
|
|
113
|
+
console.warn(`[turbine] Relation "${relName}" on table "${table.name}" shadows a column field of the same name — ` +
|
|
114
|
+
`omitting it from the generated types. Rename the relation (or the column) to expose it.`);
|
|
115
|
+
}
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
usable.push([relName, rel]);
|
|
119
|
+
}
|
|
120
|
+
return usable;
|
|
121
|
+
}
|
|
94
122
|
/**
|
|
95
123
|
* Generate the contents of `types.ts` (entity interfaces, *Create / *Update,
|
|
96
124
|
* and *Relations brand-field interfaces). Exported so tests can pin the
|
|
97
125
|
* generator output without writing files to disk.
|
|
98
126
|
*/
|
|
99
|
-
function generateTypes(schema) {
|
|
100
|
-
const lines = [...generatedFileHeader()];
|
|
127
|
+
function generateTypes(schema, options) {
|
|
128
|
+
const lines = [...generatedFileHeader(options)];
|
|
101
129
|
// We import UpdateOperatorInput so generated *Update types can express
|
|
102
130
|
// atomic increment / decrement / multiply / divide / set operators on
|
|
103
131
|
// numeric columns (TASK-3.4).
|
|
@@ -113,9 +141,15 @@ function generateTypes(schema) {
|
|
|
113
141
|
// `${TargetType}Relations` (for deep inference) or `{}` (the no-relations
|
|
114
142
|
// default) into each `RelationDescriptor`. Built once up-front because
|
|
115
143
|
// relations can point at tables we haven't iterated to yet.
|
|
144
|
+
// Relations that can be surfaced in the type layer, computed once per table
|
|
145
|
+
// (relations that would shadow a scalar column field are excluded + warned).
|
|
146
|
+
const safeRelationsByTable = new Map();
|
|
147
|
+
for (const t of Object.values(schema.tables)) {
|
|
148
|
+
safeRelationsByTable.set(t.name, typeSafeRelations(t));
|
|
149
|
+
}
|
|
116
150
|
const tablesWithRelations = new Set();
|
|
117
151
|
for (const t of Object.values(schema.tables)) {
|
|
118
|
-
if (
|
|
152
|
+
if ((safeRelationsByTable.get(t.name) ?? []).length > 0)
|
|
119
153
|
tablesWithRelations.add(t.name);
|
|
120
154
|
}
|
|
121
155
|
// Generate enum types
|
|
@@ -180,11 +214,12 @@ function generateTypes(schema) {
|
|
|
180
214
|
// any depth — `RelationRelations<R[K]>` reads the third type parameter
|
|
181
215
|
// and threads it into the next recursion step. If the target table has
|
|
182
216
|
// no relations of its own, the descriptor uses `{}` (the default).
|
|
183
|
-
const
|
|
217
|
+
const safeRelations = safeRelationsByTable.get(table.name) ?? [];
|
|
218
|
+
const hasRelations = safeRelations.length > 0;
|
|
184
219
|
if (hasRelations) {
|
|
185
220
|
lines.push(`/** Available relations for the \`${table.name}\` table */`);
|
|
186
221
|
lines.push(`export interface ${typeName}Relations {`);
|
|
187
|
-
for (const [relName, rel] of
|
|
222
|
+
for (const [relName, rel] of safeRelations) {
|
|
188
223
|
const targetType = entityName(rel.to);
|
|
189
224
|
// manyToMany is a collection too → 'many' cardinality (same as hasMany).
|
|
190
225
|
const cardinality = rel.type === 'hasMany' || rel.type === 'manyToMany' ? "'many'" : "'one'";
|
|
@@ -194,7 +229,7 @@ function generateTypes(schema) {
|
|
|
194
229
|
lines.push('}');
|
|
195
230
|
lines.push('');
|
|
196
231
|
// --- Legacy per-relation interfaces (kept for backward compatibility) ---
|
|
197
|
-
for (const [relName, rel] of
|
|
232
|
+
for (const [relName, rel] of safeRelations) {
|
|
198
233
|
const targetType = entityName(rel.to);
|
|
199
234
|
if (rel.type === 'hasMany' || rel.type === 'manyToMany') {
|
|
200
235
|
lines.push(`/** ${typeName} with \`${relName}\` relation loaded (${rel.type}: ${rel.to}) */`);
|
|
@@ -218,7 +253,8 @@ function generateTypes(schema) {
|
|
|
218
253
|
// ---------------------------------------------------------------------------
|
|
219
254
|
for (const table of Object.values(schema.tables)) {
|
|
220
255
|
const typeName = entityName(table.name);
|
|
221
|
-
const
|
|
256
|
+
const safeRelations = safeRelationsByTable.get(table.name) ?? [];
|
|
257
|
+
const hasRels = safeRelations.length > 0;
|
|
222
258
|
// WhereUnique — union of unique constraint shapes, deduplicating PK
|
|
223
259
|
const seen = new Set();
|
|
224
260
|
const uniqueSets = [];
|
|
@@ -250,14 +286,14 @@ function generateTypes(schema) {
|
|
|
250
286
|
// CreateInput / UpdateInput — extends base type with optional relation fields
|
|
251
287
|
if (hasRels) {
|
|
252
288
|
lines.push(`export type ${typeName}CreateInput = ${typeName}Create & {`);
|
|
253
|
-
for (const [relName, rel] of
|
|
289
|
+
for (const [relName, rel] of safeRelations) {
|
|
254
290
|
const targetType = entityName(rel.to);
|
|
255
291
|
lines.push(` ${relName}?: ${targetType}NestedCreateInput;`);
|
|
256
292
|
}
|
|
257
293
|
lines.push('};');
|
|
258
294
|
lines.push('');
|
|
259
295
|
lines.push(`export type ${typeName}UpdateInput = ${typeName}Update & {`);
|
|
260
|
-
for (const [relName, rel] of
|
|
296
|
+
for (const [relName, rel] of safeRelations) {
|
|
261
297
|
const targetType = entityName(rel.to);
|
|
262
298
|
if (rel.type === 'hasMany') {
|
|
263
299
|
lines.push(` ${relName}?: ${targetType}NestedUpdateInput;`);
|
|
@@ -273,7 +309,7 @@ function generateTypes(schema) {
|
|
|
273
309
|
// Emit NestedCreateInput, NestedUpdateInput, ConnectOrCreate for every table
|
|
274
310
|
for (const table of Object.values(schema.tables)) {
|
|
275
311
|
const typeName = entityName(table.name);
|
|
276
|
-
const hasRels =
|
|
312
|
+
const hasRels = (safeRelationsByTable.get(table.name) ?? []).length > 0;
|
|
277
313
|
// NestedCreateInput uses *CreateInput (which includes relation fields) when
|
|
278
314
|
// the table has relations, otherwise falls back to the plain *Create type.
|
|
279
315
|
const createRefType = hasRels ? `${typeName}CreateInput` : `${typeName}Create`;
|
|
@@ -358,8 +394,8 @@ function zodBaseType(col, enums) {
|
|
|
358
394
|
* columns omitted, every remaining column optional). Exported so tests can pin
|
|
359
395
|
* the output without writing files.
|
|
360
396
|
*/
|
|
361
|
-
function generateZod(schema) {
|
|
362
|
-
const lines = [...generatedFileHeader()];
|
|
397
|
+
function generateZod(schema, options) {
|
|
398
|
+
const lines = [...generatedFileHeader(options)];
|
|
363
399
|
// `zod` is a USER dependency — this generated file imports it, but the Turbine
|
|
364
400
|
// library runtime never does, so Zod stays out of the package's dep graph.
|
|
365
401
|
lines.push("import { z } from 'zod';");
|
|
@@ -416,9 +452,9 @@ function generateZod(schema) {
|
|
|
416
452
|
// ---------------------------------------------------------------------------
|
|
417
453
|
// metadata.ts generator
|
|
418
454
|
// ---------------------------------------------------------------------------
|
|
419
|
-
function generateMetadata(schema) {
|
|
455
|
+
function generateMetadata(schema, options) {
|
|
420
456
|
const lines = [
|
|
421
|
-
...generatedFileHeader(),
|
|
457
|
+
...generatedFileHeader(options),
|
|
422
458
|
"import type { SchemaMetadata } from 'turbine-orm';",
|
|
423
459
|
'',
|
|
424
460
|
'export const SCHEMA: SchemaMetadata = {',
|
|
@@ -518,10 +554,15 @@ function generateMetadata(schema) {
|
|
|
518
554
|
// ---------------------------------------------------------------------------
|
|
519
555
|
// index.ts generator (configured client with typed table accessors)
|
|
520
556
|
// ---------------------------------------------------------------------------
|
|
521
|
-
function generateIndex(schema) {
|
|
557
|
+
function generateIndex(schema, options) {
|
|
522
558
|
const tableEntries = Object.values(schema.tables);
|
|
559
|
+
// Must mirror generateTypes: `XRelations` only exists in types.ts when the
|
|
560
|
+
// table has at least one type-safe (non-column-shadowing) relation.
|
|
561
|
+
const hasSafeRelations = new Map();
|
|
562
|
+
for (const t of tableEntries)
|
|
563
|
+
hasSafeRelations.set(t.name, typeSafeRelations(t, false).length > 0);
|
|
523
564
|
const lines = [
|
|
524
|
-
...generatedFileHeader(),
|
|
565
|
+
...generatedFileHeader(options),
|
|
525
566
|
"import { TurbineClient as BaseTurbineClient, TransactionClient as BaseTransactionClient, QueryInterface } from 'turbine-orm';",
|
|
526
567
|
"import type { TurbineConfig, TransactionOptions, DeferredQuery, PipelineResults } from 'turbine-orm';",
|
|
527
568
|
"import { SCHEMA } from './metadata.js';",
|
|
@@ -530,7 +571,7 @@ function generateIndex(schema) {
|
|
|
530
571
|
const typeImports = [];
|
|
531
572
|
for (const t of tableEntries) {
|
|
532
573
|
typeImports.push(entityName(t.name));
|
|
533
|
-
if (
|
|
574
|
+
if (hasSafeRelations.get(t.name)) {
|
|
534
575
|
typeImports.push(`${entityName(t.name)}Relations`);
|
|
535
576
|
}
|
|
536
577
|
}
|
|
@@ -552,7 +593,7 @@ function generateIndex(schema) {
|
|
|
552
593
|
for (const table of tableEntries) {
|
|
553
594
|
const typeName = entityName(table.name);
|
|
554
595
|
const accessor = snakeToCamelStr(table.name);
|
|
555
|
-
const hasRelations =
|
|
596
|
+
const hasRelations = hasSafeRelations.get(table.name) === true;
|
|
556
597
|
const genericArgs = hasRelations ? `${typeName}, ${typeName}Relations` : typeName;
|
|
557
598
|
lines.push(` /** Query interface for the \`${table.name}\` table (transaction-scoped) */`);
|
|
558
599
|
lines.push(` declare readonly ${accessor}: ${accessorType(table, genericArgs)};`);
|
|
@@ -594,7 +635,7 @@ function generateIndex(schema) {
|
|
|
594
635
|
for (const table of tableEntries) {
|
|
595
636
|
const typeName = entityName(table.name);
|
|
596
637
|
const accessor = snakeToCamelStr(table.name);
|
|
597
|
-
const hasRelations =
|
|
638
|
+
const hasRelations = hasSafeRelations.get(table.name) === true;
|
|
598
639
|
const genericArgs = hasRelations ? `${typeName}, ${typeName}Relations` : typeName;
|
|
599
640
|
lines.push(` /** Query interface for the \`${table.name}\` table */`);
|
|
600
641
|
lines.push(` declare readonly ${accessor}: ${accessorType(table, genericArgs)};`);
|
|
@@ -677,6 +718,11 @@ function serializeColumn(col) {
|
|
|
677
718
|
`arrayType: '${escSQ(col.arrayType ?? col.pgArrayType)}'`,
|
|
678
719
|
`pgArrayType: '${escSQ(col.pgArrayType)}'`,
|
|
679
720
|
];
|
|
721
|
+
// Cross-schema type marker — introspection records it only for types living
|
|
722
|
+
// outside the introspected schema; it must survive codegen or the runtime
|
|
723
|
+
// enum-cast guard in query/builder.ts loses the signal (N-5).
|
|
724
|
+
if (col.pgTypeSchema !== undefined)
|
|
725
|
+
parts.push(`pgTypeSchema: '${escSQ(col.pgTypeSchema)}'`);
|
|
680
726
|
// Emit isGenerated only when set (server-generated serial/identity), so the
|
|
681
727
|
// output stays byte-identical for the common client-default columns.
|
|
682
728
|
if (col.isGenerated)
|
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.defineSeed = 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.schemaDefToMetadata = 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; } });
|
|
@@ -112,6 +112,9 @@ Object.defineProperty(exports, "column", { enumerable: true, get: function () {
|
|
|
112
112
|
Object.defineProperty(exports, "defineSchema", { enumerable: true, get: function () { return schema_builder_js_1.defineSchema; } });
|
|
113
113
|
// Legacy compat (deprecated — use object format with defineSchema)
|
|
114
114
|
Object.defineProperty(exports, "table", { enumerable: true, get: function () { return schema_builder_js_1.table; } });
|
|
115
|
+
// Schema metadata bridge — defineSchema() → SchemaMetadata without a live DB
|
|
116
|
+
var schema_metadata_js_1 = require("./schema-metadata.js");
|
|
117
|
+
Object.defineProperty(exports, "schemaDefToMetadata", { enumerable: true, get: function () { return schema_metadata_js_1.schemaDefToMetadata; } });
|
|
115
118
|
// Schema SQL — generate DDL, diff, and push
|
|
116
119
|
var schema_sql_js_1 = require("./schema-sql.js");
|
|
117
120
|
Object.defineProperty(exports, "schemaDiff", { enumerable: true, get: function () { return schema_sql_js_1.schemaDiff; } });
|