turbine-orm 0.75.0 → 0.76.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 +48 -7
- package/dist/cjs/cli/compile-query.d.ts +22 -2
- package/dist/cjs/cli/compile-query.js +49 -5
- package/dist/cjs/cli/config.d.ts +2 -0
- package/dist/cjs/cli/config.js +1 -1
- package/dist/cjs/cli/destructive.js +78 -43
- package/dist/cjs/cli/index.d.ts +95 -1
- package/dist/cjs/cli/index.js +609 -145
- package/dist/cjs/cli/mcp.js +30 -1
- package/dist/cjs/cli/pii-predicate-guard.d.ts +25 -0
- package/dist/cjs/cli/pii-predicate-guard.js +72 -12
- package/dist/cjs/cli/rate-limit.js +38 -1
- package/dist/cjs/cli/studio.js +26 -5
- package/dist/cjs/cli/ui.d.ts +33 -0
- package/dist/cjs/cli/ui.js +53 -7
- package/dist/cjs/client.d.ts +13 -1
- package/dist/cjs/client.js +1 -1
- package/dist/cjs/errors.d.ts +12 -1
- package/dist/cjs/errors.js +11 -2
- package/dist/cjs/generate.d.ts +26 -0
- package/dist/cjs/generate.js +174 -27
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/introspect.d.ts +17 -0
- package/dist/cjs/introspect.js +100 -1
- package/dist/cjs/mssql.d.ts +18 -0
- package/dist/cjs/mssql.js +20 -1
- package/dist/cjs/pipeline.js +44 -6
- package/dist/cjs/powql.js +51 -17
- package/dist/cjs/query/batched-loader.js +3 -3
- package/dist/cjs/query/builder.js +1 -1
- package/dist/cjs/query/relations.d.ts +5 -0
- package/dist/cjs/query/relations.js +141 -69
- package/dist/cjs/query/utils.d.ts +13 -0
- package/dist/cjs/query/utils.js +16 -0
- package/dist/cjs/serverless.d.ts +1 -1
- package/dist/cjs/serverless.js +1 -1
- package/dist/cjs/sqlite.d.ts +33 -1
- package/dist/cjs/sqlite.js +84 -3
- package/dist/cli/compile-query.d.ts +22 -2
- package/dist/cli/compile-query.js +50 -6
- package/dist/cli/config.d.ts +2 -0
- package/dist/cli/config.js +1 -1
- package/dist/cli/destructive.js +78 -43
- package/dist/cli/index.d.ts +95 -1
- package/dist/cli/index.js +604 -147
- package/dist/cli/mcp.js +30 -1
- package/dist/cli/pii-predicate-guard.d.ts +25 -0
- package/dist/cli/pii-predicate-guard.js +73 -13
- package/dist/cli/rate-limit.js +38 -1
- package/dist/cli/studio.js +27 -6
- package/dist/cli/ui.d.ts +33 -0
- package/dist/cli/ui.js +51 -7
- package/dist/client.d.ts +13 -1
- package/dist/client.js +1 -1
- package/dist/errors.d.ts +12 -1
- package/dist/errors.js +11 -2
- package/dist/generate.d.ts +26 -0
- package/dist/generate.js +172 -27
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/introspect.d.ts +17 -0
- package/dist/introspect.js +98 -1
- package/dist/mssql.d.ts +18 -0
- package/dist/mssql.js +20 -1
- package/dist/pipeline.js +44 -6
- package/dist/powql.js +53 -19
- package/dist/query/batched-loader.js +4 -4
- package/dist/query/builder.js +2 -2
- package/dist/query/relations.d.ts +5 -0
- package/dist/query/relations.js +141 -70
- package/dist/query/utils.d.ts +13 -0
- package/dist/query/utils.js +15 -0
- package/dist/serverless.d.ts +1 -1
- package/dist/serverless.js +1 -1
- package/dist/sqlite.d.ts +33 -1
- package/dist/sqlite.js +85 -4
- package/package.json +2 -2
|
@@ -49,6 +49,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
49
49
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
50
|
exports.resolveProjection = resolveProjection;
|
|
51
51
|
exports.resolveColumns = resolveColumns;
|
|
52
|
+
exports.relationOptions = relationOptions;
|
|
52
53
|
exports.withFingerprint = withFingerprint;
|
|
53
54
|
exports.collectWithParams = collectWithParams;
|
|
54
55
|
exports.collectRelationSubqueryParams = collectRelationSubqueryParams;
|
|
@@ -254,6 +255,46 @@ function resolveProjection(qi, table, meta, select, omit, includePii) {
|
|
|
254
255
|
function resolveColumns(qi, select, omit, includePii) {
|
|
255
256
|
return resolveProjection(qi, qi.table, qi.tableMeta, select, omit, includePii);
|
|
256
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* The one place `with: { posts: true }` becomes an options object.
|
|
260
|
+
*
|
|
261
|
+
* `true` is not a third kind of relation spec, it is shorthand for "include
|
|
262
|
+
* this relation with no options", i.e. `{}`. Every walker over a `with` tree
|
|
263
|
+
* needs the same reading of it, and there are SIX of them: the build path
|
|
264
|
+
* ({@link buildRelationSubquery} / {@link buildManyToManySubquery}), the
|
|
265
|
+
* param-collect mirror ({@link collectRelationSubqueryParams}), the cache
|
|
266
|
+
* fingerprint ({@link withFingerprint}), the decode shape
|
|
267
|
+
* ({@link buildRelationShape}), the projection resolver
|
|
268
|
+
* ({@link resolveTargetColumns}) and the flatten planner.
|
|
269
|
+
*
|
|
270
|
+
* Each of them USED TO spell the shorthand out for itself, with a
|
|
271
|
+
* `spec !== true && spec.x` guard per option on the build side and a single
|
|
272
|
+
* `if (spec === true) return` early exit on the collect side. Those two
|
|
273
|
+
* readings are not the same, and the difference was a live bug: since global
|
|
274
|
+
* filters reached relation subqueries the build path has emitted the target's
|
|
275
|
+
* filter UNCONDITIONALLY (a `with` must never surface rows the filter hides),
|
|
276
|
+
* while the collect path's early exit returned before pushing its params. A
|
|
277
|
+
* value-bearing filter on a relation target therefore compiled a `$N` that no
|
|
278
|
+
* value backed, and `with: { posts: true }` failed at bind time with
|
|
279
|
+
* "bind message supplies N parameters, but prepared statement requires N+1"
|
|
280
|
+
* while the identical query written `with: { posts: {} }` worked.
|
|
281
|
+
*
|
|
282
|
+
* Returning ONE frozen empty options object removes the branch instead of
|
|
283
|
+
* duplicating it: below this call there is no `true` case left to keep in
|
|
284
|
+
* sync, so a future option added to one walker cannot be forgotten by the
|
|
285
|
+
* shorthand in another. Frozen and shared because it is read-only by
|
|
286
|
+
* construction (every consumer only reads `select`/`omit`/`where`/`orderBy`/
|
|
287
|
+
* `limit`/`with`), so one instance serves every call and allocates nothing on
|
|
288
|
+
* the hot path.
|
|
289
|
+
*/
|
|
290
|
+
const EMPTY_WITH_OPTIONS = Object.freeze({});
|
|
291
|
+
/**
|
|
292
|
+
* Read a relation spec as options. See {@link EMPTY_WITH_OPTIONS} for why this
|
|
293
|
+
* is a shared authority rather than a guard repeated per walker.
|
|
294
|
+
*/
|
|
295
|
+
function relationOptions(spec) {
|
|
296
|
+
return spec === true ? EMPTY_WITH_OPTIONS : spec;
|
|
297
|
+
}
|
|
257
298
|
/**
|
|
258
299
|
* Produce a fingerprint for a `with` clause tree. Recursion mirrors
|
|
259
300
|
* buildSelectWithRelations / buildRelationSubquery.
|
|
@@ -290,11 +331,7 @@ function withFingerprint(qi, withClause, table, depth = 0) {
|
|
|
290
331
|
parts.push(`unknown:${relName}`);
|
|
291
332
|
continue;
|
|
292
333
|
}
|
|
293
|
-
|
|
294
|
-
parts.push(relName);
|
|
295
|
-
continue;
|
|
296
|
-
}
|
|
297
|
-
const opts = spec;
|
|
334
|
+
const opts = relationOptions(spec);
|
|
298
335
|
const subParts = [];
|
|
299
336
|
// select/omit shape
|
|
300
337
|
if (opts.select) {
|
|
@@ -372,8 +409,10 @@ function collectWithParams(qi, withClause, params, table, flattenPlan) {
|
|
|
372
409
|
* Collect params from a single relation subquery. Mirrors buildRelationSubquery.
|
|
373
410
|
*/
|
|
374
411
|
function collectRelationSubqueryParams(qi, relDef, spec, params, _parentRef, depth = 0) {
|
|
375
|
-
|
|
376
|
-
|
|
412
|
+
// `true` IS `{}` (see {@link relationOptions}). This used to be an early
|
|
413
|
+
// `return`, which skipped the target's global-filter params the build path
|
|
414
|
+
// always pushes.
|
|
415
|
+
const opts = relationOptions(spec);
|
|
377
416
|
const targetTable = relDef.to;
|
|
378
417
|
const targetMeta = qi.schema.tables[targetTable];
|
|
379
418
|
if (!targetMeta)
|
|
@@ -388,19 +427,19 @@ function collectRelationSubqueryParams(qi, relDef, spec, params, _parentRef, dep
|
|
|
388
427
|
// orderBy params → where params → limit param → nested-with params
|
|
389
428
|
// (always, both paths).
|
|
390
429
|
if (relDef.type === 'manyToMany') {
|
|
391
|
-
const m2mOrderEntries =
|
|
430
|
+
const m2mOrderEntries = opts.orderBy ? (0, filters_js_1.orderByEntries)(opts.orderBy).filter(([, dir]) => dir !== undefined) : [];
|
|
392
431
|
if (nativeOrderPath && m2mOrderEntries.length > 0) {
|
|
393
432
|
collectRelationOrderParams(qi, targetTable, targetMeta, m2mOrderEntries, params);
|
|
394
433
|
}
|
|
395
|
-
if (
|
|
396
|
-
whereMod.collectAliasWhereParams(qi, targetTable, targetMeta,
|
|
434
|
+
if (opts.where) {
|
|
435
|
+
whereMod.collectAliasWhereParams(qi, targetTable, targetMeta, opts.where, params);
|
|
397
436
|
}
|
|
398
437
|
whereMod.collectTargetGlobalFilterAlias(qi, targetTable, params);
|
|
399
|
-
if (
|
|
400
|
-
params.push(qi.paginationValue(
|
|
438
|
+
if (opts.limit !== undefined && !qi.dialect.inlineLimitOffset) {
|
|
439
|
+
params.push(qi.paginationValue(opts.limit, 'relation limit'));
|
|
401
440
|
}
|
|
402
|
-
if (
|
|
403
|
-
for (const [nestedRelName, nestedSpec] of (0, filters_js_1.sortedEntries)(
|
|
441
|
+
if (opts.with) {
|
|
442
|
+
for (const [nestedRelName, nestedSpec] of (0, filters_js_1.sortedEntries)(opts.with)) {
|
|
404
443
|
const nestedRelDef = (0, utils_js_1.ownLookup)(targetMeta.relations, nestedRelName);
|
|
405
444
|
if (!nestedRelDef)
|
|
406
445
|
continue;
|
|
@@ -410,12 +449,12 @@ function collectRelationSubqueryParams(qi, relDef, spec, params, _parentRef, dep
|
|
|
410
449
|
return;
|
|
411
450
|
}
|
|
412
451
|
// Mirrors buildRelationSubquery's willWrap: `orderBy: {}` is treated as absent.
|
|
413
|
-
const relOrderEntries =
|
|
452
|
+
const relOrderEntries = opts.orderBy ? (0, filters_js_1.orderByEntries)(opts.orderBy).filter(([, dir]) => dir !== undefined) : [];
|
|
414
453
|
const hasOrder = relOrderEntries.length > 0;
|
|
415
|
-
const willWrap = relDef.type === 'hasMany' && (
|
|
454
|
+
const willWrap = relDef.type === 'hasMany' && (opts.limit !== undefined || hasOrder);
|
|
416
455
|
// Non-wrapped path: nested relations BEFORE where/limit
|
|
417
|
-
if (!willWrap &&
|
|
418
|
-
for (const [nestedRelName, nestedSpec] of (0, filters_js_1.sortedEntries)(
|
|
456
|
+
if (!willWrap && opts.with) {
|
|
457
|
+
for (const [nestedRelName, nestedSpec] of (0, filters_js_1.sortedEntries)(opts.with)) {
|
|
419
458
|
const nestedRelDef = (0, utils_js_1.ownLookup)(targetMeta.relations, nestedRelName);
|
|
420
459
|
if (!nestedRelDef)
|
|
421
460
|
continue;
|
|
@@ -429,8 +468,8 @@ function collectRelationSubqueryParams(qi, relDef, spec, params, _parentRef, dep
|
|
|
429
468
|
collectRelationOrderParams(qi, targetTable, targetMeta, relOrderEntries, params);
|
|
430
469
|
}
|
|
431
470
|
// where params, mirrors buildAliasWhere push order
|
|
432
|
-
if (
|
|
433
|
-
whereMod.collectAliasWhereParams(qi, targetTable, targetMeta,
|
|
471
|
+
if (opts.where) {
|
|
472
|
+
whereMod.collectAliasWhereParams(qi, targetTable, targetMeta, opts.where, params);
|
|
434
473
|
}
|
|
435
474
|
// Global filter on the target, mirrors targetGlobalFilterAlias in
|
|
436
475
|
// buildRelationSubquery (pushed after spec.where, before limit).
|
|
@@ -439,12 +478,12 @@ function collectRelationSubqueryParams(qi, relDef, spec, params, _parentRef, dep
|
|
|
439
478
|
// buildRelationSubquery). belongsTo/hasOne ignore limit (always LIMIT 1), so
|
|
440
479
|
// pushing one here would orphan a param and desync the collect path.
|
|
441
480
|
// `limit: 0` pushes (LIMIT 0 is honored), so check !== undefined.
|
|
442
|
-
if (relDef.type === 'hasMany' &&
|
|
443
|
-
params.push(qi.paginationValue(
|
|
481
|
+
if (relDef.type === 'hasMany' && opts.limit !== undefined && !qi.dialect.inlineLimitOffset) {
|
|
482
|
+
params.push(qi.paginationValue(opts.limit, 'relation limit'));
|
|
444
483
|
}
|
|
445
484
|
// Wrapped path: nested relations AFTER where/limit (inside inner subquery)
|
|
446
|
-
if (willWrap &&
|
|
447
|
-
for (const [nestedRelName, nestedSpec] of (0, filters_js_1.sortedEntries)(
|
|
485
|
+
if (willWrap && opts.with) {
|
|
486
|
+
for (const [nestedRelName, nestedSpec] of (0, filters_js_1.sortedEntries)(opts.with)) {
|
|
448
487
|
const nestedRelDef = (0, utils_js_1.ownLookup)(targetMeta.relations, nestedRelName);
|
|
449
488
|
if (!nestedRelDef)
|
|
450
489
|
continue;
|
|
@@ -813,13 +852,12 @@ function buildRelationOrderBy(qi, relName, value, alias, params, ctx, lateralSin
|
|
|
813
852
|
// landing here on such a table is an orderBy VALUE of the wrong shape on a
|
|
814
853
|
// scalar column, which deserves to be named rather than reported as a
|
|
815
854
|
// missing relation.
|
|
816
|
-
const known = Object.keys(ownerMeta.relations);
|
|
817
855
|
const isColumn = (0, utils_js_1.resolveColumnName)(ownerMeta, relName) !== undefined;
|
|
818
856
|
throw new errors_js_1.RelationError(isColumn
|
|
819
857
|
? `[turbine] orderBy on "${ownerTable}.${relName}" got a relation-shaped value, but "${relName}" is a ` +
|
|
820
858
|
`column. Order a column with 'asc' / 'desc' (or { sort, nulls }); the object form is for relations.`
|
|
821
859
|
: `[turbine] Unknown relation "${relName}" in orderBy on table "${ownerTable}". ` +
|
|
822
|
-
(
|
|
860
|
+
(0, utils_js_1.availableClause)(Object.keys(ownerMeta.relations), `"${ownerTable}" has no relations.`));
|
|
823
861
|
}
|
|
824
862
|
// Pick-row ordering (`{ pick, by }`): order by a value from ONE related
|
|
825
863
|
// row: a correlated scalar subquery with its own ORDER BY … LIMIT 1.
|
|
@@ -1398,8 +1436,9 @@ function parseNestedRow(qi, row, table, fromJson = false) {
|
|
|
1398
1436
|
* second implementation.
|
|
1399
1437
|
*/
|
|
1400
1438
|
function resolveTargetColumns(qi, spec, targetMeta, includePii, targetTable = targetMeta.name) {
|
|
1401
|
-
const
|
|
1402
|
-
const
|
|
1439
|
+
const opts = relationOptions(spec);
|
|
1440
|
+
const select = opts.select;
|
|
1441
|
+
const omit = opts.omit;
|
|
1403
1442
|
return resolveProjection(qi, targetTable, targetMeta, select, omit, includePii) ?? targetMeta.allColumns;
|
|
1404
1443
|
}
|
|
1405
1444
|
/**
|
|
@@ -1554,8 +1593,9 @@ function buildRelationShape(qi, relDef, spec, parentMeta, includePii) {
|
|
|
1554
1593
|
const targetColumns = resolveTargetColumns(qi, spec, targetMeta, includePii, relDef.to);
|
|
1555
1594
|
const keys = targetColumns.map((col) => targetMeta.reverseColumnMap[col] ?? (0, schema_js_1.snakeToCamel)(col));
|
|
1556
1595
|
const nested = {};
|
|
1557
|
-
|
|
1558
|
-
|
|
1596
|
+
const nestedWith = relationOptions(spec).with;
|
|
1597
|
+
if (nestedWith) {
|
|
1598
|
+
for (const [nestedRelName, nestedSpec] of (0, filters_js_1.sortedEntries)(nestedWith)) {
|
|
1559
1599
|
const nestedRelDef = (0, utils_js_1.ownLookup)(targetMeta.relations, nestedRelName);
|
|
1560
1600
|
if (!nestedRelDef)
|
|
1561
1601
|
continue;
|
|
@@ -1792,15 +1832,13 @@ function planFlattenNode(qi, counter, relName, relDef, spec, depth, path, includ
|
|
|
1792
1832
|
: `its correlation column(s) on "${relDef.to}" are not provably unique (no primary key, ` +
|
|
1793
1833
|
'unique constraint or non-partial unique index covers them), so a join could multiply parent rows');
|
|
1794
1834
|
}
|
|
1795
|
-
const opts = spec
|
|
1796
|
-
if (opts)
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
return decline('it declares an `orderBy`');
|
|
1801
|
-
}
|
|
1835
|
+
const opts = relationOptions(spec);
|
|
1836
|
+
if (opts.limit !== undefined)
|
|
1837
|
+
return decline('it declares a `limit`');
|
|
1838
|
+
if (opts.orderBy && (0, filters_js_1.orderByEntries)(opts.orderBy).some(([, dir]) => dir !== undefined)) {
|
|
1839
|
+
return decline('it declares an `orderBy`');
|
|
1802
1840
|
}
|
|
1803
|
-
const nestedEntries = opts
|
|
1841
|
+
const nestedEntries = opts.with ? (0, filters_js_1.sortedEntries)(opts.with) : [];
|
|
1804
1842
|
for (const [nestedRelName] of nestedEntries) {
|
|
1805
1843
|
if (nestedRelName === '_count')
|
|
1806
1844
|
return decline('its nested `with` uses `_count`');
|
|
@@ -2001,8 +2039,9 @@ function emitFlattenInner(qi, node, params, innerSelects, innerJoins, innerWhere
|
|
|
2001
2039
|
// than dropping the parent row, matching what the correlated subquery's
|
|
2002
2040
|
// `LIMIT 1` did when it returned NULL.
|
|
2003
2041
|
const filters = innerWhere;
|
|
2004
|
-
|
|
2005
|
-
|
|
2042
|
+
const nodeWhere = relationOptions(node.spec).where;
|
|
2043
|
+
if (nodeWhere) {
|
|
2044
|
+
const extra = whereMod.buildAliasWhere(qi, targetTable, targetMeta, srcAlias, nodeWhere, params);
|
|
2006
2045
|
if (extra)
|
|
2007
2046
|
filters.push(extra);
|
|
2008
2047
|
}
|
|
@@ -2068,8 +2107,9 @@ function projectFlattenNode(qi, node, outerAlias, selectSink) {
|
|
|
2068
2107
|
}
|
|
2069
2108
|
/** Param-collect mirror of {@link emitFlattenNode}. */
|
|
2070
2109
|
function collectFlattenNodeParams(qi, node, params) {
|
|
2071
|
-
|
|
2072
|
-
|
|
2110
|
+
const nodeWhere = relationOptions(node.spec).where;
|
|
2111
|
+
if (nodeWhere) {
|
|
2112
|
+
whereMod.collectAliasWhereParams(qi, node.targetTable, node.targetMeta, nodeWhere, params);
|
|
2073
2113
|
}
|
|
2074
2114
|
whereMod.collectTargetGlobalFilterAlias(qi, node.targetTable, params);
|
|
2075
2115
|
for (const slot of node.slots) {
|
|
@@ -2218,7 +2258,7 @@ function buildSelectWithRelations(qi, table, withClause, params, columnsList, de
|
|
|
2218
2258
|
const relDef = (0, utils_js_1.ownLookup)(meta.relations, relName);
|
|
2219
2259
|
if (!relDef) {
|
|
2220
2260
|
throw new errors_js_1.RelationError(`[turbine] Unknown relation "${relName}" on table "${table}". ` +
|
|
2221
|
-
|
|
2261
|
+
(0, utils_js_1.availableClause)(Object.keys(meta.relations), `"${table}" has no relations.`));
|
|
2222
2262
|
}
|
|
2223
2263
|
// `relationLoadStrategy: 'flatten'`: an eligible to-one relation becomes a
|
|
2224
2264
|
// LEFT JOIN + prefixed scalar projection instead of a per-parent-row
|
|
@@ -2352,6 +2392,11 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
|
|
|
2352
2392
|
const targetMeta = qi.schema.tables[targetTable];
|
|
2353
2393
|
if (!targetMeta)
|
|
2354
2394
|
throw new errors_js_1.RelationError(`[turbine] Unknown relation target "${targetTable}"`);
|
|
2395
|
+
// `true` IS `{}`; below this line there is no `true` case, which is what
|
|
2396
|
+
// keeps this walk and collectRelationSubqueryParams reading one shape.
|
|
2397
|
+
// The raw `spec` is still what crosses the dialect seam below, since
|
|
2398
|
+
// RelationSubqueryContext is a published contract.
|
|
2399
|
+
const opts = relationOptions(spec);
|
|
2355
2400
|
// Dev-only: correlated relation loading probes the child table once per parent
|
|
2356
2401
|
// row, so a missing FK index multiplies into per-parent full-table scans (a
|
|
2357
2402
|
// batched-loader ORM pays the same missing index only once, which is why
|
|
@@ -2400,9 +2445,33 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
|
|
|
2400
2445
|
depth: currentDepth,
|
|
2401
2446
|
path: currentPath,
|
|
2402
2447
|
quote: (name) => qi.q(name),
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2448
|
+
// The relation target's WHERE, which is the caller's `spec.where` AND the
|
|
2449
|
+
// target table's GLOBAL FILTER. Both, not just the first.
|
|
2450
|
+
//
|
|
2451
|
+
// A dialect that overrides buildRelationSubquery takes over the whole
|
|
2452
|
+
// subquery, so the generic builder returns at this seam BEFORE it would
|
|
2453
|
+
// have applied targetGlobalFilterAlias itself. The override cannot apply
|
|
2454
|
+
// the filter on its own: RelationSubqueryContext deliberately hands it no
|
|
2455
|
+
// BuilderCtx, so `targetGlobalFilterAlias` is not reachable from a dialect
|
|
2456
|
+
// file. That left SQL Server's `FOR JSON PATH` path emitting a correlation
|
|
2457
|
+
// predicate and nothing else, so a `with` returned rows a tenancy or
|
|
2458
|
+
// soft-delete filter is supposed to hide, while the collect mirror still
|
|
2459
|
+
// pushed the filter's params. Wrong rows AND an orphan param, on every
|
|
2460
|
+
// spec shape.
|
|
2461
|
+
//
|
|
2462
|
+
// Folding it in here rather than at each override keeps ONE authority for
|
|
2463
|
+
// "what does this relation's WHERE contain", and lands the params exactly
|
|
2464
|
+
// where collectRelationSubqueryParams already expects them: after
|
|
2465
|
+
// spec.where, before the limit.
|
|
2466
|
+
buildWhere: (whereAlias) => {
|
|
2467
|
+
const userWhere = (opts.where
|
|
2468
|
+
? whereMod.buildAliasWhere(qi, targetTable, targetMeta, whereAlias, opts.where, params)
|
|
2469
|
+
: '') ?? '';
|
|
2470
|
+
const gf = whereMod.targetGlobalFilterAlias(qi, targetTable, whereAlias, params);
|
|
2471
|
+
if (userWhere && gf)
|
|
2472
|
+
return `${userWhere} AND ${gf}`;
|
|
2473
|
+
return userWhere || gf || '';
|
|
2474
|
+
},
|
|
2406
2475
|
recurse: (nRelDef, nSpec, nParent, nDepth, nPath) => buildRelationSubquery(qi, nRelDef, nSpec, params, nParent, aliasCounter, nDepth, nPath, includePii),
|
|
2407
2476
|
});
|
|
2408
2477
|
}
|
|
@@ -2414,8 +2483,8 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
|
|
|
2414
2483
|
// An orderBy with no defined entries (`orderBy: {}`) is treated as absent -
|
|
2415
2484
|
// it must neither trigger the wrap (dropping nested relations) nor render a
|
|
2416
2485
|
// dangling `ORDER BY `. `limit: 0` is meaningful (LIMIT 0) and DOES wrap.
|
|
2417
|
-
const relOrderEntries =
|
|
2418
|
-
const willWrap = relDef.type === 'hasMany' &&
|
|
2486
|
+
const relOrderEntries = opts.orderBy ? (0, filters_js_1.orderByEntries)(opts.orderBy).filter(([, dir]) => dir !== undefined) : [];
|
|
2487
|
+
const willWrap = relDef.type === 'hasMany' && (opts.limit !== undefined || relOrderEntries.length > 0);
|
|
2419
2488
|
// manyToMany takes a dedicated JOIN-through-junction path. Nested relations,
|
|
2420
2489
|
// where, orderBy, and select/omit are handled there (the target alias is the
|
|
2421
2490
|
// row source, exactly like hasMany), so short-circuit before the hasMany logic.
|
|
@@ -2423,12 +2492,12 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
|
|
|
2423
2492
|
return buildManyToManySubquery(qi, relDef, spec, params, parentRef, aliasCounter, currentDepth, currentPath, alias, targetMeta, targetColumns, includePii);
|
|
2424
2493
|
}
|
|
2425
2494
|
// Nested relations, only in the non-wrapped path (wrapped path builds them separately)
|
|
2426
|
-
if (!willWrap &&
|
|
2427
|
-
for (const [nestedRelName, nestedSpec] of (0, filters_js_1.sortedEntries)(
|
|
2495
|
+
if (!willWrap && opts.with) {
|
|
2496
|
+
for (const [nestedRelName, nestedSpec] of (0, filters_js_1.sortedEntries)(opts.with)) {
|
|
2428
2497
|
const nestedRelDef = (0, utils_js_1.ownLookup)(targetMeta.relations, nestedRelName);
|
|
2429
2498
|
if (!nestedRelDef) {
|
|
2430
2499
|
throw new errors_js_1.RelationError(`[turbine] Unknown relation "${nestedRelName}" on table "${targetTable}". ` +
|
|
2431
|
-
|
|
2500
|
+
(0, utils_js_1.availableClause)(Object.keys(targetMeta.relations), `"${relDef.to}" has no relations.`));
|
|
2432
2501
|
}
|
|
2433
2502
|
// Recursively build nested subquery, passing THIS alias as the parent reference
|
|
2434
2503
|
const nestedSubquery = buildRelationSubquery(qi, nestedRelDef, nestedSpec, params, alias, aliasCounter, currentDepth + 1, [...currentPath, relDef.name], includePii);
|
|
@@ -2465,8 +2534,8 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
|
|
|
2465
2534
|
}
|
|
2466
2535
|
// Additional filters, full scalar where surface (equality, null, operator
|
|
2467
2536
|
// objects, OR/AND/NOT), properly parameterized against this alias.
|
|
2468
|
-
if (
|
|
2469
|
-
const extra = whereMod.buildAliasWhere(qi, targetTable, targetMeta, alias,
|
|
2537
|
+
if (opts.where) {
|
|
2538
|
+
const extra = whereMod.buildAliasWhere(qi, targetTable, targetMeta, alias, opts.where, params);
|
|
2470
2539
|
if (extra)
|
|
2471
2540
|
whereClause += ` AND ${extra}`;
|
|
2472
2541
|
}
|
|
@@ -2483,8 +2552,8 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
|
|
|
2483
2552
|
// (and shifts every later placeholder by one). To-one relations ignore limit.
|
|
2484
2553
|
// `limit: 0` is honored (LIMIT 0 → empty array), so check !== undefined.
|
|
2485
2554
|
let limitClause = '';
|
|
2486
|
-
if (relDef.type === 'hasMany' &&
|
|
2487
|
-
limitClause = ` LIMIT ${qi.paginationRef(
|
|
2555
|
+
if (relDef.type === 'hasMany' && opts.limit !== undefined) {
|
|
2556
|
+
limitClause = ` LIMIT ${qi.paginationRef(opts.limit, params, 'relation limit')}`;
|
|
2488
2557
|
}
|
|
2489
2558
|
if (relDef.type === 'hasMany') {
|
|
2490
2559
|
// When LIMIT or ORDER BY is used, wrap in a subquery so LIMIT applies to rows
|
|
@@ -2497,12 +2566,12 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
|
|
|
2497
2566
|
// For the json_build_object, reference the inner alias, only include resolved columns
|
|
2498
2567
|
const innerJsonPairs = jsonScalarPairs(qi, targetMeta, targetColumns, innerAlias);
|
|
2499
2568
|
// Build nested relation subqueries referencing innerAlias
|
|
2500
|
-
if (
|
|
2501
|
-
for (const [nestedRelName, nestedSpec] of (0, filters_js_1.sortedEntries)(
|
|
2569
|
+
if (opts.with) {
|
|
2570
|
+
for (const [nestedRelName, nestedSpec] of (0, filters_js_1.sortedEntries)(opts.with)) {
|
|
2502
2571
|
const nestedRelDef = (0, utils_js_1.ownLookup)(targetMeta.relations, nestedRelName);
|
|
2503
2572
|
if (!nestedRelDef) {
|
|
2504
2573
|
throw new errors_js_1.RelationError(`[turbine] Unknown relation "${nestedRelName}" on table "${targetTable}". ` +
|
|
2505
|
-
|
|
2574
|
+
(0, utils_js_1.availableClause)(Object.keys(targetMeta.relations), `"${relDef.to}" has no relations.`));
|
|
2506
2575
|
}
|
|
2507
2576
|
const nestedSub = buildRelationSubquery(qi, nestedRelDef, nestedSpec, params, innerAlias, aliasCounter, currentDepth + 1, [...currentPath, relDef.name], includePii);
|
|
2508
2577
|
const fallback = nestedRelDef.type === 'hasMany' ? qi.dialect.emptyJsonArrayLiteral : qi.dialect.nullJsonLiteral;
|
|
@@ -2547,6 +2616,9 @@ function buildManyToManySubquery(qi, relDef, spec, params, parentRef, aliasCount
|
|
|
2547
2616
|
if (!relDef.through) {
|
|
2548
2617
|
throw new errors_js_1.ValidationError(`[turbine] manyToMany relation "${relDef.name}" is missing a \`through\` junction descriptor.`);
|
|
2549
2618
|
}
|
|
2619
|
+
// `true` IS `{}`; see {@link relationOptions}. Same reading as
|
|
2620
|
+
// buildRelationSubquery and collectRelationSubqueryParams' m2m branch.
|
|
2621
|
+
const opts = relationOptions(spec);
|
|
2550
2622
|
const targetTable = relDef.to;
|
|
2551
2623
|
const qTarget = qi.q(targetTable);
|
|
2552
2624
|
const qJunction = qi.q(relDef.through.table);
|
|
@@ -2582,15 +2654,15 @@ function buildManyToManySubquery(qi, relDef, spec, params, parentRef, aliasCount
|
|
|
2582
2654
|
// `orderBy: {}` (no defined entries) is treated as absent: it must not
|
|
2583
2655
|
// render a dangling `ORDER BY `. Param pushes here land BEFORE the
|
|
2584
2656
|
// spec.where params, mirrored by collectRelationSubqueryParams' m2m branch.
|
|
2585
|
-
const relOrderEntries =
|
|
2657
|
+
const relOrderEntries = opts.orderBy ? (0, filters_js_1.orderByEntries)(opts.orderBy).filter(([, dir]) => dir !== undefined) : [];
|
|
2586
2658
|
let orderClause = '';
|
|
2587
2659
|
if (relOrderEntries.length > 0) {
|
|
2588
2660
|
orderClause = buildRelationOrderClause(qi, targetTable, targetMeta, talias, relOrderEntries, params);
|
|
2589
2661
|
}
|
|
2590
2662
|
// Additional WHERE filters on the target, full scalar where surface,
|
|
2591
2663
|
// properly parameterized against the target alias.
|
|
2592
|
-
if (
|
|
2593
|
-
const extra = whereMod.buildAliasWhere(qi, targetTable, targetMeta, talias,
|
|
2664
|
+
if (opts.where) {
|
|
2665
|
+
const extra = whereMod.buildAliasWhere(qi, targetTable, targetMeta, talias, opts.where, params);
|
|
2594
2666
|
if (extra)
|
|
2595
2667
|
whereClause += ` AND ${extra}`;
|
|
2596
2668
|
}
|
|
@@ -2601,8 +2673,8 @@ function buildManyToManySubquery(qi, relDef, spec, params, parentRef, aliasCount
|
|
|
2601
2673
|
whereClause += ` AND ${gfExtra}`;
|
|
2602
2674
|
// LIMIT, `limit: 0` is honored (LIMIT 0 → empty array)
|
|
2603
2675
|
let limitClause = '';
|
|
2604
|
-
if (
|
|
2605
|
-
limitClause = ` LIMIT ${qi.paginationRef(
|
|
2676
|
+
if (opts.limit !== undefined) {
|
|
2677
|
+
limitClause = ` LIMIT ${qi.paginationRef(opts.limit, params, 'relation limit')}`;
|
|
2606
2678
|
}
|
|
2607
2679
|
const fromJoin = `FROM ${qTarget} ${talias} JOIN ${qJunction} ${jalias} ON ${joinOn}`;
|
|
2608
2680
|
// When LIMIT or ORDER BY is present, wrap the joined rows in an inner subquery
|
|
@@ -2613,12 +2685,12 @@ function buildManyToManySubquery(qi, relDef, spec, params, parentRef, aliasCount
|
|
|
2613
2685
|
`${fromJoin} WHERE ${whereClause}${orderClause}${limitClause}`;
|
|
2614
2686
|
const innerJsonPairs = jsonScalarPairs(qi, targetMeta, targetColumns, innerAlias);
|
|
2615
2687
|
// Nested relations reference the inner alias.
|
|
2616
|
-
if (
|
|
2617
|
-
for (const [nestedRelName, nestedSpec] of (0, filters_js_1.sortedEntries)(
|
|
2688
|
+
if (opts.with) {
|
|
2689
|
+
for (const [nestedRelName, nestedSpec] of (0, filters_js_1.sortedEntries)(opts.with)) {
|
|
2618
2690
|
const nestedRelDef = (0, utils_js_1.ownLookup)(targetMeta.relations, nestedRelName);
|
|
2619
2691
|
if (!nestedRelDef) {
|
|
2620
2692
|
throw new errors_js_1.RelationError(`[turbine] Unknown relation "${nestedRelName}" on table "${targetTable}". ` +
|
|
2621
|
-
|
|
2693
|
+
(0, utils_js_1.availableClause)(Object.keys(targetMeta.relations), `"${relDef.to}" has no relations.`));
|
|
2622
2694
|
}
|
|
2623
2695
|
const nestedSub = buildRelationSubquery(qi, nestedRelDef, nestedSpec, params, innerAlias, aliasCounter, currentDepth + 1, [...currentPath, relDef.name], includePii);
|
|
2624
2696
|
const fallback = nestedRelDef.type === 'belongsTo' || nestedRelDef.type === 'hasOne'
|
|
@@ -2633,12 +2705,12 @@ function buildManyToManySubquery(qi, relDef, spec, params, parentRef, aliasCount
|
|
|
2633
2705
|
// Simple path: build the json object pairs directly off the target alias,
|
|
2634
2706
|
// including any nested relations (correlated to the target alias).
|
|
2635
2707
|
const jsonPairs = jsonScalarPairs(qi, targetMeta, targetColumns, talias);
|
|
2636
|
-
if (
|
|
2637
|
-
for (const [nestedRelName, nestedSpec] of (0, filters_js_1.sortedEntries)(
|
|
2708
|
+
if (opts.with) {
|
|
2709
|
+
for (const [nestedRelName, nestedSpec] of (0, filters_js_1.sortedEntries)(opts.with)) {
|
|
2638
2710
|
const nestedRelDef = (0, utils_js_1.ownLookup)(targetMeta.relations, nestedRelName);
|
|
2639
2711
|
if (!nestedRelDef) {
|
|
2640
2712
|
throw new errors_js_1.RelationError(`[turbine] Unknown relation "${nestedRelName}" on table "${targetTable}". ` +
|
|
2641
|
-
|
|
2713
|
+
(0, utils_js_1.availableClause)(Object.keys(targetMeta.relations), `"${relDef.to}" has no relations.`));
|
|
2642
2714
|
}
|
|
2643
2715
|
const nestedSub = buildRelationSubquery(qi, nestedRelDef, nestedSpec, params, talias, aliasCounter, currentDepth + 1, [...currentPath, relDef.name], includePii);
|
|
2644
2716
|
const fallback = nestedRelDef.type === 'belongsTo' || nestedRelDef.type === 'hasOne'
|
|
@@ -677,6 +677,19 @@ export declare function unknownFieldMessage(table: string, field: string, meta:
|
|
|
677
677
|
columnMap: Record<string, string>;
|
|
678
678
|
relations?: Record<string, unknown>;
|
|
679
679
|
}): string;
|
|
680
|
+
/**
|
|
681
|
+
* The trailing "Available: a, b, c" clause of a name-not-found error, or
|
|
682
|
+
* `emptySentence` when there is nothing to list.
|
|
683
|
+
*
|
|
684
|
+
* The empty branch is the whole reason this exists. Fifteen error sites
|
|
685
|
+
* interpolated `Object.keys(...).join(', ')` directly, and on a table with no
|
|
686
|
+
* relations (or a schema with no tables) that renders a dangling
|
|
687
|
+
* `Available: ` with nothing after the colon, which reads as a broken error
|
|
688
|
+
* message rather than as an answer. One site already got this right by hand;
|
|
689
|
+
* the other fourteen are now the same function, so the next one cannot get it
|
|
690
|
+
* wrong by omission.
|
|
691
|
+
*/
|
|
692
|
+
export declare function availableClause(names: readonly string[], emptySentence: string): string;
|
|
680
693
|
/**
|
|
681
694
|
* The error text for a RELATION named inside `select` / `omit`.
|
|
682
695
|
*
|
package/dist/cjs/query/utils.js
CHANGED
|
@@ -45,6 +45,7 @@ exports.coerceJsonWireValue = coerceJsonWireValue;
|
|
|
45
45
|
exports.closestName = closestName;
|
|
46
46
|
exports.suggestKey = suggestKey;
|
|
47
47
|
exports.unknownFieldMessage = unknownFieldMessage;
|
|
48
|
+
exports.availableClause = availableClause;
|
|
48
49
|
exports.relationInProjectionMessage = relationInProjectionMessage;
|
|
49
50
|
exports.warnRedundantSortTerm = warnRedundantSortTerm;
|
|
50
51
|
exports.selectNamesNothingMessage = selectNamesNothingMessage;
|
|
@@ -1467,6 +1468,21 @@ function unknownFieldMessage(table, field, meta) {
|
|
|
1467
1468
|
` Known columns: ${columns.join(', ') || '(none)'}.` +
|
|
1468
1469
|
(relations.length ? ` Known relations (valid in \`where\` and \`with\`): ${relations.join(', ')}.` : ''));
|
|
1469
1470
|
}
|
|
1471
|
+
/**
|
|
1472
|
+
* The trailing "Available: a, b, c" clause of a name-not-found error, or
|
|
1473
|
+
* `emptySentence` when there is nothing to list.
|
|
1474
|
+
*
|
|
1475
|
+
* The empty branch is the whole reason this exists. Fifteen error sites
|
|
1476
|
+
* interpolated `Object.keys(...).join(', ')` directly, and on a table with no
|
|
1477
|
+
* relations (or a schema with no tables) that renders a dangling
|
|
1478
|
+
* `Available: ` with nothing after the colon, which reads as a broken error
|
|
1479
|
+
* message rather than as an answer. One site already got this right by hand;
|
|
1480
|
+
* the other fourteen are now the same function, so the next one cannot get it
|
|
1481
|
+
* wrong by omission.
|
|
1482
|
+
*/
|
|
1483
|
+
function availableClause(names, emptySentence) {
|
|
1484
|
+
return names.length > 0 ? `Available: ${names.join(', ')}` : emptySentence;
|
|
1485
|
+
}
|
|
1470
1486
|
/**
|
|
1471
1487
|
* The error text for a RELATION named inside `select` / `omit`.
|
|
1472
1488
|
*
|
package/dist/cjs/serverless.d.ts
CHANGED
|
@@ -143,7 +143,7 @@ export type TurbineHttpOptions = Omit<TurbineConfig, 'pool'>;
|
|
|
143
143
|
* ```ts
|
|
144
144
|
* import { Pool } from '@neondatabase/serverless';
|
|
145
145
|
* import { turbineHttp } from 'turbine-orm/serverless';
|
|
146
|
-
* import type { TurbineClient } from './generated/turbine';
|
|
146
|
+
* import type { TurbineClient } from './generated/turbine/index.js';
|
|
147
147
|
* import { SCHEMA } from './generated/turbine/metadata.js';
|
|
148
148
|
*
|
|
149
149
|
* const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
package/dist/cjs/serverless.js
CHANGED
|
@@ -126,7 +126,7 @@ const client_js_1 = require("./client.js");
|
|
|
126
126
|
* ```ts
|
|
127
127
|
* import { Pool } from '@neondatabase/serverless';
|
|
128
128
|
* import { turbineHttp } from 'turbine-orm/serverless';
|
|
129
|
-
* import type { TurbineClient } from './generated/turbine';
|
|
129
|
+
* import type { TurbineClient } from './generated/turbine/index.js';
|
|
130
130
|
* import { SCHEMA } from './generated/turbine/metadata.js';
|
|
131
131
|
*
|
|
132
132
|
* const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
package/dist/cjs/sqlite.d.ts
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* - **Case-insensitive matching** uses `COLLATE NOCASE`, which is **ASCII-only**
|
|
35
35
|
* (no Unicode case folding).
|
|
36
36
|
*
|
|
37
|
-
* ## Example, `:memory:` database
|
|
37
|
+
* ## Example, `:memory:` database, from a generated schema
|
|
38
38
|
*
|
|
39
39
|
* ```ts
|
|
40
40
|
* import { turbineSqlite } from 'turbine-orm/sqlite';
|
|
@@ -44,6 +44,38 @@
|
|
|
44
44
|
* const users = await db.users.findMany({ with: { posts: true }, limit: 10 });
|
|
45
45
|
* await db.disconnect();
|
|
46
46
|
* ```
|
|
47
|
+
*
|
|
48
|
+
* ## Example, SQLite with no Postgres anywhere
|
|
49
|
+
*
|
|
50
|
+
* The snippet above needs `turbine generate`, which reads a live **Postgres**
|
|
51
|
+
* catalog, so it is the wrong starting point if SQLite is your only database.
|
|
52
|
+
* Describe the schema in code instead: `schemaToSQL` emits the DDL and
|
|
53
|
+
* `schemaDefToMetadata` derives the runtime metadata (relations included, from
|
|
54
|
+
* the same `references:`), both pure functions with no database involved.
|
|
55
|
+
*
|
|
56
|
+
* ```ts
|
|
57
|
+
* import { defineSchema, schemaDefToMetadata, schemaToSQL } from 'turbine-orm';
|
|
58
|
+
* import { sqliteDialect, turbineSqlite } from 'turbine-orm/sqlite';
|
|
59
|
+
*
|
|
60
|
+
* const schema = defineSchema({
|
|
61
|
+
* users: { id: { type: 'serial', primaryKey: true }, email: { type: 'text', notNull: true } },
|
|
62
|
+
* posts: {
|
|
63
|
+
* id: { type: 'serial', primaryKey: true },
|
|
64
|
+
* userId: { type: 'integer', notNull: true, references: 'users.id' },
|
|
65
|
+
* title: { type: 'text', notNull: true },
|
|
66
|
+
* },
|
|
67
|
+
* });
|
|
68
|
+
*
|
|
69
|
+
* const db = turbineSqlite(':memory:', schemaDefToMetadata(schema));
|
|
70
|
+
* for (const stmt of schemaToSQL(schema, { dialect: sqliteDialect })) {
|
|
71
|
+
* await db.raw([stmt] as never);
|
|
72
|
+
* }
|
|
73
|
+
*
|
|
74
|
+
* // `db.table(...)`, not `db.users`: the typed property accessors are emitted
|
|
75
|
+
* // by `turbine generate`, and this path skips it.
|
|
76
|
+
* const users = await db.table('users').findMany({ with: { posts: true }, orderBy: { id: 'asc' } });
|
|
77
|
+
* await db.disconnect();
|
|
78
|
+
* ```
|
|
47
79
|
*/
|
|
48
80
|
import type { DatabaseSync } from 'node:sqlite';
|
|
49
81
|
import { type PgCompatPool, type PgCompatPoolClient, TurbineClient } from './client.js';
|