turbine-orm 0.27.0 → 0.28.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 +17 -13
- package/dist/cjs/cli/config.js +20 -3
- package/dist/cjs/cli/destructive.js +47 -31
- package/dist/cjs/cli/index.js +273 -71
- package/dist/cjs/cli/mcp.js +788 -0
- package/dist/cjs/cli/migrate.js +95 -20
- package/dist/cjs/cli/studio.js +3 -2
- package/dist/cjs/client.js +267 -34
- package/dist/cjs/dialect.js +2 -0
- package/dist/cjs/generate.js +171 -7
- package/dist/cjs/index.js +4 -1
- package/dist/cjs/introspect.js +177 -4
- package/dist/cjs/query/batched-loader.js +148 -0
- package/dist/cjs/query/builder.js +714 -133
- package/dist/cjs/schema-builder.js +59 -4
- package/dist/cjs/schema-sql.js +315 -6
- package/dist/cjs/seed.js +66 -0
- package/dist/cli/config.d.ts +9 -2
- package/dist/cli/config.js +19 -3
- package/dist/cli/destructive.js +47 -31
- package/dist/cli/index.d.ts +52 -1
- package/dist/cli/index.js +272 -74
- package/dist/cli/mcp.d.ts +17 -0
- package/dist/cli/mcp.js +781 -0
- package/dist/cli/migrate.d.ts +37 -0
- package/dist/cli/migrate.js +92 -20
- package/dist/cli/studio.d.ts +3 -2
- package/dist/cli/studio.js +3 -2
- package/dist/client.d.ts +136 -1
- package/dist/client.js +267 -34
- package/dist/dialect.d.ts +17 -0
- package/dist/dialect.js +2 -0
- package/dist/generate.d.ts +17 -0
- package/dist/generate.js +171 -10
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -0
- package/dist/introspect.d.ts +20 -1
- package/dist/introspect.js +175 -4
- package/dist/query/batched-loader.d.ts +29 -2
- package/dist/query/batched-loader.js +148 -1
- package/dist/query/builder.d.ts +156 -8
- package/dist/query/builder.js +715 -134
- package/dist/query/index.d.ts +1 -1
- package/dist/query/types.d.ts +113 -8
- package/dist/schema-builder.d.ts +73 -8
- package/dist/schema-builder.js +59 -4
- package/dist/schema-sql.d.ts +67 -0
- package/dist/schema-sql.js +310 -6
- package/dist/schema.d.ts +53 -0
- package/dist/seed.d.ts +4 -0
- package/dist/seed.js +63 -0
- package/package.json +2 -3
|
@@ -51,6 +51,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
51
51
|
exports.includeKeysForBatching = includeKeysForBatching;
|
|
52
52
|
exports.stripFields = stripFields;
|
|
53
53
|
exports.neededParentKeyFields = neededParentKeyFields;
|
|
54
|
+
exports.resolveCountRelations = resolveCountRelations;
|
|
54
55
|
exports.loadRelationsBatched = loadRelationsBatched;
|
|
55
56
|
const errors_js_1 = require("../errors.js");
|
|
56
57
|
const schema_js_1 = require("../schema.js");
|
|
@@ -120,6 +121,14 @@ function neededParentKeyFields(parentMeta, withClause) {
|
|
|
120
121
|
for (const [relName, spec] of Object.entries(withClause)) {
|
|
121
122
|
if (!spec)
|
|
122
123
|
continue;
|
|
124
|
+
// `_count` needs each counted relation's parent-side key to stitch counts.
|
|
125
|
+
if (relName === '_count') {
|
|
126
|
+
for (const rel of resolveCountRelations(parentMeta, spec)) {
|
|
127
|
+
for (const col of localKeyColumns(rel))
|
|
128
|
+
fields.add(parentMeta.reverseColumnMap[col] ?? col);
|
|
129
|
+
}
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
123
132
|
const rel = parentMeta.relations[relName];
|
|
124
133
|
if (!rel)
|
|
125
134
|
continue; // unknown relation — the join path throws; let the loader surface it
|
|
@@ -140,6 +149,37 @@ function localKeyColumns(rel) {
|
|
|
140
149
|
return (0, schema_js_1.normalizeKeyColumns)(rel.foreignKey);
|
|
141
150
|
return (0, schema_js_1.normalizeKeyColumns)(rel.referenceKey);
|
|
142
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Resolve the set of to-many relations a `_count` spec selects. `true` counts
|
|
154
|
+
* every to-many relation (hasMany + manyToMany) of the table; the record form
|
|
155
|
+
* counts only the enabled names. Shared by the join builder and the batched
|
|
156
|
+
* loader so both count the exact same relations.
|
|
157
|
+
*
|
|
158
|
+
* Errors: E005 ({@link RelationError}) for an unknown relation name, E003
|
|
159
|
+
* ({@link ValidationError}) when a named relation is to-one.
|
|
160
|
+
*/
|
|
161
|
+
function resolveCountRelations(parentMeta, countSpec) {
|
|
162
|
+
const isToMany = (r) => r.type === 'hasMany' || r.type === 'manyToMany';
|
|
163
|
+
if (countSpec === true) {
|
|
164
|
+
return Object.values(parentMeta.relations).filter(isToMany);
|
|
165
|
+
}
|
|
166
|
+
const out = [];
|
|
167
|
+
for (const [relName, enabled] of Object.entries(countSpec)) {
|
|
168
|
+
if (!enabled)
|
|
169
|
+
continue;
|
|
170
|
+
const rel = parentMeta.relations[relName];
|
|
171
|
+
if (!rel) {
|
|
172
|
+
throw new errors_js_1.RelationError(`[turbine] Unknown relation "${relName}" in _count on table "${parentMeta.name}". ` +
|
|
173
|
+
`Available: ${Object.keys(parentMeta.relations).join(', ')}`);
|
|
174
|
+
}
|
|
175
|
+
if (!isToMany(rel)) {
|
|
176
|
+
throw new errors_js_1.ValidationError(`[turbine] _count is only supported for to-many relations; "${relName}" on ` +
|
|
177
|
+
`"${parentMeta.name}" is a to-one relation.`);
|
|
178
|
+
}
|
|
179
|
+
out.push(rel);
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
143
183
|
/** Stringified stitch key — robust to number/uuid/bigint type drift across a join. */
|
|
144
184
|
function keyOf(value) {
|
|
145
185
|
return String(value);
|
|
@@ -161,6 +201,11 @@ async function loadRelationsBatched(ctx, parents, withClause, timeout, depth = 0
|
|
|
161
201
|
for (const [relName, spec] of Object.entries(withClause)) {
|
|
162
202
|
if (!spec)
|
|
163
203
|
continue;
|
|
204
|
+
// Reserved `_count` key — one grouped COUNT(*) follow-up per counted relation.
|
|
205
|
+
if (relName === '_count') {
|
|
206
|
+
loads.push(loadCounts(ctx, parents, spec));
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
164
209
|
const rel = ctx.parentMeta.relations[relName];
|
|
165
210
|
if (!rel) {
|
|
166
211
|
throw new errors_js_1.ValidationError(`[turbine] Unknown relation "${relName}" on table "${ctx.parentMeta.name}". ` +
|
|
@@ -215,6 +260,7 @@ async function loadToOneOrMany(ctx, parents, rel, relName, options, timeout, dep
|
|
|
215
260
|
select: proj.select,
|
|
216
261
|
omit: proj.omit,
|
|
217
262
|
orderBy: options.orderBy,
|
|
263
|
+
skipGlobalFilters: ctx.skipGlobalFilters,
|
|
218
264
|
});
|
|
219
265
|
const result = await ctx.exec(deferred.sql, deferred.params, deferred.preparedName);
|
|
220
266
|
return deferred.transform(result);
|
|
@@ -313,6 +359,7 @@ async function loadManyToMany(ctx, parents, rel, relName, options, timeout, dept
|
|
|
313
359
|
select: proj.select,
|
|
314
360
|
omit: proj.omit,
|
|
315
361
|
orderBy: options.orderBy,
|
|
362
|
+
skipGlobalFilters: ctx.skipGlobalFilters,
|
|
316
363
|
});
|
|
317
364
|
const result = await ctx.exec(deferred.sql, deferred.params, deferred.preparedName);
|
|
318
365
|
return deferred.transform(result);
|
|
@@ -347,6 +394,107 @@ async function loadManyToMany(ctx, parents, rel, relName, options, timeout, dept
|
|
|
347
394
|
}
|
|
348
395
|
stripFields(targetsInOrder, proj.strip);
|
|
349
396
|
}
|
|
397
|
+
/**
|
|
398
|
+
* Load correlated `_count` values for the counted relations. One grouped
|
|
399
|
+
* follow-up per relation (`SELECT key, COUNT(*) … WHERE key = ANY($1) GROUP BY
|
|
400
|
+
* key`), attached onto each parent's `_count` object (0 when a parent has no
|
|
401
|
+
* matching rows) — byte-identical to the join strategy's `_count` output.
|
|
402
|
+
*/
|
|
403
|
+
async function loadCounts(ctx, parents, countSpec) {
|
|
404
|
+
const rels = resolveCountRelations(ctx.parentMeta, countSpec);
|
|
405
|
+
// Initialise every parent's `_count` up-front so the concurrent per-relation
|
|
406
|
+
// loads below (each writing its own key) never race on the object creation.
|
|
407
|
+
for (const parent of parents) {
|
|
408
|
+
if (parent._count === undefined)
|
|
409
|
+
parent._count = {};
|
|
410
|
+
}
|
|
411
|
+
await Promise.all(rels.map((rel) => loadOneCount(ctx, parents, rel)));
|
|
412
|
+
}
|
|
413
|
+
/** One grouped COUNT(*) follow-up for a single to-many relation. */
|
|
414
|
+
async function loadOneCount(ctx, parents, rel) {
|
|
415
|
+
let parentKeyCol;
|
|
416
|
+
let childTable;
|
|
417
|
+
let childKeyCol;
|
|
418
|
+
if (rel.type === 'manyToMany') {
|
|
419
|
+
const through = rel.through;
|
|
420
|
+
if (!through) {
|
|
421
|
+
throw new errors_js_1.ValidationError(`[turbine] manyToMany relation "${rel.name}" is missing its junction (\`through\`).`);
|
|
422
|
+
}
|
|
423
|
+
const sourceRef = (0, schema_js_1.normalizeKeyColumns)(rel.referenceKey);
|
|
424
|
+
const sourceJ = (0, schema_js_1.normalizeKeyColumns)(through.sourceKey);
|
|
425
|
+
if (sourceRef.length > 1 || sourceJ.length > 1) {
|
|
426
|
+
throw new errors_js_1.UnsupportedFeatureError('composite-key batched _count', 'relationLoadStrategy: "batched"', `relation "${rel.name}" — use the default 'join' strategy for composite-key m2m _count`);
|
|
427
|
+
}
|
|
428
|
+
parentKeyCol = sourceRef[0];
|
|
429
|
+
childTable = through.table;
|
|
430
|
+
childKeyCol = sourceJ[0];
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
// hasMany: child FK correlates to the parent reference key.
|
|
434
|
+
const fk = (0, schema_js_1.normalizeKeyColumns)(rel.foreignKey);
|
|
435
|
+
const rk = (0, schema_js_1.normalizeKeyColumns)(rel.referenceKey);
|
|
436
|
+
if (fk.length > 1 || rk.length > 1) {
|
|
437
|
+
throw new errors_js_1.UnsupportedFeatureError('composite-key batched _count', 'relationLoadStrategy: "batched"', `relation "${rel.name}" — use the default 'join' strategy for composite-key _count`);
|
|
438
|
+
}
|
|
439
|
+
parentKeyCol = rk[0];
|
|
440
|
+
childTable = rel.to;
|
|
441
|
+
childKeyCol = fk[0];
|
|
442
|
+
}
|
|
443
|
+
const parentKeyField = ctx.parentMeta.reverseColumnMap[parentKeyCol] ?? parentKeyCol;
|
|
444
|
+
const keys = uniqueKeys(parents, parentKeyField);
|
|
445
|
+
const counts = new Map();
|
|
446
|
+
if (keys.length > 0) {
|
|
447
|
+
const qChild = ctx.quote(childTable);
|
|
448
|
+
const qKey = ctx.quote(childKeyCol);
|
|
449
|
+
const chunks = [];
|
|
450
|
+
for (let i = 0; i < keys.length; i += MAX_RELATION_KEYS)
|
|
451
|
+
chunks.push(keys.slice(i, i + MAX_RELATION_KEYS));
|
|
452
|
+
// Global filter on the counted target, matching the join strategy so the
|
|
453
|
+
// two strategies return identical counts under a filter. hasMany filters
|
|
454
|
+
// the counted table directly; m2m counts junction rows but restricts them
|
|
455
|
+
// to junction rows whose TARGET survives the target table's filter via
|
|
456
|
+
// EXISTS — mirroring buildRelationCountExpr's EXISTS-on-target (which also
|
|
457
|
+
// skips the filter when the junction targetKey arity doesn't match the
|
|
458
|
+
// target PK). Rendered after the $1 key array.
|
|
459
|
+
let gf = null;
|
|
460
|
+
if (ctx.tableGlobalFilter) {
|
|
461
|
+
if (rel.type === 'manyToMany' && rel.through) {
|
|
462
|
+
const targetKeys = (0, schema_js_1.normalizeKeyColumns)(rel.through.targetKey);
|
|
463
|
+
const targetMeta = ctx.schema.tables[rel.to];
|
|
464
|
+
const pk = targetMeta?.primaryKey ?? [];
|
|
465
|
+
if (targetMeta && pk.length > 0 && pk.length === targetKeys.length) {
|
|
466
|
+
const targetGf = ctx.tableGlobalFilter(rel.to, 't', 1);
|
|
467
|
+
if (targetGf) {
|
|
468
|
+
const join = targetKeys.map((jc, i) => `t.${ctx.quote(pk[i])} = ${qChild}.${ctx.quote(jc)}`).join(' AND ');
|
|
469
|
+
gf = {
|
|
470
|
+
clause: `EXISTS (SELECT 1 FROM ${ctx.quote(rel.to)} t WHERE ${join} AND ${targetGf.clause})`,
|
|
471
|
+
params: targetGf.params,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
else if (rel.type !== 'manyToMany') {
|
|
477
|
+
gf = ctx.tableGlobalFilter(childTable, qChild, 1);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
const gfAnd = gf ? ` AND ${gf.clause}` : '';
|
|
481
|
+
const results = await Promise.all(chunks.map((chunk) => {
|
|
482
|
+
const params = [ctx.inClauseParam(chunk), ...(gf ? gf.params : [])];
|
|
483
|
+
const predicate = ctx.buildInClause(`${qChild}.${qKey}`, ctx.paramPlaceholder(1), false);
|
|
484
|
+
const sql = `SELECT ${qChild}.${qKey} AS "k", COUNT(*) AS "c" FROM ${qChild} ` +
|
|
485
|
+
`WHERE ${predicate}${gfAnd} GROUP BY ${qChild}.${qKey}`;
|
|
486
|
+
return ctx.exec(sql, params);
|
|
487
|
+
}));
|
|
488
|
+
for (const { rows } of results) {
|
|
489
|
+
for (const row of rows) {
|
|
490
|
+
counts.set(keyOf(row.k), Number(row.c));
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
for (const parent of parents) {
|
|
495
|
+
parent._count[rel.name] = counts.get(keyOf(parent[parentKeyField])) ?? 0;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
350
498
|
// ---------------------------------------------------------------------------
|
|
351
499
|
// Small helpers
|
|
352
500
|
// ---------------------------------------------------------------------------
|