turbine-orm 0.28.3 → 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 +69 -5
- 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 +440 -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 +44 -6
- package/dist/client.js +69 -5
- 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 +94 -26
- package/dist/powdb.js +435 -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/dist/client.d.ts
CHANGED
|
@@ -57,6 +57,17 @@ export interface PgCompatQueryResult<R = Record<string, unknown>> {
|
|
|
57
57
|
export interface PgCompatPoolClient {
|
|
58
58
|
query<R = Record<string, unknown>>(text: string, values?: unknown[]): Promise<PgCompatQueryResult<R>>;
|
|
59
59
|
release(err?: Error | boolean): void;
|
|
60
|
+
/**
|
|
61
|
+
* Optional driver capability: `true` when `query()` may be called again on
|
|
62
|
+
* this connection while earlier calls are still in flight, with replies
|
|
63
|
+
* delivered to callers in FIFO submission order. Drivers that set this let
|
|
64
|
+
* the batch `$transaction([...])` overload dispatch every statement in one
|
|
65
|
+
* write burst (~1 network round trip plus server time) instead of awaiting
|
|
66
|
+
* each reply before sending the next (N round trips). Leave unset for
|
|
67
|
+
* drivers (node-postgres included) whose batch path must stay strictly
|
|
68
|
+
* sequential.
|
|
69
|
+
*/
|
|
70
|
+
readonly supportsPipelining?: boolean;
|
|
60
71
|
}
|
|
61
72
|
/**
|
|
62
73
|
* Minimal pg-compatible pool. Pass any driver that satisfies this interface
|
|
@@ -138,8 +149,12 @@ export interface TurbineConfig {
|
|
|
138
149
|
logging?: boolean;
|
|
139
150
|
/** Default LIMIT applied to findMany() when no limit is specified (opt-in, default: undefined) */
|
|
140
151
|
defaultLimit?: number;
|
|
141
|
-
/**
|
|
142
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Log a warning when findMany() is called without a limit (default: false).
|
|
154
|
+
* Pass a per-table map (`{ users: false }`) to override the default for
|
|
155
|
+
* specific tables; per-call `warnOnUnlimited` on findMany args wins over both.
|
|
156
|
+
*/
|
|
157
|
+
warnOnUnlimited?: boolean | Record<string, boolean>;
|
|
143
158
|
/**
|
|
144
159
|
* Interpret Postgres `timestamp` (without time zone) values as UTC — both
|
|
145
160
|
* at the driver level (OID 1114 type parser, registered only when Turbine
|
|
@@ -564,8 +579,13 @@ export declare class TurbineClient {
|
|
|
564
579
|
* of each query's transformed result; any failure rolls the whole batch back.
|
|
565
580
|
*
|
|
566
581
|
* Unlike {@link pipeline}, this never uses the extended-query pipeline
|
|
567
|
-
* protocol
|
|
568
|
-
* safe on every driver
|
|
582
|
+
* protocol. Statements run on the single transaction connection: strictly
|
|
583
|
+
* sequentially by default (safe on every driver, including HTTP/serverless
|
|
584
|
+
* pools), or — when the checked-out connection advertises
|
|
585
|
+
* {@link PgCompatPoolClient.supportsPipelining} — dispatched in one write
|
|
586
|
+
* burst with replies collected in order, saving a network round trip per
|
|
587
|
+
* statement. Either way the failure contract is identical: the first
|
|
588
|
+
* (lowest-index) failure aborts the batch and rolls everything back.
|
|
569
589
|
*
|
|
570
590
|
* @example
|
|
571
591
|
* ```ts
|
|
@@ -580,8 +600,26 @@ export declare class TurbineClient {
|
|
|
580
600
|
* Execute a batch of {@link DeferredQuery} objects atomically inside one
|
|
581
601
|
* transaction. Backs the `$transaction([...])` array overload. Reuses the raw
|
|
582
602
|
* {@link transaction} machinery (BEGIN/COMMIT/ROLLBACK + connection release);
|
|
583
|
-
*
|
|
584
|
-
*
|
|
603
|
+
* each result is passed through its query's `transform`.
|
|
604
|
+
*
|
|
605
|
+
* Execution strategy on the single transaction connection:
|
|
606
|
+
* - **Sequential (default).** Await each statement's reply before sending
|
|
607
|
+
* the next. Safe on every driver; on a networked driver a batch of N
|
|
608
|
+
* costs N round trips.
|
|
609
|
+
* - **Pipelined.** When the checked-out connection advertises
|
|
610
|
+
* {@link PgCompatPoolClient.supportsPipelining} (its `query()` accepts
|
|
611
|
+
* concurrent calls and completes them in FIFO submission order), all
|
|
612
|
+
* statements are dispatched in one write burst and the replies are
|
|
613
|
+
* collected in order — ~1 round trip plus server time. Only taken when
|
|
614
|
+
* the dialect's writes surface rows directly (`resultStrategy` !==
|
|
615
|
+
* 'reselect'): a reselect plan is itself a sequential write+read pair.
|
|
616
|
+
*
|
|
617
|
+
* The two paths share one failure contract: the first (lowest-index) failed
|
|
618
|
+
* statement's error is thrown (wrapped via {@link wrapPgError}) and the
|
|
619
|
+
* surrounding transaction rolls back, so no statement's effect survives. The
|
|
620
|
+
* pipelined path drains every in-flight reply (`Promise.allSettled`) before
|
|
621
|
+
* rethrowing, which keeps the connection's request/reply pairing intact and
|
|
622
|
+
* means ROLLBACK is only issued once no statement is still in flight.
|
|
585
623
|
*/
|
|
586
624
|
private transactionBatch;
|
|
587
625
|
/**
|
package/dist/client.js
CHANGED
|
@@ -763,14 +763,31 @@ 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());
|
|
777
|
+
began = true;
|
|
768
778
|
const result = await fn(client);
|
|
769
779
|
await client.query(this.dialect.commitStatement());
|
|
770
780
|
return result;
|
|
771
781
|
}
|
|
772
782
|
catch (err) {
|
|
773
|
-
|
|
783
|
+
if (began) {
|
|
784
|
+
try {
|
|
785
|
+
await client.query(this.dialect.rollbackStatement());
|
|
786
|
+
}
|
|
787
|
+
catch {
|
|
788
|
+
// Best-effort rollback — the connection may have died mid-query.
|
|
789
|
+
}
|
|
790
|
+
}
|
|
774
791
|
throw err;
|
|
775
792
|
}
|
|
776
793
|
finally {
|
|
@@ -804,11 +821,21 @@ export class TurbineClient {
|
|
|
804
821
|
}
|
|
805
822
|
};
|
|
806
823
|
let timedOut = false;
|
|
824
|
+
/**
|
|
825
|
+
* Only true once BEGIN has actually succeeded. If BEGIN itself throws —
|
|
826
|
+
* e.g. a single-writer engine's transaction gate times out in its FIFO
|
|
827
|
+
* queue or rejects a re-entrant begin (PowDB, E002/E017) — this context
|
|
828
|
+
* never opened a transaction, so the catch below must NOT issue its
|
|
829
|
+
* best-effort ROLLBACK: on a driver with one shared engine handle that
|
|
830
|
+
* stray ROLLBACK would tear down a DIFFERENT caller's open transaction.
|
|
831
|
+
*/
|
|
832
|
+
let began = false;
|
|
807
833
|
try {
|
|
808
834
|
// BEGIN with optional isolation level — the dialect owns the keyword and
|
|
809
835
|
// BEGIN+isolation composition (Postgres appends ` ISOLATION LEVEL …`).
|
|
810
836
|
const isolationSql = options?.isolationLevel ? ISOLATION_LEVELS[options.isolationLevel] : undefined;
|
|
811
837
|
await client.query(this.dialect.beginStatement(isolationSql));
|
|
838
|
+
began = true;
|
|
812
839
|
// Apply transaction-local session context (RLS / multi-tenant GUCs).
|
|
813
840
|
// Order matters: BEGIN -> isolation level (above) -> set_config loop ->
|
|
814
841
|
// user fn. Any error here propagates to the catch below and rolls back
|
|
@@ -881,8 +908,11 @@ export class TurbineClient {
|
|
|
881
908
|
// If the timeout fired we already destroyed the connection — issuing a
|
|
882
909
|
// ROLLBACK on a released client would throw "Client has already been
|
|
883
910
|
// released". Skip the rollback in that case (the backend rolled back
|
|
884
|
-
// when its socket was closed).
|
|
885
|
-
|
|
911
|
+
// when its socket was closed). Likewise skip it when BEGIN never
|
|
912
|
+
// succeeded (`began` false) — there is no transaction to roll back and
|
|
913
|
+
// the stray statement could hit another caller's transaction on a
|
|
914
|
+
// shared-handle engine.
|
|
915
|
+
if (began && !timedOut && !released) {
|
|
886
916
|
try {
|
|
887
917
|
await client.query(this.dialect.rollbackStatement());
|
|
888
918
|
}
|
|
@@ -903,14 +933,48 @@ export class TurbineClient {
|
|
|
903
933
|
* Execute a batch of {@link DeferredQuery} objects atomically inside one
|
|
904
934
|
* transaction. Backs the `$transaction([...])` array overload. Reuses the raw
|
|
905
935
|
* {@link transaction} machinery (BEGIN/COMMIT/ROLLBACK + connection release);
|
|
906
|
-
*
|
|
907
|
-
*
|
|
936
|
+
* each result is passed through its query's `transform`.
|
|
937
|
+
*
|
|
938
|
+
* Execution strategy on the single transaction connection:
|
|
939
|
+
* - **Sequential (default).** Await each statement's reply before sending
|
|
940
|
+
* the next. Safe on every driver; on a networked driver a batch of N
|
|
941
|
+
* costs N round trips.
|
|
942
|
+
* - **Pipelined.** When the checked-out connection advertises
|
|
943
|
+
* {@link PgCompatPoolClient.supportsPipelining} (its `query()` accepts
|
|
944
|
+
* concurrent calls and completes them in FIFO submission order), all
|
|
945
|
+
* statements are dispatched in one write burst and the replies are
|
|
946
|
+
* collected in order — ~1 round trip plus server time. Only taken when
|
|
947
|
+
* the dialect's writes surface rows directly (`resultStrategy` !==
|
|
948
|
+
* 'reselect'): a reselect plan is itself a sequential write+read pair.
|
|
949
|
+
*
|
|
950
|
+
* The two paths share one failure contract: the first (lowest-index) failed
|
|
951
|
+
* statement's error is thrown (wrapped via {@link wrapPgError}) and the
|
|
952
|
+
* surrounding transaction rolls back, so no statement's effect survives. The
|
|
953
|
+
* pipelined path drains every in-flight reply (`Promise.allSettled`) before
|
|
954
|
+
* rethrowing, which keeps the connection's request/reply pairing intact and
|
|
955
|
+
* means ROLLBACK is only issued once no statement is still in flight.
|
|
908
956
|
*/
|
|
909
957
|
async transactionBatch(queries) {
|
|
910
958
|
if (queries.length === 0) {
|
|
911
959
|
return [];
|
|
912
960
|
}
|
|
913
961
|
return this.transaction(async (client) => {
|
|
962
|
+
const pipelined = client.supportsPipelining === true &&
|
|
963
|
+
this.dialect.resultStrategy !== 'reselect';
|
|
964
|
+
if (pipelined) {
|
|
965
|
+
// Dispatch every statement before awaiting any reply. The driver's
|
|
966
|
+
// FIFO guarantee makes settled[i] the reply to queries[i].
|
|
967
|
+
const settled = await Promise.allSettled(queries.map((dq) => client.query(dq.sql, dq.params)));
|
|
968
|
+
const results = [];
|
|
969
|
+
for (let i = 0; i < settled.length; i++) {
|
|
970
|
+
const outcome = settled[i];
|
|
971
|
+
if (outcome.status === 'rejected') {
|
|
972
|
+
throw wrapPgError(outcome.reason);
|
|
973
|
+
}
|
|
974
|
+
results.push(queries[i].transform(outcome.value));
|
|
975
|
+
}
|
|
976
|
+
return results;
|
|
977
|
+
}
|
|
914
978
|
const results = [];
|
|
915
979
|
for (const dq of queries) {
|
|
916
980
|
let raw;
|
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
|
@@ -48,6 +48,7 @@ export { type ActiveSubscription, type NotificationHandler, type Subscription, v
|
|
|
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
|
package/dist/introspect.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* This is the foundation of `npx turbine generate`.
|
|
9
9
|
*/
|
|
10
10
|
import { type Dialect } from './dialect.js';
|
|
11
|
-
import { type ReferentialAction, type SchemaMetadata } from './schema.js';
|
|
11
|
+
import { type ColumnMetadata, type ReferentialAction, type RelationDef, type SchemaMetadata } from './schema.js';
|
|
12
12
|
/**
|
|
13
13
|
* Map a `pg_constraint.confdeltype` / `confupdtype` character to a
|
|
14
14
|
* {@link ReferentialAction}. Postgres encodes: `a` = NO ACTION, `r` = RESTRICT,
|
|
@@ -56,3 +56,96 @@ export declare function introspectPostgresCatalog(options: IntrospectOptions): P
|
|
|
56
56
|
* balanced outer paren pair; leaves anything unexpected untouched.
|
|
57
57
|
*/
|
|
58
58
|
export declare function stripCheckWrapper(def: string): string;
|
|
59
|
+
/** A foreign-key constraint grouped by constraint name (composite FKs carry column arrays). */
|
|
60
|
+
export interface ForeignKeyEntry {
|
|
61
|
+
sourceTable: string;
|
|
62
|
+
sourceColumns: string[];
|
|
63
|
+
targetTable: string;
|
|
64
|
+
targetColumns: string[];
|
|
65
|
+
constraintName: string;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Derive a belongsTo relation name from its FK column. Strips a trailing
|
|
69
|
+
* `_id` (snake_case) or `Id` (camelCase column names — common in Prisma-ported
|
|
70
|
+
* schemas where columns are quoted camelCase identifiers), then camelCases:
|
|
71
|
+
* `current_version_id` and `currentVersionId` both yield `currentVersion`.
|
|
72
|
+
* Stripping is what keeps the scalar FK field (`currentVersionId`) targetable
|
|
73
|
+
* alongside the relation. A column literally named `id` (nothing left after
|
|
74
|
+
* stripping) keeps its own name.
|
|
75
|
+
*/
|
|
76
|
+
export declare function relationNameFromColumn(column: string): string;
|
|
77
|
+
/**
|
|
78
|
+
* True for the tsType forms a json/jsonb column maps to (`unknown`, nullable
|
|
79
|
+
* `unknown | null`). A relation shadowing such a column is a HISTORICAL shadow
|
|
80
|
+
* that worked at runtime and compiled (`unknown` absorbs the relation
|
|
81
|
+
* payload), so the legacy-first naming keeps it instead of renaming.
|
|
82
|
+
*/
|
|
83
|
+
export declare function isUnknownTsType(tsType: string): boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Build the belongsTo/hasMany relation maps for every table from its foreign
|
|
86
|
+
* keys. Naming rules (LEGACY-FIRST — a relation name that previously worked at
|
|
87
|
+
* runtime must never change out from under a regenerating app):
|
|
88
|
+
*
|
|
89
|
+
* 1. First compute the historical derivation exactly as it shipped before
|
|
90
|
+
* the collision guard existed: belongsTo strips a case-SENSITIVE `_id`
|
|
91
|
+
* suffix (`snakeToCamel(col.replace(/_id$/, ''))` when several FKs point
|
|
92
|
+
* at the same target, else the singularized target table), and hasMany is
|
|
93
|
+
* `snakeToCamel(`${source}_by_${strippedColumn}`)` (else the source
|
|
94
|
+
* table). If that legacy name is free, KEEP IT — even when it looks odd
|
|
95
|
+
* (`blogPostsByAuthorId`, `postsBy_Author`): those names were collision-
|
|
96
|
+
* free and worked, so regenerating must not rename them.
|
|
97
|
+
* 2. If the legacy name collides ONLY with a scalar column whose tsType is
|
|
98
|
+
* `unknown` (json/jsonb), keep it anyway with a warning: the shadow is
|
|
99
|
+
* historical, ran fine at runtime, and compiled (`unknown` absorbs the
|
|
100
|
+
* relation payload; generate.ts's typeSafeRelations omits the relation
|
|
101
|
+
* from the type layer).
|
|
102
|
+
* 3. On a genuine collision (concrete-typed column shadow, or a previously
|
|
103
|
+
* assigned relation), fall back to the modern derivation — the `_id`/`Id`
|
|
104
|
+
* case-insensitive strip of {@link relationNameFromColumn} plus the
|
|
105
|
+
* `By`-composed reverse name — which fixes the camelCase-FK shadowing
|
|
106
|
+
* shapes that were actually BROKEN before (relation name === scalar FK
|
|
107
|
+
* field → unusable types).
|
|
108
|
+
* 4. Last resort: deterministic `Rel`/`Rel2` suffix + warning.
|
|
109
|
+
*
|
|
110
|
+
* @param columnFieldsByTable camelCase column *fields* per table — used to
|
|
111
|
+
* guarantee relations never shadow concrete-typed scalar columns.
|
|
112
|
+
* @param unknownTypedFieldsByTable subset of the column fields whose tsType is
|
|
113
|
+
* `unknown` (json/jsonb) — legacy shadows of these are preserved (rule 2).
|
|
114
|
+
*/
|
|
115
|
+
export declare function buildRelationsFromForeignKeys(foreignKeys: ForeignKeyEntry[], columnFieldsByTable: Map<string, Set<string>>, fkActions?: Map<string, {
|
|
116
|
+
onDelete: ReferentialAction;
|
|
117
|
+
onUpdate: ReferentialAction;
|
|
118
|
+
}>, unknownTypedFieldsByTable?: Map<string, Set<string>>): Map<string, Record<string, RelationDef>>;
|
|
119
|
+
/**
|
|
120
|
+
* Conservative auto-`manyToMany` detection over pure junction tables, shared
|
|
121
|
+
* by the Postgres introspector, the engine introspectors (SQLite / MySQL /
|
|
122
|
+
* MSSQL), the MCP server, and `schemaDefToMetadata()` so all surfaces derive
|
|
123
|
+
* IDENTICAL relation names for the same logical schema.
|
|
124
|
+
*
|
|
125
|
+
* A table J is a PURE junction only when ALL of these hold:
|
|
126
|
+
* 1. J's primary key is exactly two columns.
|
|
127
|
+
* 2. J has exactly two FKs, each single-column.
|
|
128
|
+
* 3. Each FK's source column is one of J's two PK columns.
|
|
129
|
+
* 4. The two FKs target two DISTINCT tables (A and B).
|
|
130
|
+
* 5. J has no payload columns beyond the two FK/PK columns.
|
|
131
|
+
*
|
|
132
|
+
* For such a J linking A and B this ADDS a `manyToMany` on A → B and B → A
|
|
133
|
+
* routed `through` J. It never removes or renames an existing relation:
|
|
134
|
+
* - an already-assigned relation with the same name → SKIP (additive-only,
|
|
135
|
+
* unchanged historical behavior);
|
|
136
|
+
* - a shadowed json/jsonb (`unknown`-typed) column → keep the historical
|
|
137
|
+
* name + warn (it worked at runtime and compiled);
|
|
138
|
+
* - a shadowed concrete-typed column → deterministic `Rel` suffix + warn
|
|
139
|
+
* instead of silently dropping the relation.
|
|
140
|
+
*/
|
|
141
|
+
export declare function addAutoManyToManyRelations(tableNames: Iterable<string>, foreignKeys: ForeignKeyEntry[], pkByTable: Map<string, string[]>, columnNamesByTable: Map<string, string[]>, relationsByTable: Map<string, Record<string, RelationDef>>, columnFieldsByTable?: Map<string, Set<string>>, unknownTypedFieldsByTable?: Map<string, Set<string>>): void;
|
|
142
|
+
/**
|
|
143
|
+
* One-stop relation derivation for the engine introspectors (SQLite / MySQL /
|
|
144
|
+
* MSSQL): filters the FK list to the introspected table set, seeds the
|
|
145
|
+
* taken-name / json-shadow maps from the engine's column metadata, and runs
|
|
146
|
+
* the SAME `buildRelationsFromForeignKeys` + `addAutoManyToManyRelations`
|
|
147
|
+
* pipeline as the Postgres introspector — so every engine derives identical
|
|
148
|
+
* relation names for the same logical schema (the engines previously carried
|
|
149
|
+
* stale copies of a retired naming scheme).
|
|
150
|
+
*/
|
|
151
|
+
export declare function deriveEngineRelations(tableNames: string[], foreignKeys: ForeignKeyEntry[], pkByTable: Map<string, string[]>, columnsByTable: Map<string, Pick<ColumnMetadata, 'name' | 'field' | 'tsType'>[]>): Map<string, Record<string, RelationDef>>;
|