turbine-orm 0.29.0 → 0.31.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 +47 -6
- 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 +42 -136
- package/dist/cjs/mysql.js +16 -129
- package/dist/cjs/optional-peer-import.cjs +122 -0
- package/dist/cjs/powdb.js +579 -89
- package/dist/cjs/powql.js +56 -26
- package/dist/cjs/query/builder.js +601 -86
- package/dist/cjs/query/filters.js +80 -2
- 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 +19 -2
- package/dist/client.js +47 -6
- package/dist/generate.d.ts +16 -4
- package/dist/generate.js +71 -25
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -0
- package/dist/introspect.d.ts +94 -1
- package/dist/introspect.js +345 -120
- package/dist/mssql.js +40 -104
- 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 +118 -23
- package/dist/powdb.js +574 -88
- package/dist/powql.d.ts +6 -0
- package/dist/powql.js +58 -28
- package/dist/query/builder.d.ts +145 -8
- package/dist/query/builder.js +602 -87
- package/dist/query/deferred.d.ts +7 -2
- package/dist/query/filters.d.ts +46 -1
- package/dist/query/filters.js +76 -1
- package/dist/query/index.d.ts +1 -1
- package/dist/query/types.d.ts +85 -11
- 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/dist/cli/mcp.js
CHANGED
|
@@ -3,8 +3,9 @@ import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
|
3
3
|
import { dirname, resolve } from 'node:path';
|
|
4
4
|
import pg from 'pg';
|
|
5
5
|
import { findMissingRelationIndexes } from '../index-advisor.js';
|
|
6
|
+
import { addAutoManyToManyRelations, buildRelationsFromForeignKeys, isUnknownTsType, } from '../introspect.js';
|
|
6
7
|
import { QueryInterface, quoteIdent } from '../query/index.js';
|
|
7
|
-
import { isDateType, pgArrayType, pgTypeToTs,
|
|
8
|
+
import { isDateType, pgArrayType, pgTypeToTs, snakeToCamel, } from '../schema.js';
|
|
8
9
|
import { listMigrationFiles } from './migrate.js';
|
|
9
10
|
/**
|
|
10
11
|
* Walk up from the running script to find turbine-orm's own package.json.
|
|
@@ -550,7 +551,7 @@ async function loadSchemaMetadata(client, options) {
|
|
|
550
551
|
labels.push(row.enumlabel);
|
|
551
552
|
enums[row.typname] = labels;
|
|
552
553
|
}
|
|
553
|
-
const relationsByTable = buildRelations(tableNames, columnsByTable, pkByTable, fkResult.rows);
|
|
554
|
+
const relationsByTable = buildRelations(tableNames, columnsByTable, pkByTable, fkResult.rows, enums);
|
|
554
555
|
const tables = {};
|
|
555
556
|
for (const tableName of tableNames) {
|
|
556
557
|
const columns = columnsByTable.get(tableName) ?? [];
|
|
@@ -589,7 +590,15 @@ async function loadSchemaMetadata(client, options) {
|
|
|
589
590
|
}
|
|
590
591
|
return { tables, enums };
|
|
591
592
|
}
|
|
592
|
-
|
|
593
|
+
/**
|
|
594
|
+
* Group raw FK rows into constraint-level entries and delegate relation
|
|
595
|
+
* naming to the SHARED introspection builder (`buildRelationsFromForeignKeys`
|
|
596
|
+
* + `addAutoManyToManyRelations` in ../introspect.ts). MCP previously carried
|
|
597
|
+
* a stale copy of a retired naming scheme, so `turbine mcp` and `turbine
|
|
598
|
+
* generate` derived DIFFERENT relation names from the same database (N-3).
|
|
599
|
+
* Exported for the parity unit test.
|
|
600
|
+
*/
|
|
601
|
+
export function buildRelations(tableNames, columnsByTable, pkByTable, rows, enums = {}) {
|
|
593
602
|
const tableSet = new Set(tableNames);
|
|
594
603
|
const groups = new Map();
|
|
595
604
|
for (const row of rows) {
|
|
@@ -607,95 +616,18 @@ function buildRelations(tableNames, columnsByTable, pkByTable, rows) {
|
|
|
607
616
|
groups.set(row.constraint_name, group);
|
|
608
617
|
}
|
|
609
618
|
const foreignKeys = [...groups.values()];
|
|
610
|
-
const
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
const referenceKey = oneOrMany(fk.targetColumns);
|
|
621
|
-
const belongsToName = needsDisambiguation
|
|
622
|
-
? fk.sourceColumns.length === 1
|
|
623
|
-
? snakeToCamel(fk.sourceColumns[0].replace(/_id$/, ''))
|
|
624
|
-
: snakeToCamel(fk.constraintName.replace(/^fk_/, '').replace(/_fkey$/, ''))
|
|
625
|
-
: singularize(snakeToCamel(fk.targetTable));
|
|
626
|
-
const hasManyName = needsDisambiguation
|
|
627
|
-
? fk.sourceColumns.length === 1
|
|
628
|
-
? snakeToCamel(`${fk.sourceTable}_by_${fk.sourceColumns[0].replace(/_id$/, '')}`)
|
|
629
|
-
: snakeToCamel(`${fk.sourceTable}_by_${fk.constraintName.replace(/^fk_/, '').replace(/_fkey$/, '')}`)
|
|
630
|
-
: snakeToCamel(fk.sourceTable);
|
|
631
|
-
const sourceRels = relations.get(fk.sourceTable) ?? {};
|
|
632
|
-
sourceRels[belongsToName] = {
|
|
633
|
-
type: 'belongsTo',
|
|
634
|
-
name: belongsToName,
|
|
635
|
-
from: fk.sourceTable,
|
|
636
|
-
to: fk.targetTable,
|
|
637
|
-
foreignKey,
|
|
638
|
-
referenceKey,
|
|
639
|
-
};
|
|
640
|
-
relations.set(fk.sourceTable, sourceRels);
|
|
641
|
-
const targetRels = relations.get(fk.targetTable) ?? {};
|
|
642
|
-
targetRels[hasManyName] = {
|
|
643
|
-
type: 'hasMany',
|
|
644
|
-
name: hasManyName,
|
|
645
|
-
from: fk.targetTable,
|
|
646
|
-
to: fk.sourceTable,
|
|
647
|
-
foreignKey,
|
|
648
|
-
referenceKey,
|
|
649
|
-
};
|
|
650
|
-
relations.set(fk.targetTable, targetRels);
|
|
651
|
-
}
|
|
652
|
-
addManyToManyRelations(tableNames, columnsByTable, pkByTable, foreignKeys, relations);
|
|
619
|
+
const columnFieldsByTable = new Map();
|
|
620
|
+
const unknownTypedFieldsByTable = new Map();
|
|
621
|
+
for (const [tbl, cols] of columnsByTable) {
|
|
622
|
+
columnFieldsByTable.set(tbl, new Set(cols.map((c) => c.field)));
|
|
623
|
+
// Enum-typed columns also report tsType 'unknown', but the generated type
|
|
624
|
+
// layer gives them a concrete union — only json/jsonb qualify as shadows.
|
|
625
|
+
unknownTypedFieldsByTable.set(tbl, new Set(cols.filter((c) => isUnknownTsType(c.tsType) && !Object.hasOwn(enums, c.pgType)).map((c) => c.field)));
|
|
626
|
+
}
|
|
627
|
+
const relations = buildRelationsFromForeignKeys(foreignKeys, columnFieldsByTable, undefined, unknownTypedFieldsByTable);
|
|
628
|
+
addAutoManyToManyRelations(tableNames, foreignKeys, pkByTable, new Map(Array.from(columnsByTable, ([tbl, cols]) => [tbl, cols.map((c) => c.name)])), relations, columnFieldsByTable, unknownTypedFieldsByTable);
|
|
653
629
|
return relations;
|
|
654
630
|
}
|
|
655
|
-
function addManyToManyRelations(tableNames, columnsByTable, pkByTable, foreignKeys, relations) {
|
|
656
|
-
for (const tableName of tableNames) {
|
|
657
|
-
const pk = pkByTable.get(tableName) ?? [];
|
|
658
|
-
if (pk.length !== 2)
|
|
659
|
-
continue;
|
|
660
|
-
const tableFks = foreignKeys.filter((fk) => fk.sourceTable === tableName);
|
|
661
|
-
if (tableFks.length !== 2 || tableFks.some((fk) => fk.sourceColumns.length !== 1))
|
|
662
|
-
continue;
|
|
663
|
-
const fkCols = tableFks.map((fk) => fk.sourceColumns[0]);
|
|
664
|
-
const pkSet = new Set(pk);
|
|
665
|
-
if (!fkCols.every((column) => pkSet.has(column)) || new Set(fkCols).size !== 2)
|
|
666
|
-
continue;
|
|
667
|
-
const [fkA, fkB] = tableFks;
|
|
668
|
-
if (fkA.targetTable === fkB.targetTable)
|
|
669
|
-
continue;
|
|
670
|
-
const junctionColumns = (columnsByTable.get(tableName) ?? []).map((column) => column.name);
|
|
671
|
-
if (junctionColumns.length !== 2)
|
|
672
|
-
continue;
|
|
673
|
-
addManyToManyDirection(relations, tableName, fkA, fkB);
|
|
674
|
-
addManyToManyDirection(relations, tableName, fkB, fkA);
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
function addManyToManyDirection(relations, junctionTable, self, other) {
|
|
678
|
-
const sourceTable = self.targetTable;
|
|
679
|
-
const targetTable = other.targetTable;
|
|
680
|
-
const relName = snakeToCamel(targetTable);
|
|
681
|
-
const tableRelations = relations.get(sourceTable) ?? {};
|
|
682
|
-
if (tableRelations[relName])
|
|
683
|
-
return;
|
|
684
|
-
tableRelations[relName] = {
|
|
685
|
-
type: 'manyToMany',
|
|
686
|
-
name: relName,
|
|
687
|
-
from: sourceTable,
|
|
688
|
-
to: targetTable,
|
|
689
|
-
referenceKey: oneOrMany(self.targetColumns),
|
|
690
|
-
foreignKey: oneOrMany(self.targetColumns),
|
|
691
|
-
through: {
|
|
692
|
-
table: junctionTable,
|
|
693
|
-
sourceKey: self.sourceColumns[0],
|
|
694
|
-
targetKey: other.sourceColumns[0],
|
|
695
|
-
},
|
|
696
|
-
};
|
|
697
|
-
relations.set(sourceTable, tableRelations);
|
|
698
|
-
}
|
|
699
631
|
async function estimateRows(client, schema) {
|
|
700
632
|
const result = await client.query(`SELECT c.relname, c.reltuples::bigint::text AS reltuples
|
|
701
633
|
FROM pg_class c
|
|
@@ -723,9 +655,6 @@ function extractIndexColumns(indexdef) {
|
|
|
723
655
|
.replace(/ (ASC|DESC)$/i, '')
|
|
724
656
|
.replace(/^"|"$/g, ''));
|
|
725
657
|
}
|
|
726
|
-
function oneOrMany(columns) {
|
|
727
|
-
return columns.length === 1 ? columns[0] : columns;
|
|
728
|
-
}
|
|
729
658
|
function optionalLimit(value) {
|
|
730
659
|
if (value === undefined)
|
|
731
660
|
return 50;
|
package/dist/client.d.ts
CHANGED
|
@@ -68,6 +68,19 @@ export interface PgCompatPoolClient {
|
|
|
68
68
|
* sequential.
|
|
69
69
|
*/
|
|
70
70
|
readonly supportsPipelining?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Optional engine seam: scope a transaction's user callback to its own
|
|
73
|
+
* async subtree. When present, `TurbineClient.transaction` / `$transaction`
|
|
74
|
+
* invoke the callback as `wrapTransactionCallback(() => fn(tx))` instead of
|
|
75
|
+
* `fn(tx)` directly. Single-writer engines (PowDB) implement it with
|
|
76
|
+
* `AsyncLocalStorage.run()` to plant their re-entrancy marker so that it
|
|
77
|
+
* exists ONLY inside the callback's async subtree: a transaction opened
|
|
78
|
+
* from inside the callback is detected as re-entrant (typed E017), while
|
|
79
|
+
* the CALLER's context stays unmarked, so same-tick sibling transactions
|
|
80
|
+
* queue FIFO instead of being falsely flagged. Absent on pg and every other
|
|
81
|
+
* engine, in which case the callback runs unwrapped (zero behavior change).
|
|
82
|
+
*/
|
|
83
|
+
wrapTransactionCallback?<R>(fn: () => Promise<R>): Promise<R>;
|
|
71
84
|
}
|
|
72
85
|
/**
|
|
73
86
|
* Minimal pg-compatible pool. Pass any driver that satisfies this interface
|
|
@@ -149,8 +162,12 @@ export interface TurbineConfig {
|
|
|
149
162
|
logging?: boolean;
|
|
150
163
|
/** Default LIMIT applied to findMany() when no limit is specified (opt-in, default: undefined) */
|
|
151
164
|
defaultLimit?: number;
|
|
152
|
-
/**
|
|
153
|
-
|
|
165
|
+
/**
|
|
166
|
+
* Log a warning when findMany() is called without a limit (default: false).
|
|
167
|
+
* Pass a per-table map (`{ users: false }`) to override the default for
|
|
168
|
+
* specific tables; per-call `warnOnUnlimited` on findMany args wins over both.
|
|
169
|
+
*/
|
|
170
|
+
warnOnUnlimited?: boolean | Record<string, boolean>;
|
|
154
171
|
/**
|
|
155
172
|
* Interpret Postgres `timestamp` (without time zone) values as UTC — both
|
|
156
173
|
* at the driver level (OID 1114 type parser, registered only when Turbine
|
package/dist/client.js
CHANGED
|
@@ -763,14 +763,36 @@ export class TurbineClient {
|
|
|
763
763
|
*/
|
|
764
764
|
async transaction(fn) {
|
|
765
765
|
const client = await this.pool.connect();
|
|
766
|
+
/**
|
|
767
|
+
* Only true once BEGIN has actually succeeded. If BEGIN itself throws
|
|
768
|
+
* (e.g. a single-writer engine's transaction gate times out or rejects a
|
|
769
|
+
* re-entrant begin), issuing a "best-effort" ROLLBACK would be a stray
|
|
770
|
+
* statement from a context that never opened a transaction — on a driver
|
|
771
|
+
* with one shared engine handle (PowDB embedded) it would roll back a
|
|
772
|
+
* DIFFERENT caller's open transaction.
|
|
773
|
+
*/
|
|
774
|
+
let began = false;
|
|
766
775
|
try {
|
|
767
776
|
await client.query(this.dialect.beginStatement());
|
|
768
|
-
|
|
777
|
+
began = true;
|
|
778
|
+
// Engine seam: single-writer engines scope their transaction re-entrancy
|
|
779
|
+
// marker to the callback's async subtree (see
|
|
780
|
+
// PgCompatPoolClient.wrapTransactionCallback). Absent everywhere else.
|
|
781
|
+
const wrap = client.wrapTransactionCallback;
|
|
782
|
+
// `.call` erases the generic, so the callback's Promise<T> is re-asserted.
|
|
783
|
+
const result = wrap ? (await wrap.call(client, () => fn(client))) : await fn(client);
|
|
769
784
|
await client.query(this.dialect.commitStatement());
|
|
770
785
|
return result;
|
|
771
786
|
}
|
|
772
787
|
catch (err) {
|
|
773
|
-
|
|
788
|
+
if (began) {
|
|
789
|
+
try {
|
|
790
|
+
await client.query(this.dialect.rollbackStatement());
|
|
791
|
+
}
|
|
792
|
+
catch {
|
|
793
|
+
// Best-effort rollback — the connection may have died mid-query.
|
|
794
|
+
}
|
|
795
|
+
}
|
|
774
796
|
throw err;
|
|
775
797
|
}
|
|
776
798
|
finally {
|
|
@@ -804,11 +826,21 @@ export class TurbineClient {
|
|
|
804
826
|
}
|
|
805
827
|
};
|
|
806
828
|
let timedOut = false;
|
|
829
|
+
/**
|
|
830
|
+
* Only true once BEGIN has actually succeeded. If BEGIN itself throws —
|
|
831
|
+
* e.g. a single-writer engine's transaction gate times out in its FIFO
|
|
832
|
+
* queue or rejects a re-entrant begin (PowDB, E002/E017) — this context
|
|
833
|
+
* never opened a transaction, so the catch below must NOT issue its
|
|
834
|
+
* best-effort ROLLBACK: on a driver with one shared engine handle that
|
|
835
|
+
* stray ROLLBACK would tear down a DIFFERENT caller's open transaction.
|
|
836
|
+
*/
|
|
837
|
+
let began = false;
|
|
807
838
|
try {
|
|
808
839
|
// BEGIN with optional isolation level — the dialect owns the keyword and
|
|
809
840
|
// BEGIN+isolation composition (Postgres appends ` ISOLATION LEVEL …`).
|
|
810
841
|
const isolationSql = options?.isolationLevel ? ISOLATION_LEVELS[options.isolationLevel] : undefined;
|
|
811
842
|
await client.query(this.dialect.beginStatement(isolationSql));
|
|
843
|
+
began = true;
|
|
812
844
|
// Apply transaction-local session context (RLS / multi-tenant GUCs).
|
|
813
845
|
// Order matters: BEGIN -> isolation level (above) -> set_config loop ->
|
|
814
846
|
// user fn. Any error here propagates to the catch below and rolls back
|
|
@@ -841,6 +873,12 @@ export class TurbineClient {
|
|
|
841
873
|
}
|
|
842
874
|
}
|
|
843
875
|
let result;
|
|
876
|
+
// Engine seam: when the checked-out connection exposes
|
|
877
|
+
// wrapTransactionCallback (single-writer engines such as PowDB), run the user
|
|
878
|
+
// callback through it so the engine can scope its re-entrancy marker to
|
|
879
|
+
// the callback's async subtree. All other drivers: plain fn(tx).
|
|
880
|
+
const wrap = client.wrapTransactionCallback;
|
|
881
|
+
const runCallback = () => (wrap ? wrap.call(client, () => fn(tx)) : fn(tx));
|
|
844
882
|
if (timeout) {
|
|
845
883
|
// Race between the function and a timeout. If the timeout fires we
|
|
846
884
|
// need to actually abort the in-flight query — otherwise the backend
|
|
@@ -862,14 +900,14 @@ export class TurbineClient {
|
|
|
862
900
|
}, timeout);
|
|
863
901
|
});
|
|
864
902
|
try {
|
|
865
|
-
result = await Promise.race([
|
|
903
|
+
result = await Promise.race([runCallback(), timeoutPromise]);
|
|
866
904
|
}
|
|
867
905
|
finally {
|
|
868
906
|
clearTimeout(timer);
|
|
869
907
|
}
|
|
870
908
|
}
|
|
871
909
|
else {
|
|
872
|
-
result = await
|
|
910
|
+
result = await runCallback();
|
|
873
911
|
}
|
|
874
912
|
await client.query(this.dialect.commitStatement());
|
|
875
913
|
if (this.logging) {
|
|
@@ -881,8 +919,11 @@ export class TurbineClient {
|
|
|
881
919
|
// If the timeout fired we already destroyed the connection — issuing a
|
|
882
920
|
// ROLLBACK on a released client would throw "Client has already been
|
|
883
921
|
// released". Skip the rollback in that case (the backend rolled back
|
|
884
|
-
// when its socket was closed).
|
|
885
|
-
|
|
922
|
+
// when its socket was closed). Likewise skip it when BEGIN never
|
|
923
|
+
// succeeded (`began` false) — there is no transaction to roll back and
|
|
924
|
+
// the stray statement could hit another caller's transaction on a
|
|
925
|
+
// shared-handle engine.
|
|
926
|
+
if (began && !timedOut && !released) {
|
|
886
927
|
try {
|
|
887
928
|
await client.query(this.dialect.rollbackStatement());
|
|
888
929
|
}
|
package/dist/generate.d.ts
CHANGED
|
@@ -23,6 +23,18 @@ export interface GenerateOptions {
|
|
|
23
23
|
* library's dependency graph. Default: `false`.
|
|
24
24
|
*/
|
|
25
25
|
zod?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Omit the `Generated at: <ISO timestamp>` line from every generated file
|
|
28
|
+
* header (T-8b — reproducible codegen). With this set, byte-identical
|
|
29
|
+
* schemas regenerate to byte-identical output, so regens produce empty
|
|
30
|
+
* diffs. Default: `false` (timestamp included, unchanged behavior).
|
|
31
|
+
*/
|
|
32
|
+
noTimestamp?: boolean;
|
|
33
|
+
}
|
|
34
|
+
/** Per-file generator options (subset of {@link GenerateOptions} the emitters need). */
|
|
35
|
+
export interface GenerateFileOptions {
|
|
36
|
+
/** Omit the `Generated at:` header line for reproducible output. */
|
|
37
|
+
noTimestamp?: boolean;
|
|
26
38
|
}
|
|
27
39
|
export declare function generate(options: GenerateOptions): {
|
|
28
40
|
outDir: string;
|
|
@@ -33,7 +45,7 @@ export declare function generate(options: GenerateOptions): {
|
|
|
33
45
|
* and *Relations brand-field interfaces). Exported so tests can pin the
|
|
34
46
|
* generator output without writing files to disk.
|
|
35
47
|
*/
|
|
36
|
-
export declare function generateTypes(schema: SchemaMetadata): string;
|
|
48
|
+
export declare function generateTypes(schema: SchemaMetadata, options?: GenerateFileOptions): string;
|
|
37
49
|
/**
|
|
38
50
|
* Generate the contents of `zod.ts`. Emits, per table, `XSchema` (the full
|
|
39
51
|
* row), `XCreateSchema` (PK/defaulted/nullable columns optional, STORED
|
|
@@ -41,6 +53,6 @@ export declare function generateTypes(schema: SchemaMetadata): string;
|
|
|
41
53
|
* columns omitted, every remaining column optional). Exported so tests can pin
|
|
42
54
|
* the output without writing files.
|
|
43
55
|
*/
|
|
44
|
-
export declare function generateZod(schema: SchemaMetadata): string;
|
|
45
|
-
export declare function generateMetadata(schema: SchemaMetadata): string;
|
|
46
|
-
export declare function generateIndex(schema: SchemaMetadata): string;
|
|
56
|
+
export declare function generateZod(schema: SchemaMetadata, options?: GenerateFileOptions): string;
|
|
57
|
+
export declare function generateMetadata(schema: SchemaMetadata, options?: GenerateFileOptions): string;
|
|
58
|
+
export declare function generateIndex(schema: SchemaMetadata, options?: GenerateFileOptions): string;
|
package/dist/generate.js
CHANGED
|
@@ -50,21 +50,22 @@ export function generate(options) {
|
|
|
50
50
|
}
|
|
51
51
|
mkdirSync(outDir, { recursive: true });
|
|
52
52
|
const files = [];
|
|
53
|
+
const fileOptions = { noTimestamp: options.noTimestamp };
|
|
53
54
|
// Generate types.ts
|
|
54
|
-
const typesContent = generateTypes(options.schema);
|
|
55
|
+
const typesContent = generateTypes(options.schema, fileOptions);
|
|
55
56
|
writeFileSync(join(outDir, 'types.ts'), typesContent, 'utf-8');
|
|
56
57
|
files.push('types.ts');
|
|
57
58
|
// Generate metadata.ts
|
|
58
|
-
const metadataContent = generateMetadata(options.schema);
|
|
59
|
+
const metadataContent = generateMetadata(options.schema, fileOptions);
|
|
59
60
|
writeFileSync(join(outDir, 'metadata.ts'), metadataContent, 'utf-8');
|
|
60
61
|
files.push('metadata.ts');
|
|
61
62
|
// Generate index.ts (configured client)
|
|
62
|
-
const indexContent = generateIndex(options.schema);
|
|
63
|
+
const indexContent = generateIndex(options.schema, fileOptions);
|
|
63
64
|
writeFileSync(join(outDir, 'index.ts'), indexContent, 'utf-8');
|
|
64
65
|
files.push('index.ts');
|
|
65
66
|
// Generate zod.ts (optional — --zod flag)
|
|
66
67
|
if (options.zod) {
|
|
67
|
-
const zodContent = generateZod(options.schema);
|
|
68
|
+
const zodContent = generateZod(options.schema, fileOptions);
|
|
68
69
|
writeFileSync(join(outDir, 'zod.ts'), zodContent, 'utf-8');
|
|
69
70
|
files.push('zod.ts');
|
|
70
71
|
}
|
|
@@ -73,24 +74,51 @@ export function generate(options) {
|
|
|
73
74
|
// ---------------------------------------------------------------------------
|
|
74
75
|
// types.ts generator
|
|
75
76
|
// ---------------------------------------------------------------------------
|
|
76
|
-
function generatedFileHeader() {
|
|
77
|
+
function generatedFileHeader(options) {
|
|
78
|
+
// `noTimestamp` omits the volatile line entirely (T-8b) so regenerating an
|
|
79
|
+
// unchanged schema produces byte-identical files.
|
|
77
80
|
return [
|
|
78
81
|
'/**',
|
|
79
82
|
' * Auto-generated by turbine-orm — DO NOT EDIT',
|
|
80
83
|
' *',
|
|
81
|
-
` * Generated at: ${new Date().toISOString()}
|
|
84
|
+
...(options?.noTimestamp ? [] : [` * Generated at: ${new Date().toISOString()}`]),
|
|
82
85
|
' * @see https://turbineorm.dev',
|
|
83
86
|
' */',
|
|
84
87
|
'',
|
|
85
88
|
];
|
|
86
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* The relations of a table that are safe to surface in the generated TYPE
|
|
92
|
+
* layer. A relation whose name equals a scalar column field would shadow the
|
|
93
|
+
* column: `interface XWithY extends X` becomes TS2430, the `XCreate & { y?: … }`
|
|
94
|
+
* intersection collapses (TS2322), and neither the column nor the relation is
|
|
95
|
+
* targetable. Introspection no longer produces such names (they are
|
|
96
|
+
* disambiguated at the source), but hand-written or legacy metadata may —
|
|
97
|
+
* skip those relations here with a warning instead of emitting broken types.
|
|
98
|
+
* The runtime metadata (metadata.ts) still carries every relation.
|
|
99
|
+
*/
|
|
100
|
+
function typeSafeRelations(table, warn = true) {
|
|
101
|
+
const columnFields = new Set(table.columns.map((c) => c.field));
|
|
102
|
+
const usable = [];
|
|
103
|
+
for (const [relName, rel] of Object.entries(table.relations)) {
|
|
104
|
+
if (columnFields.has(relName)) {
|
|
105
|
+
if (warn) {
|
|
106
|
+
console.warn(`[turbine] Relation "${relName}" on table "${table.name}" shadows a column field of the same name — ` +
|
|
107
|
+
`omitting it from the generated types. Rename the relation (or the column) to expose it.`);
|
|
108
|
+
}
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
usable.push([relName, rel]);
|
|
112
|
+
}
|
|
113
|
+
return usable;
|
|
114
|
+
}
|
|
87
115
|
/**
|
|
88
116
|
* Generate the contents of `types.ts` (entity interfaces, *Create / *Update,
|
|
89
117
|
* and *Relations brand-field interfaces). Exported so tests can pin the
|
|
90
118
|
* generator output without writing files to disk.
|
|
91
119
|
*/
|
|
92
|
-
export function generateTypes(schema) {
|
|
93
|
-
const lines = [...generatedFileHeader()];
|
|
120
|
+
export function generateTypes(schema, options) {
|
|
121
|
+
const lines = [...generatedFileHeader(options)];
|
|
94
122
|
// We import UpdateOperatorInput so generated *Update types can express
|
|
95
123
|
// atomic increment / decrement / multiply / divide / set operators on
|
|
96
124
|
// numeric columns (TASK-3.4).
|
|
@@ -106,9 +134,15 @@ export function generateTypes(schema) {
|
|
|
106
134
|
// `${TargetType}Relations` (for deep inference) or `{}` (the no-relations
|
|
107
135
|
// default) into each `RelationDescriptor`. Built once up-front because
|
|
108
136
|
// relations can point at tables we haven't iterated to yet.
|
|
137
|
+
// Relations that can be surfaced in the type layer, computed once per table
|
|
138
|
+
// (relations that would shadow a scalar column field are excluded + warned).
|
|
139
|
+
const safeRelationsByTable = new Map();
|
|
140
|
+
for (const t of Object.values(schema.tables)) {
|
|
141
|
+
safeRelationsByTable.set(t.name, typeSafeRelations(t));
|
|
142
|
+
}
|
|
109
143
|
const tablesWithRelations = new Set();
|
|
110
144
|
for (const t of Object.values(schema.tables)) {
|
|
111
|
-
if (
|
|
145
|
+
if ((safeRelationsByTable.get(t.name) ?? []).length > 0)
|
|
112
146
|
tablesWithRelations.add(t.name);
|
|
113
147
|
}
|
|
114
148
|
// Generate enum types
|
|
@@ -173,11 +207,12 @@ export function generateTypes(schema) {
|
|
|
173
207
|
// any depth — `RelationRelations<R[K]>` reads the third type parameter
|
|
174
208
|
// and threads it into the next recursion step. If the target table has
|
|
175
209
|
// no relations of its own, the descriptor uses `{}` (the default).
|
|
176
|
-
const
|
|
210
|
+
const safeRelations = safeRelationsByTable.get(table.name) ?? [];
|
|
211
|
+
const hasRelations = safeRelations.length > 0;
|
|
177
212
|
if (hasRelations) {
|
|
178
213
|
lines.push(`/** Available relations for the \`${table.name}\` table */`);
|
|
179
214
|
lines.push(`export interface ${typeName}Relations {`);
|
|
180
|
-
for (const [relName, rel] of
|
|
215
|
+
for (const [relName, rel] of safeRelations) {
|
|
181
216
|
const targetType = entityName(rel.to);
|
|
182
217
|
// manyToMany is a collection too → 'many' cardinality (same as hasMany).
|
|
183
218
|
const cardinality = rel.type === 'hasMany' || rel.type === 'manyToMany' ? "'many'" : "'one'";
|
|
@@ -187,7 +222,7 @@ export function generateTypes(schema) {
|
|
|
187
222
|
lines.push('}');
|
|
188
223
|
lines.push('');
|
|
189
224
|
// --- Legacy per-relation interfaces (kept for backward compatibility) ---
|
|
190
|
-
for (const [relName, rel] of
|
|
225
|
+
for (const [relName, rel] of safeRelations) {
|
|
191
226
|
const targetType = entityName(rel.to);
|
|
192
227
|
if (rel.type === 'hasMany' || rel.type === 'manyToMany') {
|
|
193
228
|
lines.push(`/** ${typeName} with \`${relName}\` relation loaded (${rel.type}: ${rel.to}) */`);
|
|
@@ -211,7 +246,8 @@ export function generateTypes(schema) {
|
|
|
211
246
|
// ---------------------------------------------------------------------------
|
|
212
247
|
for (const table of Object.values(schema.tables)) {
|
|
213
248
|
const typeName = entityName(table.name);
|
|
214
|
-
const
|
|
249
|
+
const safeRelations = safeRelationsByTable.get(table.name) ?? [];
|
|
250
|
+
const hasRels = safeRelations.length > 0;
|
|
215
251
|
// WhereUnique — union of unique constraint shapes, deduplicating PK
|
|
216
252
|
const seen = new Set();
|
|
217
253
|
const uniqueSets = [];
|
|
@@ -243,14 +279,14 @@ export function generateTypes(schema) {
|
|
|
243
279
|
// CreateInput / UpdateInput — extends base type with optional relation fields
|
|
244
280
|
if (hasRels) {
|
|
245
281
|
lines.push(`export type ${typeName}CreateInput = ${typeName}Create & {`);
|
|
246
|
-
for (const [relName, rel] of
|
|
282
|
+
for (const [relName, rel] of safeRelations) {
|
|
247
283
|
const targetType = entityName(rel.to);
|
|
248
284
|
lines.push(` ${relName}?: ${targetType}NestedCreateInput;`);
|
|
249
285
|
}
|
|
250
286
|
lines.push('};');
|
|
251
287
|
lines.push('');
|
|
252
288
|
lines.push(`export type ${typeName}UpdateInput = ${typeName}Update & {`);
|
|
253
|
-
for (const [relName, rel] of
|
|
289
|
+
for (const [relName, rel] of safeRelations) {
|
|
254
290
|
const targetType = entityName(rel.to);
|
|
255
291
|
if (rel.type === 'hasMany') {
|
|
256
292
|
lines.push(` ${relName}?: ${targetType}NestedUpdateInput;`);
|
|
@@ -266,7 +302,7 @@ export function generateTypes(schema) {
|
|
|
266
302
|
// Emit NestedCreateInput, NestedUpdateInput, ConnectOrCreate for every table
|
|
267
303
|
for (const table of Object.values(schema.tables)) {
|
|
268
304
|
const typeName = entityName(table.name);
|
|
269
|
-
const hasRels =
|
|
305
|
+
const hasRels = (safeRelationsByTable.get(table.name) ?? []).length > 0;
|
|
270
306
|
// NestedCreateInput uses *CreateInput (which includes relation fields) when
|
|
271
307
|
// the table has relations, otherwise falls back to the plain *Create type.
|
|
272
308
|
const createRefType = hasRels ? `${typeName}CreateInput` : `${typeName}Create`;
|
|
@@ -351,8 +387,8 @@ function zodBaseType(col, enums) {
|
|
|
351
387
|
* columns omitted, every remaining column optional). Exported so tests can pin
|
|
352
388
|
* the output without writing files.
|
|
353
389
|
*/
|
|
354
|
-
export function generateZod(schema) {
|
|
355
|
-
const lines = [...generatedFileHeader()];
|
|
390
|
+
export function generateZod(schema, options) {
|
|
391
|
+
const lines = [...generatedFileHeader(options)];
|
|
356
392
|
// `zod` is a USER dependency — this generated file imports it, but the Turbine
|
|
357
393
|
// library runtime never does, so Zod stays out of the package's dep graph.
|
|
358
394
|
lines.push("import { z } from 'zod';");
|
|
@@ -409,9 +445,9 @@ export function generateZod(schema) {
|
|
|
409
445
|
// ---------------------------------------------------------------------------
|
|
410
446
|
// metadata.ts generator
|
|
411
447
|
// ---------------------------------------------------------------------------
|
|
412
|
-
export function generateMetadata(schema) {
|
|
448
|
+
export function generateMetadata(schema, options) {
|
|
413
449
|
const lines = [
|
|
414
|
-
...generatedFileHeader(),
|
|
450
|
+
...generatedFileHeader(options),
|
|
415
451
|
"import type { SchemaMetadata } from 'turbine-orm';",
|
|
416
452
|
'',
|
|
417
453
|
'export const SCHEMA: SchemaMetadata = {',
|
|
@@ -511,10 +547,15 @@ export function generateMetadata(schema) {
|
|
|
511
547
|
// ---------------------------------------------------------------------------
|
|
512
548
|
// index.ts generator (configured client with typed table accessors)
|
|
513
549
|
// ---------------------------------------------------------------------------
|
|
514
|
-
export function generateIndex(schema) {
|
|
550
|
+
export function generateIndex(schema, options) {
|
|
515
551
|
const tableEntries = Object.values(schema.tables);
|
|
552
|
+
// Must mirror generateTypes: `XRelations` only exists in types.ts when the
|
|
553
|
+
// table has at least one type-safe (non-column-shadowing) relation.
|
|
554
|
+
const hasSafeRelations = new Map();
|
|
555
|
+
for (const t of tableEntries)
|
|
556
|
+
hasSafeRelations.set(t.name, typeSafeRelations(t, false).length > 0);
|
|
516
557
|
const lines = [
|
|
517
|
-
...generatedFileHeader(),
|
|
558
|
+
...generatedFileHeader(options),
|
|
518
559
|
"import { TurbineClient as BaseTurbineClient, TransactionClient as BaseTransactionClient, QueryInterface } from 'turbine-orm';",
|
|
519
560
|
"import type { TurbineConfig, TransactionOptions, DeferredQuery, PipelineResults } from 'turbine-orm';",
|
|
520
561
|
"import { SCHEMA } from './metadata.js';",
|
|
@@ -523,7 +564,7 @@ export function generateIndex(schema) {
|
|
|
523
564
|
const typeImports = [];
|
|
524
565
|
for (const t of tableEntries) {
|
|
525
566
|
typeImports.push(entityName(t.name));
|
|
526
|
-
if (
|
|
567
|
+
if (hasSafeRelations.get(t.name)) {
|
|
527
568
|
typeImports.push(`${entityName(t.name)}Relations`);
|
|
528
569
|
}
|
|
529
570
|
}
|
|
@@ -545,7 +586,7 @@ export function generateIndex(schema) {
|
|
|
545
586
|
for (const table of tableEntries) {
|
|
546
587
|
const typeName = entityName(table.name);
|
|
547
588
|
const accessor = snakeToCamelStr(table.name);
|
|
548
|
-
const hasRelations =
|
|
589
|
+
const hasRelations = hasSafeRelations.get(table.name) === true;
|
|
549
590
|
const genericArgs = hasRelations ? `${typeName}, ${typeName}Relations` : typeName;
|
|
550
591
|
lines.push(` /** Query interface for the \`${table.name}\` table (transaction-scoped) */`);
|
|
551
592
|
lines.push(` declare readonly ${accessor}: ${accessorType(table, genericArgs)};`);
|
|
@@ -587,7 +628,7 @@ export function generateIndex(schema) {
|
|
|
587
628
|
for (const table of tableEntries) {
|
|
588
629
|
const typeName = entityName(table.name);
|
|
589
630
|
const accessor = snakeToCamelStr(table.name);
|
|
590
|
-
const hasRelations =
|
|
631
|
+
const hasRelations = hasSafeRelations.get(table.name) === true;
|
|
591
632
|
const genericArgs = hasRelations ? `${typeName}, ${typeName}Relations` : typeName;
|
|
592
633
|
lines.push(` /** Query interface for the \`${table.name}\` table */`);
|
|
593
634
|
lines.push(` declare readonly ${accessor}: ${accessorType(table, genericArgs)};`);
|
|
@@ -670,6 +711,11 @@ function serializeColumn(col) {
|
|
|
670
711
|
`arrayType: '${escSQ(col.arrayType ?? col.pgArrayType)}'`,
|
|
671
712
|
`pgArrayType: '${escSQ(col.pgArrayType)}'`,
|
|
672
713
|
];
|
|
714
|
+
// Cross-schema type marker — introspection records it only for types living
|
|
715
|
+
// outside the introspected schema; it must survive codegen or the runtime
|
|
716
|
+
// enum-cast guard in query/builder.ts loses the signal (N-5).
|
|
717
|
+
if (col.pgTypeSchema !== undefined)
|
|
718
|
+
parts.push(`pgTypeSchema: '${escSQ(col.pgTypeSchema)}'`);
|
|
673
719
|
// Emit isGenerated only when set (server-generated serial/identity), so the
|
|
674
720
|
// output stays byte-identical for the common client-default columns.
|
|
675
721
|
if (col.isGenerated)
|
package/dist/index.d.ts
CHANGED
|
@@ -43,11 +43,12 @@ export { type IntrospectOptions, introspect } from './introspect.js';
|
|
|
43
43
|
export { executeNestedCreate, executeNestedUpdate, hasRelationFields, type NestedWriteContext, } from './nested-write.js';
|
|
44
44
|
export type { ObserveConfig, ObserveHandle } from './observe.js';
|
|
45
45
|
export { executePipeline, type PipelineOptions, type PipelineResults, pipelineSupported } from './pipeline.js';
|
|
46
|
-
export { type AggregateArgs, type AggregateResult, type ArrayFilter, type ConnectOrCreateOp, type CountArgs, type CreateArgs, type CreateDataInput, type CreateManyArgs, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FieldResult, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GlobalFilters, type GroupByArgs, type HavingClause, type JsonFilter, type MiddlewareFn, type NestedCreateOp, type NestedUpdateOp, type NestedUpdateOpItem, type NestedUpsertOpItem, type OmitResult, type OrderByClause, type OrderDirection, type QueryEvent, type QueryEventListener, QueryInterface, type QueryResult, type RelationDescriptor, type RelationFilter, type SelectResult, type SkipGlobalFilters, type TextSearchFilter, type TypedWithClause, type UpdateArgs, type UpdateDataInput, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type VectorDistanceFilter, type VectorFilter, type VectorMetric, type VectorOrderBy, type VectorOrderByDistance, type WhereClause, type WhereOperator, type WhereValue, type WithClause, type WithOptions, type WithResult, } from './query/index.js';
|
|
46
|
+
export { type AggregateArgs, type AggregateResult, type ArrayFilter, type ColumnRef, type ConnectOrCreateOp, type CountArgs, type CreateArgs, type CreateDataInput, type CreateManyArgs, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FieldResult, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GlobalFilters, type GroupByArgs, type HavingClause, type JsonFilter, type JsonPathOrderBy, type MiddlewareFn, type NestedCreateOp, type NestedUpdateOp, type NestedUpdateOpItem, type NestedUpsertOpItem, type OmitResult, type OrderByClause, type OrderDirection, type QueryEvent, type QueryEventListener, QueryInterface, type QueryResult, type RelationDescriptor, type RelationFilter, type SelectResult, type SkipGlobalFilters, type TextSearchFilter, type TypedWithClause, type UpdateArgs, type UpdateDataInput, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type VectorDistanceFilter, type VectorFilter, type VectorMetric, type VectorOrderBy, type VectorOrderByDistance, type WhereClause, type WhereOperator, type WhereValue, type WithClause, type WithOptions, type WithResult, } from './query/index.js';
|
|
47
47
|
export { type ActiveSubscription, type NotificationHandler, type Subscription, validateChannel } from './realtime.js';
|
|
48
48
|
export type { CheckMetadata, ColumnMetadata, IndexMetadata, ReferentialAction, RelationDef, SchemaMetadata, TableMetadata, } from './schema.js';
|
|
49
49
|
export { camelToSnake, isDateType, normalizeKeyColumns, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, } from './schema.js';
|
|
50
50
|
export { applyManyToManyRelations, type CheckDef, ColumnBuilder, type ColumnConfig, type ColumnDef, type ColumnType, type ColumnTypeName, column, type DefineSchemaOptions, defineSchema, type ManyToManyDef, type ReferenceDef, type SchemaDef, type TableDef, table, } from './schema-builder.js';
|
|
51
|
+
export { schemaDefToMetadata } from './schema-metadata.js';
|
|
51
52
|
export { type AlterColumnDef, type AlterDef, type DiffResult, type PushResult, type SchemaSqlOptions, schemaDiff, schemaPush, schemaToSQL, schemaToSQLString, } from './schema-sql.js';
|
|
52
53
|
export { type DefinedSeed, defineSeed, type SeedFunction } from './seed.js';
|
|
53
54
|
export { type TurbineHttpOptions, turbineHttp } from './serverless.js';
|
package/dist/index.js
CHANGED
|
@@ -56,6 +56,8 @@ export { camelToSnake, isDateType, normalizeKeyColumns, pgArrayType, pgTypeToTs,
|
|
|
56
56
|
export { applyManyToManyRelations, ColumnBuilder, column, defineSchema,
|
|
57
57
|
// Legacy compat (deprecated — use object format with defineSchema)
|
|
58
58
|
table, } from './schema-builder.js';
|
|
59
|
+
// Schema metadata bridge — defineSchema() → SchemaMetadata without a live DB
|
|
60
|
+
export { schemaDefToMetadata } from './schema-metadata.js';
|
|
59
61
|
// Schema SQL — generate DDL, diff, and push
|
|
60
62
|
export { schemaDiff, schemaPush, schemaToSQL, schemaToSQLString, } from './schema-sql.js';
|
|
61
63
|
// Seed helper
|