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
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* turbine-orm — defineSchema() → SchemaMetadata bridge
|
|
4
|
+
*
|
|
5
|
+
* Converts a code-first {@link SchemaDef} (the output of `defineSchema()`)
|
|
6
|
+
* into the runtime {@link SchemaMetadata} shape that the query builder,
|
|
7
|
+
* `TurbineClient`, and the non-SQL engines consume — without touching a
|
|
8
|
+
* live database.
|
|
9
|
+
*
|
|
10
|
+
* Why this exists: the historical converter path (`introspect()` +
|
|
11
|
+
* `generate()`) requires a running SQL database, but code-first engines
|
|
12
|
+
* (PowDB, in-memory SQLite bootstraps, tests) only have the `SchemaDef`.
|
|
13
|
+
* `schemaDefToMetadata()` is the pure-function equivalent: its output
|
|
14
|
+
* matches what `turbine generate` would emit into `metadata.ts` for the
|
|
15
|
+
* same schema, minus the pieces only a live catalog can know (real index
|
|
16
|
+
* names, constraint names, view flags).
|
|
17
|
+
*
|
|
18
|
+
* Parity notes (ground truth = introspect.ts + generate.ts):
|
|
19
|
+
* - Relations are derived from `references:` exactly like introspection
|
|
20
|
+
* derives them from foreign keys: a `belongsTo` on the child table and
|
|
21
|
+
* a `hasMany` on the parent, with the same disambiguation rules when
|
|
22
|
+
* multiple FKs point at the same target.
|
|
23
|
+
* - Pure junction tables (2-column composite PK that IS the two
|
|
24
|
+
* single-column FKs to two distinct tables, no payload columns) get
|
|
25
|
+
* the same conservative auto-`manyToMany` treatment as introspection.
|
|
26
|
+
* - Explicit `manyToMany` declarations on the SchemaDef are merged via
|
|
27
|
+
* {@link applyManyToManyRelations} (additive, never clobbering).
|
|
28
|
+
* - `indexes` is always `[]` — SchemaDef cannot express indexes, and an
|
|
29
|
+
* empty list keeps `schemaHasIndexInfo()` false so the index advisor
|
|
30
|
+
* and the dev-mode missing-index warning stay silent instead of
|
|
31
|
+
* producing blanket false positives.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* import { defineSchema, schemaDefToMetadata } from 'turbine-orm';
|
|
36
|
+
*
|
|
37
|
+
* const def = defineSchema({
|
|
38
|
+
* users: { id: { type: 'serial', primaryKey: true }, name: { type: 'text', notNull: true } },
|
|
39
|
+
* posts: { id: { type: 'serial', primaryKey: true },
|
|
40
|
+
* userId: { type: 'integer', notNull: true, references: 'users.id' } },
|
|
41
|
+
* });
|
|
42
|
+
* const metadata = schemaDefToMetadata(def);
|
|
43
|
+
* // → usable anywhere SchemaMetadata is expected (e.g. turbinePowDB, TurbineClient)
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
+
exports.schemaDefToMetadata = schemaDefToMetadata;
|
|
48
|
+
const introspect_js_1 = require("./introspect.js");
|
|
49
|
+
const schema_js_1 = require("./schema.js");
|
|
50
|
+
const schema_builder_js_1 = require("./schema-builder.js");
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// DDL type → Postgres udt_name (what introspection reads from the catalog)
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
const DDL_TO_UDT = {
|
|
55
|
+
SERIAL: 'int4',
|
|
56
|
+
BIGSERIAL: 'int8',
|
|
57
|
+
BIGINT: 'int8',
|
|
58
|
+
INTEGER: 'int4',
|
|
59
|
+
SMALLINT: 'int2',
|
|
60
|
+
TEXT: 'text',
|
|
61
|
+
VARCHAR: 'varchar',
|
|
62
|
+
BOOLEAN: 'bool',
|
|
63
|
+
TIMESTAMPTZ: 'timestamptz',
|
|
64
|
+
DATE: 'date',
|
|
65
|
+
JSONB: 'jsonb',
|
|
66
|
+
UUID: 'uuid',
|
|
67
|
+
REAL: 'float4',
|
|
68
|
+
'DOUBLE PRECISION': 'float8',
|
|
69
|
+
NUMERIC: 'numeric',
|
|
70
|
+
BYTEA: 'bytea',
|
|
71
|
+
};
|
|
72
|
+
/** Resolve a ColumnConfig's Postgres base type name (udt_name form). */
|
|
73
|
+
function udtName(config) {
|
|
74
|
+
if (config.type === 'ENUM')
|
|
75
|
+
return config.enumName ?? 'text';
|
|
76
|
+
if (config.type === 'VECTOR')
|
|
77
|
+
return 'vector';
|
|
78
|
+
return DDL_TO_UDT[config.type];
|
|
79
|
+
}
|
|
80
|
+
/** Server-generated (sequence-backed) types — pg's `nextval(...)` default. */
|
|
81
|
+
function isSerialType(type) {
|
|
82
|
+
return type === 'SERIAL' || type === 'BIGSERIAL';
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Resolve the raw column part of a `references: 'table.column'` target to a
|
|
86
|
+
* snake_case column name — accepting either the camelCase field name or the
|
|
87
|
+
* snake_case DDL name, mirroring how schema-sql.ts accepts both table forms.
|
|
88
|
+
*/
|
|
89
|
+
function resolveColumnName(raw, target) {
|
|
90
|
+
if (target) {
|
|
91
|
+
const viaField = target.fieldToColumn.get(raw);
|
|
92
|
+
if (viaField !== undefined)
|
|
93
|
+
return viaField;
|
|
94
|
+
if (target.columnNames.has(raw))
|
|
95
|
+
return raw;
|
|
96
|
+
}
|
|
97
|
+
return (0, schema_js_1.camelToSnake)(raw);
|
|
98
|
+
}
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// The converter
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
/**
|
|
103
|
+
* Convert a code-first {@link SchemaDef} into runtime {@link SchemaMetadata}.
|
|
104
|
+
*
|
|
105
|
+
* Pure function — no database connection, no side effects, input untouched.
|
|
106
|
+
* The output is shaped identically to the `SCHEMA` constant `turbine generate`
|
|
107
|
+
* emits from introspection, so it can be handed to any consumer that expects
|
|
108
|
+
* introspected metadata: `new TurbineClient(config, metadata)`,
|
|
109
|
+
* `turbinePowDB(..., metadata)`, `QueryInterface`, the index advisor, etc.
|
|
110
|
+
*
|
|
111
|
+
* What maps:
|
|
112
|
+
* - Columns → full {@link ColumnMetadata} (snake_case name, camelCase field,
|
|
113
|
+
* pg type names, TS types, nullability, defaults, `isGenerated` for
|
|
114
|
+
* serial/bigserial, array + varchar length info, date-column tracking).
|
|
115
|
+
* - Column-level `primaryKey` and table-level composite `primaryKey`.
|
|
116
|
+
* - `unique: true` columns → single-column `uniqueColumns` entries.
|
|
117
|
+
* - `references:` FKs → `belongsTo` (child) + `hasMany` (parent) relations,
|
|
118
|
+
* including `onDelete`/`onUpdate` actions (the `'no action'` default is
|
|
119
|
+
* omitted, matching introspection).
|
|
120
|
+
* - Pure junction tables → auto-detected `manyToMany` relations (same
|
|
121
|
+
* conservative rules as introspection).
|
|
122
|
+
* - Explicit `manyToMany` declarations → merged additively.
|
|
123
|
+
* - Schema-level `enums`.
|
|
124
|
+
*
|
|
125
|
+
* What SchemaDef cannot express (and how it degrades):
|
|
126
|
+
* - Indexes → every table gets `indexes: []`, which keeps
|
|
127
|
+
* `schemaHasIndexInfo()` false so index-advisor consumers produce no
|
|
128
|
+
* false positives on code-first metadata.
|
|
129
|
+
* - Views → never marked (`isView` is introspection-only).
|
|
130
|
+
* - Composite foreign keys → `references:` is single-column by design.
|
|
131
|
+
*/
|
|
132
|
+
function schemaDefToMetadata(def) {
|
|
133
|
+
// ----- Pass 1: resolve every table's snake_case names for FK lookups -----
|
|
134
|
+
// Lookup accepts the accessor key (camelCase), the DDL name (snake_case),
|
|
135
|
+
// and the explicit `accessor` field — same tolerance as schema-sql.ts.
|
|
136
|
+
const lookup = new Map();
|
|
137
|
+
for (const [key, tableDef] of Object.entries(def.tables)) {
|
|
138
|
+
const fieldToColumn = new Map();
|
|
139
|
+
const columnNames = new Set();
|
|
140
|
+
for (const field of Object.keys(tableDef.columns)) {
|
|
141
|
+
const snake = (0, schema_js_1.camelToSnake)(field);
|
|
142
|
+
fieldToColumn.set(field, snake);
|
|
143
|
+
columnNames.add(snake);
|
|
144
|
+
}
|
|
145
|
+
const resolved = { name: tableDef.name, fieldToColumn, columnNames };
|
|
146
|
+
lookup.set(key, resolved);
|
|
147
|
+
if (tableDef.name)
|
|
148
|
+
lookup.set(tableDef.name, resolved);
|
|
149
|
+
if (tableDef.accessor)
|
|
150
|
+
lookup.set(tableDef.accessor, resolved);
|
|
151
|
+
}
|
|
152
|
+
// ----- Pass 2: collect resolved single-column FKs -----
|
|
153
|
+
const foreignKeys = [];
|
|
154
|
+
for (const tableDef of Object.values(def.tables)) {
|
|
155
|
+
for (const [field, config] of Object.entries(tableDef.columns)) {
|
|
156
|
+
if (!config.referencesTarget)
|
|
157
|
+
continue;
|
|
158
|
+
const parts = config.referencesTarget.split('.');
|
|
159
|
+
if (parts.length !== 2)
|
|
160
|
+
continue;
|
|
161
|
+
const target = lookup.get(parts[0]);
|
|
162
|
+
// Reference to a table outside this SchemaDef — skip, exactly like
|
|
163
|
+
// introspection skips FKs whose target is excluded from the table set.
|
|
164
|
+
if (!target)
|
|
165
|
+
continue;
|
|
166
|
+
foreignKeys.push({
|
|
167
|
+
sourceTable: tableDef.name,
|
|
168
|
+
sourceColumn: (0, schema_js_1.camelToSnake)(field),
|
|
169
|
+
targetTable: target.name,
|
|
170
|
+
targetColumn: resolveColumnName(parts[1], target),
|
|
171
|
+
onDelete: config.onDelete ?? undefined,
|
|
172
|
+
onUpdate: config.onUpdate ?? undefined,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// ----- Pass 3: derive belongsTo / hasMany relations (SHARED with introspect)
|
|
177
|
+
//
|
|
178
|
+
// Delegates to the exact same builder introspection uses
|
|
179
|
+
// (`buildRelationsFromForeignKeys`), so the same logical schema yields
|
|
180
|
+
// IDENTICAL relation names whether it arrives via `defineSchema()` or a live
|
|
181
|
+
// catalog: legacy-first naming, per-column disambiguation for several FKs to
|
|
182
|
+
// the same target, and collision resolution against scalar column fields
|
|
183
|
+
// (json/jsonb `unknown`-typed shadows keep the historical name). The old
|
|
184
|
+
// local reimplementation had NO collision guard — `posts.user` (text) +
|
|
185
|
+
// `userId references users.id` produced a relation `user` that shadowed the
|
|
186
|
+
// scalar, and two FKs deriving the same name silently clobbered each other
|
|
187
|
+
// (N-4).
|
|
188
|
+
//
|
|
189
|
+
// Constraint names are synthesized in pg's default `<table>_<column>_fkey`
|
|
190
|
+
// form; they only feed the referential-action lookup and composite-FK
|
|
191
|
+
// naming (never hit here — `references:` is single-column by design).
|
|
192
|
+
const fkEntries = [];
|
|
193
|
+
const fkActions = new Map();
|
|
194
|
+
for (const fk of foreignKeys) {
|
|
195
|
+
const constraintName = `${fk.sourceTable}_${fk.sourceColumn}_fkey`;
|
|
196
|
+
fkEntries.push({
|
|
197
|
+
sourceTable: fk.sourceTable,
|
|
198
|
+
sourceColumns: [fk.sourceColumn],
|
|
199
|
+
targetTable: fk.targetTable,
|
|
200
|
+
targetColumns: [fk.targetColumn],
|
|
201
|
+
constraintName,
|
|
202
|
+
});
|
|
203
|
+
fkActions.set(constraintName, {
|
|
204
|
+
onDelete: fk.onDelete ?? 'no action',
|
|
205
|
+
onUpdate: fk.onUpdate ?? 'no action',
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
// Taken-name seeds: every table's camelCase column fields (the SchemaDef
|
|
209
|
+
// keys ARE the fields) + which of them are json/jsonb (`unknown`-typed).
|
|
210
|
+
const columnFieldsByTable = new Map();
|
|
211
|
+
const unknownTypedFieldsByTable = new Map();
|
|
212
|
+
for (const tableDef of Object.values(def.tables)) {
|
|
213
|
+
const fields = Object.keys(tableDef.columns);
|
|
214
|
+
columnFieldsByTable.set(tableDef.name, new Set(fields));
|
|
215
|
+
// ENUM columns map to concrete union types in the generated layer, so
|
|
216
|
+
// only genuine json/jsonb (`unknown`-typed) columns qualify as shadows.
|
|
217
|
+
unknownTypedFieldsByTable.set(tableDef.name, new Set(fields.filter((f) => {
|
|
218
|
+
const config = tableDef.columns[f];
|
|
219
|
+
if (config.type === 'ENUM')
|
|
220
|
+
return false;
|
|
221
|
+
const wire = config.isArray ? `_${udtName(config)}` : udtName(config);
|
|
222
|
+
return (0, introspect_js_1.isUnknownTsType)((0, schema_js_1.pgTypeToTs)(wire, false));
|
|
223
|
+
})));
|
|
224
|
+
}
|
|
225
|
+
const relationsByTable = (0, introspect_js_1.buildRelationsFromForeignKeys)(fkEntries, columnFieldsByTable, fkActions, unknownTypedFieldsByTable);
|
|
226
|
+
// ----- Pass 4: conservative junction auto-m2m (SHARED with introspect) ---
|
|
227
|
+
// Same shared detector + naming/collision rules as introspection: a table J
|
|
228
|
+
// is a PURE junction only when its PK is exactly the two single-column FKs
|
|
229
|
+
// to two DISTINCT tables and it has no payload columns.
|
|
230
|
+
const pkByTable = new Map();
|
|
231
|
+
const columnNamesByTable = new Map();
|
|
232
|
+
for (const tableDef of Object.values(def.tables)) {
|
|
233
|
+
const pkFields = tableDef.primaryKey && tableDef.primaryKey.length > 0
|
|
234
|
+
? [...tableDef.primaryKey]
|
|
235
|
+
: Object.entries(tableDef.columns)
|
|
236
|
+
.filter(([, c]) => c.isPrimaryKey)
|
|
237
|
+
.map(([f]) => f);
|
|
238
|
+
pkByTable.set(tableDef.name, pkFields.map(schema_js_1.camelToSnake));
|
|
239
|
+
columnNamesByTable.set(tableDef.name, Object.keys(tableDef.columns).map(schema_js_1.camelToSnake));
|
|
240
|
+
}
|
|
241
|
+
(0, introspect_js_1.addAutoManyToManyRelations)(Object.values(def.tables).map((t) => t.name), fkEntries, pkByTable, columnNamesByTable, relationsByTable, columnFieldsByTable, unknownTypedFieldsByTable);
|
|
242
|
+
// ----- Pass 5: assemble TableMetadata -----
|
|
243
|
+
const tables = {};
|
|
244
|
+
for (const tableDef of Object.values(def.tables)) {
|
|
245
|
+
const columns = [];
|
|
246
|
+
const columnMap = {};
|
|
247
|
+
const reverseColumnMap = {};
|
|
248
|
+
const dateColumns = new Set();
|
|
249
|
+
const dialectTypes = {};
|
|
250
|
+
const pgTypes = {};
|
|
251
|
+
const allColumns = [];
|
|
252
|
+
const primaryKey = [];
|
|
253
|
+
const uniqueColumns = [];
|
|
254
|
+
for (const [field, config] of Object.entries(tableDef.columns)) {
|
|
255
|
+
const name = (0, schema_js_1.camelToSnake)(field);
|
|
256
|
+
const base = udtName(config);
|
|
257
|
+
// Introspection reports array columns with pg's leading-underscore
|
|
258
|
+
// udt_name (`_text`), which pgTypeToTs also understands.
|
|
259
|
+
const wireType = config.isArray ? `_${base}` : base;
|
|
260
|
+
const serial = isSerialType(config.type);
|
|
261
|
+
// PK members and serials are NOT NULL in Postgres regardless of flags.
|
|
262
|
+
const nullable = !(config.isPrimaryKey || config.isNotNull || serial);
|
|
263
|
+
const col = {
|
|
264
|
+
name,
|
|
265
|
+
field,
|
|
266
|
+
dialectType: wireType,
|
|
267
|
+
pgType: wireType,
|
|
268
|
+
tsType: (0, schema_js_1.pgTypeToTs)(wireType, nullable),
|
|
269
|
+
nullable,
|
|
270
|
+
hasDefault: config.defaultValue != null || serial,
|
|
271
|
+
isArray: config.isArray,
|
|
272
|
+
arrayType: (0, schema_js_1.pgArrayType)(base),
|
|
273
|
+
pgArrayType: (0, schema_js_1.pgArrayType)(base),
|
|
274
|
+
...(serial ? { isGenerated: true } : {}),
|
|
275
|
+
...(config.maxLength != null ? { maxLength: config.maxLength } : {}),
|
|
276
|
+
};
|
|
277
|
+
columns.push(col);
|
|
278
|
+
columnMap[field] = name;
|
|
279
|
+
reverseColumnMap[name] = field;
|
|
280
|
+
allColumns.push(name);
|
|
281
|
+
dialectTypes[name] = wireType;
|
|
282
|
+
pgTypes[name] = wireType;
|
|
283
|
+
if ((0, schema_js_1.isDateType)(base))
|
|
284
|
+
dateColumns.add(name);
|
|
285
|
+
if (config.isPrimaryKey)
|
|
286
|
+
primaryKey.push(name);
|
|
287
|
+
if (config.isUnique)
|
|
288
|
+
uniqueColumns.push([name]);
|
|
289
|
+
}
|
|
290
|
+
// Table-level composite PK takes precedence (defineSchema already cleared
|
|
291
|
+
// the column-level flags for its members).
|
|
292
|
+
const pk = tableDef.primaryKey && tableDef.primaryKey.length > 0 ? tableDef.primaryKey.map(schema_js_1.camelToSnake) : primaryKey;
|
|
293
|
+
tables[tableDef.name] = {
|
|
294
|
+
name: tableDef.name,
|
|
295
|
+
columns,
|
|
296
|
+
columnMap,
|
|
297
|
+
reverseColumnMap,
|
|
298
|
+
dateColumns,
|
|
299
|
+
dialectTypes,
|
|
300
|
+
pgTypes,
|
|
301
|
+
allColumns,
|
|
302
|
+
primaryKey: pk,
|
|
303
|
+
uniqueColumns,
|
|
304
|
+
relations: relationsByTable.get(tableDef.name) ?? {},
|
|
305
|
+
// SchemaDef cannot express indexes. An empty list keeps
|
|
306
|
+
// schemaHasIndexInfo() false → no index-advisor false positives.
|
|
307
|
+
indexes: [],
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
const enums = {};
|
|
311
|
+
for (const [name, labels] of Object.entries(def.enums ?? {})) {
|
|
312
|
+
enums[name] = [...labels];
|
|
313
|
+
}
|
|
314
|
+
// ----- Pass 6: merge explicit manyToMany declarations (additive) ---------
|
|
315
|
+
return (0, schema_builder_js_1.applyManyToManyRelations)({ tables, enums }, def);
|
|
316
|
+
}
|
package/dist/cjs/sqlite.js
CHANGED
|
@@ -56,6 +56,7 @@ const node_module_1 = require("node:module");
|
|
|
56
56
|
const client_js_1 = require("./client.js");
|
|
57
57
|
const dialect_js_1 = require("./dialect.js");
|
|
58
58
|
const errors_js_1 = require("./errors.js");
|
|
59
|
+
const introspect_js_1 = require("./introspect.js");
|
|
59
60
|
const schema_js_1 = require("./schema.js");
|
|
60
61
|
let cachedDatabaseSync;
|
|
61
62
|
/**
|
|
@@ -645,95 +646,13 @@ function introspectSqliteDatabase(db, options = {}) {
|
|
|
645
646
|
indexesByTable.set(tableName, idxMeta);
|
|
646
647
|
uniqueByTable.set(tableName, uniques);
|
|
647
648
|
}
|
|
648
|
-
// ----- Build relations from foreign keys (belongsTo + hasMany) -----
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
const relationsByTable =
|
|
655
|
-
for (const fk of foreignKeys) {
|
|
656
|
-
const needsDisambiguation = (fkCounts.get(`${fk.sourceTable}->${fk.targetTable}`) ?? 0) > 1;
|
|
657
|
-
const foreignKey = fk.sourceColumns.length === 1 ? fk.sourceColumns[0] : fk.sourceColumns;
|
|
658
|
-
const referenceKey = fk.targetColumns.length === 1 ? fk.targetColumns[0] : fk.targetColumns;
|
|
659
|
-
const belongsToName = needsDisambiguation && fk.sourceColumns.length === 1
|
|
660
|
-
? (0, schema_js_1.snakeToCamel)(fk.sourceColumns[0].replace(/_id$/, ''))
|
|
661
|
-
: (0, schema_js_1.singularize)((0, schema_js_1.snakeToCamel)(fk.targetTable));
|
|
662
|
-
if (!relationsByTable.has(fk.sourceTable))
|
|
663
|
-
relationsByTable.set(fk.sourceTable, {});
|
|
664
|
-
relationsByTable.get(fk.sourceTable)[belongsToName] = {
|
|
665
|
-
type: 'belongsTo',
|
|
666
|
-
name: belongsToName,
|
|
667
|
-
from: fk.sourceTable,
|
|
668
|
-
to: fk.targetTable,
|
|
669
|
-
foreignKey,
|
|
670
|
-
referenceKey,
|
|
671
|
-
};
|
|
672
|
-
const hasManyName = needsDisambiguation && fk.sourceColumns.length === 1
|
|
673
|
-
? (0, schema_js_1.snakeToCamel)(`${fk.sourceTable}_by_${fk.sourceColumns[0].replace(/_id$/, '')}`)
|
|
674
|
-
: (0, schema_js_1.snakeToCamel)(fk.sourceTable);
|
|
675
|
-
if (!relationsByTable.has(fk.targetTable))
|
|
676
|
-
relationsByTable.set(fk.targetTable, {});
|
|
677
|
-
relationsByTable.get(fk.targetTable)[hasManyName] = {
|
|
678
|
-
type: 'hasMany',
|
|
679
|
-
name: hasManyName,
|
|
680
|
-
from: fk.targetTable,
|
|
681
|
-
to: fk.sourceTable,
|
|
682
|
-
foreignKey,
|
|
683
|
-
referenceKey,
|
|
684
|
-
};
|
|
685
|
-
}
|
|
686
|
-
// ----- Conservative many-to-many auto-detection (additive) -----
|
|
687
|
-
// A table J is a pure junction iff: PK is exactly two columns, exactly two
|
|
688
|
-
// single-column FKs whose source columns ARE the PK, two distinct target
|
|
689
|
-
// tables, and no payload columns. Mirrors the Postgres introspector.
|
|
690
|
-
for (const tableName of tableNames) {
|
|
691
|
-
const pk = pkByTable.get(tableName) ?? [];
|
|
692
|
-
if (pk.length !== 2)
|
|
693
|
-
continue;
|
|
694
|
-
const tableFks = foreignKeys.filter((fk) => fk.sourceTable === tableName);
|
|
695
|
-
if (tableFks.length !== 2)
|
|
696
|
-
continue;
|
|
697
|
-
if (tableFks.some((fk) => fk.sourceColumns.length !== 1))
|
|
698
|
-
continue;
|
|
699
|
-
const fkCols = tableFks.map((fk) => fk.sourceColumns[0]);
|
|
700
|
-
const pkSet = new Set(pk);
|
|
701
|
-
if (!fkCols.every((c) => pkSet.has(c)))
|
|
702
|
-
continue;
|
|
703
|
-
if (new Set(fkCols).size !== 2)
|
|
704
|
-
continue;
|
|
705
|
-
const [fkA, fkB] = tableFks;
|
|
706
|
-
if (fkA.targetTable === fkB.targetTable)
|
|
707
|
-
continue;
|
|
708
|
-
const jCols = (columnsByTable.get(tableName) ?? []).map((c) => c.name);
|
|
709
|
-
if (jCols.length !== 2)
|
|
710
|
-
continue;
|
|
711
|
-
const addM2M = (self, other) => {
|
|
712
|
-
const sourceTbl = self.targetTable;
|
|
713
|
-
const targetTbl = other.targetTable;
|
|
714
|
-
const relName = (0, schema_js_1.snakeToCamel)(targetTbl);
|
|
715
|
-
if (!relationsByTable.has(sourceTbl))
|
|
716
|
-
relationsByTable.set(sourceTbl, {});
|
|
717
|
-
const existing = relationsByTable.get(sourceTbl);
|
|
718
|
-
if (existing[relName])
|
|
719
|
-
return;
|
|
720
|
-
existing[relName] = {
|
|
721
|
-
type: 'manyToMany',
|
|
722
|
-
name: relName,
|
|
723
|
-
from: sourceTbl,
|
|
724
|
-
to: targetTbl,
|
|
725
|
-
referenceKey: self.targetColumns.length === 1 ? self.targetColumns[0] : self.targetColumns,
|
|
726
|
-
foreignKey: self.targetColumns.length === 1 ? self.targetColumns[0] : self.targetColumns,
|
|
727
|
-
through: {
|
|
728
|
-
table: tableName,
|
|
729
|
-
sourceKey: self.sourceColumns[0],
|
|
730
|
-
targetKey: other.sourceColumns[0],
|
|
731
|
-
},
|
|
732
|
-
};
|
|
733
|
-
};
|
|
734
|
-
addM2M(fkA, fkB);
|
|
735
|
-
addM2M(fkB, fkA);
|
|
736
|
-
}
|
|
649
|
+
// ----- Build relations from foreign keys (belongsTo + hasMany + m2m) -----
|
|
650
|
+
// Delegated to the SHARED introspection pipeline (introspect.ts) so SQLite
|
|
651
|
+
// derives IDENTICAL relation names to the Postgres introspector for the
|
|
652
|
+
// same logical schema — legacy-first naming, per-column disambiguation,
|
|
653
|
+
// collision resolution against scalar column fields, and the conservative
|
|
654
|
+
// pure-junction manyToMany auto-detection included.
|
|
655
|
+
const relationsByTable = (0, introspect_js_1.deriveEngineRelations)(tableNames, foreignKeys, pkByTable, columnsByTable);
|
|
737
656
|
// ----- Assemble TableMetadata -----
|
|
738
657
|
const tables = {};
|
|
739
658
|
for (const tableName of tableNames) {
|
package/dist/cli/index.d.ts
CHANGED
package/dist/cli/index.js
CHANGED
|
@@ -102,6 +102,9 @@ export function parseArgs(argv = process.argv.slice(2)) {
|
|
|
102
102
|
case '--include-views':
|
|
103
103
|
result.includeViews = true;
|
|
104
104
|
break;
|
|
105
|
+
case '--no-timestamp':
|
|
106
|
+
result.noTimestamp = true;
|
|
107
|
+
break;
|
|
105
108
|
case '--allow-destructive':
|
|
106
109
|
result.allowDestructive = true;
|
|
107
110
|
break;
|
|
@@ -518,6 +521,7 @@ async function cmdGenerate(args, config) {
|
|
|
518
521
|
outDir: config.out,
|
|
519
522
|
connectionString: url,
|
|
520
523
|
zod: args.zod,
|
|
524
|
+
noTimestamp: args.noTimestamp,
|
|
521
525
|
});
|
|
522
526
|
genSpinner.succeed(`Generated ${bold(String(result.files.length))} files in ${elapsed(startTime)}`);
|
|
523
527
|
// List files
|
|
@@ -1464,6 +1468,7 @@ function showGenerateHelp() {
|
|
|
1464
1468
|
console.log(` ${cyan('--exclude')} ${dim('<tables>')} Comma-separated tables to exclude`);
|
|
1465
1469
|
console.log(` ${cyan('--zod')} Also emit ${cyan('zod.ts')} validation schemas ${dim('(needs the zod dep)')}`);
|
|
1466
1470
|
console.log(` ${cyan('--include-views')} Include views + materialized views as read-only entities`);
|
|
1471
|
+
console.log(` ${cyan('--no-timestamp')} Omit the ${dim('Generated at:')} header line ${dim('(reproducible, diff-stable output)')}`);
|
|
1467
1472
|
console.log(` ${cyan('--allow-empty')} Generate even when introspection matches 0 tables`);
|
|
1468
1473
|
newline();
|
|
1469
1474
|
}
|
package/dist/cli/mcp.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Readable, Writable } from 'node:stream';
|
|
2
|
+
import { type ColumnMetadata, type RelationDef } from '../schema.js';
|
|
2
3
|
export interface McpServerOptions {
|
|
3
4
|
url: string;
|
|
4
5
|
schema: string;
|
|
@@ -14,4 +15,21 @@ export interface McpServerHandle {
|
|
|
14
15
|
dispose(): Promise<void>;
|
|
15
16
|
}
|
|
16
17
|
export declare function startMcpServer(options: McpServerOptions, transport?: McpTransport): McpServerHandle;
|
|
18
|
+
interface ForeignKeyRow {
|
|
19
|
+
source_table: string;
|
|
20
|
+
source_column: string;
|
|
21
|
+
target_table: string;
|
|
22
|
+
target_column: string;
|
|
23
|
+
constraint_name: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Group raw FK rows into constraint-level entries and delegate relation
|
|
27
|
+
* naming to the SHARED introspection builder (`buildRelationsFromForeignKeys`
|
|
28
|
+
* + `addAutoManyToManyRelations` in ../introspect.ts). MCP previously carried
|
|
29
|
+
* a stale copy of a retired naming scheme, so `turbine mcp` and `turbine
|
|
30
|
+
* generate` derived DIFFERENT relation names from the same database (N-3).
|
|
31
|
+
* Exported for the parity unit test.
|
|
32
|
+
*/
|
|
33
|
+
export declare function buildRelations(tableNames: string[], columnsByTable: Map<string, ColumnMetadata[]>, pkByTable: Map<string, string[]>, rows: ForeignKeyRow[], enums?: Record<string, string[]>): Map<string, Record<string, RelationDef>>;
|
|
17
34
|
export declare function runMcpServer(options: McpServerOptions): Promise<void>;
|
|
35
|
+
export {};
|
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
|
@@ -149,8 +149,12 @@ export interface TurbineConfig {
|
|
|
149
149
|
logging?: boolean;
|
|
150
150
|
/** Default LIMIT applied to findMany() when no limit is specified (opt-in, default: undefined) */
|
|
151
151
|
defaultLimit?: number;
|
|
152
|
-
/**
|
|
153
|
-
|
|
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>;
|
|
154
158
|
/**
|
|
155
159
|
* Interpret Postgres `timestamp` (without time zone) values as UTC — both
|
|
156
160
|
* at the driver level (OID 1114 type parser, registered only when Turbine
|