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