turbine-orm 0.71.0 → 0.72.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 +2 -2
- package/dist/cjs/client.d.ts +0 -18
- package/dist/cjs/client.js +2 -29
- package/dist/cjs/connection-url.d.ts +30 -0
- package/dist/cjs/connection-url.js +15 -17
- package/dist/cjs/powql.d.ts +38 -1
- package/dist/cjs/powql.js +106 -18
- package/dist/cjs/query/aggregates.d.ts +0 -13
- package/dist/cjs/query/aggregates.js +81 -33
- package/dist/cjs/query/batched-loader.d.ts +13 -1
- package/dist/cjs/query/batched-loader.js +46 -11
- package/dist/cjs/query/builder.d.ts +13 -0
- package/dist/cjs/query/builder.js +104 -14
- package/dist/cjs/query/compound-unique.js +29 -5
- package/dist/cjs/query/relation-names.d.ts +52 -0
- package/dist/cjs/query/relation-names.js +120 -0
- package/dist/cjs/query/relations.d.ts +11 -6
- package/dist/cjs/query/relations.js +45 -27
- package/dist/cjs/query/utils.d.ts +107 -3
- package/dist/cjs/query/utils.js +408 -7
- package/dist/cjs/query/where-compile.js +9 -4
- package/dist/cjs/query/where.js +9 -5
- package/dist/client.d.ts +0 -18
- package/dist/client.js +2 -29
- package/dist/connection-url.d.ts +30 -0
- package/dist/connection-url.js +15 -18
- package/dist/powql.d.ts +38 -1
- package/dist/powql.js +107 -19
- package/dist/query/aggregates.d.ts +0 -13
- package/dist/query/aggregates.js +82 -34
- package/dist/query/batched-loader.d.ts +13 -1
- package/dist/query/batched-loader.js +47 -12
- package/dist/query/builder.d.ts +13 -0
- package/dist/query/builder.js +105 -15
- package/dist/query/compound-unique.js +30 -6
- package/dist/query/relation-names.d.ts +52 -0
- package/dist/query/relation-names.js +117 -0
- package/dist/query/relations.d.ts +11 -6
- package/dist/query/relations.js +47 -29
- package/dist/query/utils.d.ts +107 -3
- package/dist/query/utils.js +404 -8
- package/dist/query/where-compile.js +10 -5
- package/dist/query/where.js +10 -6
- package/package.json +5 -3
|
@@ -89,12 +89,14 @@ function assertAggregatePiiOptIn(table, meta, field, column, usage, includePii)
|
|
|
89
89
|
}
|
|
90
90
|
function buildGroupBy(qi, args) {
|
|
91
91
|
const meta = qi.schema.tables[qi.table];
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
92
|
+
// Up-front, so a bad `by` key is reported before anything else in the call,
|
|
93
|
+
// and through `qi.toColumn` (the ONE `resolveColumnName` rule) rather than
|
|
94
|
+
// `key in meta.columnMap`, which knows only the FIELD spelling and so
|
|
95
|
+
// rejected the snake_case COLUMN name that `where` / `select` / `distinct`
|
|
96
|
+
// accept and that an introspected schema's DDL declares.
|
|
97
|
+
for (const key of args.by) {
|
|
98
|
+
if (typeof key === 'string')
|
|
99
|
+
qi.toColumn(key);
|
|
98
100
|
}
|
|
99
101
|
qi.currentSkip = (0, types_js_1.resolveSkipGlobalFilters)(args.skipGlobalFilters);
|
|
100
102
|
// Resolve the PII opt-in ONCE, here, so the sentinel check runs on every
|
|
@@ -150,18 +152,30 @@ function buildGroupBy(qi, args) {
|
|
|
150
152
|
for (const entry of args.by) {
|
|
151
153
|
if (typeof entry === 'string') {
|
|
152
154
|
const col = qi.toColumn(entry);
|
|
155
|
+
// The group key's identity is the COLUMN, so everything keyed off it
|
|
156
|
+
// uses the canonical FIELD name rather than whichever spelling the caller
|
|
157
|
+
// wrote. The result key above all: rows are read through `parseRow`,
|
|
158
|
+
// whose keys are field names, so `by: ['created_at']` keyed by the
|
|
159
|
+
// caller's spelling read `parsed['created_at']` and returned `undefined`
|
|
160
|
+
// for every group. `_count` / `_sum` / `_min` in the same transform
|
|
161
|
+
// already map their alias back through `reverseColumnMap`.
|
|
162
|
+
const field = qi.tableMeta.reverseColumnMap[col] ?? (0, schema_js_1.snakeToCamel)(col);
|
|
153
163
|
assertAggregatePiiOptIn(qi.table, meta, entry, col, 'groupBy `by` key', includePii);
|
|
154
|
-
claimResultKey(
|
|
164
|
+
claimResultKey(field, `column "${col}"`);
|
|
155
165
|
// The emitted output column is the snake_case name; claim it too (when
|
|
156
166
|
// it differs from the result key) so a JSON alias like 'created_at'
|
|
157
167
|
// cannot silently shadow the 'createdAt' group key on the wire.
|
|
158
|
-
if (col !==
|
|
168
|
+
if (col !== field)
|
|
159
169
|
claimResultKey(col, `column "${col}"`);
|
|
160
170
|
groupExprs.push(qi.q(col));
|
|
161
171
|
selectExprs.push(qi.q(col));
|
|
162
|
-
byReaders.push({ resultKey:
|
|
163
|
-
|
|
164
|
-
|
|
172
|
+
byReaders.push({ resultKey: field, rowKey: col, raw: false });
|
|
173
|
+
// Registered ONCE, under the canonical field: `orderBy` and `having`
|
|
174
|
+
// may spell the same group key the other way, and {@link lookupGroupKey}
|
|
175
|
+
// reconciles that at lookup time rather than doubling every
|
|
176
|
+
// "orderable keys" list.
|
|
177
|
+
byOrderExprs.set(field, qi.q(col));
|
|
178
|
+
havingGroupKeys.set(field, { kind: 'column', field });
|
|
165
179
|
}
|
|
166
180
|
else {
|
|
167
181
|
const col = resolveJsonPathTarget(qi, 'group key', entry.field, entry.path);
|
|
@@ -242,7 +256,9 @@ function buildGroupBy(qi, args) {
|
|
|
242
256
|
const inner = `${sqlFn}(${qi.q(col)})`;
|
|
243
257
|
const expr = aggKey === '_avg' ? qi.castAgg(inner, 'float') : inner;
|
|
244
258
|
selectExprs.push(`${expr} AS ${qi.q(`${aggKey}_${col}`)}`);
|
|
245
|
-
|
|
259
|
+
// Canonical field, matching the result bucket the transform fills;
|
|
260
|
+
// `orderBy` may spell it either way (see {@link lookupGroupKey}).
|
|
261
|
+
aggOrderExprs.set(`${aggKey}:${qi.tableMeta.reverseColumnMap[col] ?? (0, schema_js_1.snakeToCamel)(col)}`, expr);
|
|
246
262
|
continue;
|
|
247
263
|
}
|
|
248
264
|
const col = resolveJsonPathTarget(qi, `${aggKey} target "${key}"`, target.field, target.path);
|
|
@@ -397,6 +413,37 @@ function buildGroupBy(qi, args) {
|
|
|
397
413
|
* no `$n` renumbering). An aggregate key that was not requested, or an unknown
|
|
398
414
|
* by-key, throws {@link ValidationError} E003 listing the valid keys.
|
|
399
415
|
*/
|
|
416
|
+
/**
|
|
417
|
+
* Read a caller-supplied groupBy result key out of a registry keyed by the
|
|
418
|
+
* CANONICAL name (the field for a `by` column / aggregate target, the alias
|
|
419
|
+
* for a JSON group key).
|
|
420
|
+
*
|
|
421
|
+
* `by`, `orderBy` and `having` are three arguments of one call, each free to
|
|
422
|
+
* spell a column either way, so `by`'s choice must not decide what the other
|
|
423
|
+
* two may name. Reconciled here rather than by registering both spellings,
|
|
424
|
+
* which would list every group key twice in the "orderable keys" text: try the
|
|
425
|
+
* key as written (which is what carries a JSON alias, not a column), then its
|
|
426
|
+
* canonical field.
|
|
427
|
+
*/
|
|
428
|
+
function lookupGroupKey(qi, registry, key) {
|
|
429
|
+
const direct = registry.get(key);
|
|
430
|
+
if (direct !== undefined)
|
|
431
|
+
return direct;
|
|
432
|
+
const canonical = canonicalFieldName(qi, key);
|
|
433
|
+
return canonical === undefined ? undefined : registry.get(canonical);
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* The canonical FIELD name for a caller-supplied column name, or `undefined`
|
|
437
|
+
* when it names no column (a JSON group-key alias, a typo). One hop through
|
|
438
|
+
* {@link resolveColumnName} and back via `reverseColumnMap`, so both spellings
|
|
439
|
+
* land on one string.
|
|
440
|
+
*/
|
|
441
|
+
function canonicalFieldName(qi, key) {
|
|
442
|
+
const column = (0, utils_js_1.resolveColumnName)(qi.tableMeta, key);
|
|
443
|
+
if (column === undefined)
|
|
444
|
+
return undefined;
|
|
445
|
+
return qi.tableMeta.reverseColumnMap[column] ?? (0, schema_js_1.snakeToCamel)(column);
|
|
446
|
+
}
|
|
400
447
|
function buildGroupByOrderBy(qi, orderBy, byOrderExprs, aggOrderExprs) {
|
|
401
448
|
const aggBlocks = new Set(['_count', '_sum', '_avg', '_min', '_max']);
|
|
402
449
|
/** Human-readable list of every key this call can order by (for E003). */
|
|
@@ -459,7 +506,11 @@ function buildGroupByOrderBy(qi, orderBy, byOrderExprs, aggOrderExprs) {
|
|
|
459
506
|
for (const [field, dirSpec] of Object.entries(value)) {
|
|
460
507
|
if (dirSpec === undefined)
|
|
461
508
|
continue;
|
|
462
|
-
|
|
509
|
+
// `field` is the caller's spelling of the aggregate's target column;
|
|
510
|
+
// the registry is keyed by the canonical one.
|
|
511
|
+
const canonical = canonicalFieldName(qi, field);
|
|
512
|
+
const expr = aggOrderExprs.get(`${key}:${field}`) ??
|
|
513
|
+
(canonical === undefined ? undefined : aggOrderExprs.get(`${key}:${canonical}`));
|
|
463
514
|
if (!expr) {
|
|
464
515
|
throw new errors_js_1.ValidationError(`[turbine] Cannot order groupBy by "${key}.${field}" on table "${qi.table}": ` +
|
|
465
516
|
`that aggregate is not requested in this call. Orderable keys: ${validKeys()}.`);
|
|
@@ -470,7 +521,7 @@ function buildGroupByOrderBy(qi, orderBy, byOrderExprs, aggOrderExprs) {
|
|
|
470
521
|
continue;
|
|
471
522
|
}
|
|
472
523
|
// Plain by-field name or JSON group-key alias.
|
|
473
|
-
const expr = byOrderExprs
|
|
524
|
+
const expr = lookupGroupKey(qi, byOrderExprs, key);
|
|
474
525
|
if (!expr) {
|
|
475
526
|
throw new errors_js_1.ValidationError(`[turbine] Unknown field "${key}" in groupBy orderBy on table "${qi.table}". ` +
|
|
476
527
|
`Orderable keys: ${validKeys()}.`);
|
|
@@ -734,7 +785,7 @@ function buildHavingCombinator(qi, key, value, params, jsonAggExprs, groupKeys,
|
|
|
734
785
|
* its re-emitted extract expression.
|
|
735
786
|
*/
|
|
736
787
|
function buildHavingScalarClauses(qi, field, value, params, groupKeys) {
|
|
737
|
-
const ref = groupKeys
|
|
788
|
+
const ref = groupKeys ? lookupGroupKey(qi, groupKeys, field) : undefined;
|
|
738
789
|
if (!ref) {
|
|
739
790
|
const known = groupKeys ? [...groupKeys.keys()] : [];
|
|
740
791
|
throw new errors_js_1.ValidationError(`[turbine] having on "${field}" (table "${qi.table}") filters the grouped value itself, but ` +
|
|
@@ -885,25 +936,22 @@ function buildAggregate(qi, args) {
|
|
|
885
936
|
? whereMod.buildWhere(qi, aggWhere)
|
|
886
937
|
: { sql: '', params: [] };
|
|
887
938
|
const meta = qi.schema.tables[qi.table];
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
939
|
+
// Every target is validated up front, including one whose falsy value the
|
|
940
|
+
// builders below skip, and through `qi.toColumn` (the ONE
|
|
941
|
+
// `resolveColumnName` rule). `key in meta.columnMap` knows only the FIELD
|
|
942
|
+
// spelling, so the snake_case COLUMN name that `where` / `select` and even
|
|
943
|
+
// `groupBy`'s own `_min` accepted was rejected here.
|
|
944
|
+
for (const group of [args._sum, args._avg, args._min, args._max]) {
|
|
945
|
+
if (group && typeof group === 'object') {
|
|
946
|
+
for (const key of Object.keys(group))
|
|
947
|
+
qi.toColumn(key);
|
|
897
948
|
}
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
throw new errors_js_1.ValidationError((0, utils_js_1.unknownFieldMessage)(qi.table, key, meta));
|
|
905
|
-
}
|
|
906
|
-
}
|
|
949
|
+
}
|
|
950
|
+
if (args._count && typeof args._count === 'object') {
|
|
951
|
+
for (const key of Object.keys(args._count)) {
|
|
952
|
+
// `_all` is the reserved COUNT(*) selector, not a column.
|
|
953
|
+
if (key !== '_all')
|
|
954
|
+
qi.toColumn(key);
|
|
907
955
|
}
|
|
908
956
|
}
|
|
909
957
|
const selectExprs = [];
|
|
@@ -58,6 +58,7 @@ import type { PgCompatQueryResult } from '../pg-types.js';
|
|
|
58
58
|
import { type RelationDef, type SchemaMetadata, type TableMetadata } from '../schema.js';
|
|
59
59
|
import type { ReselectExecutor } from './builder.js';
|
|
60
60
|
import type { SkipGlobalFilters, Unsafe, WithClause, WithCount } from './types.js';
|
|
61
|
+
import { type ColumnNameSource } from './utils.js';
|
|
61
62
|
/**
|
|
62
63
|
* A DeferredQuery, minimally typed for what the loader consumes. Kept local to
|
|
63
64
|
* avoid a value import of builder.ts (which imports this module).
|
|
@@ -166,7 +167,18 @@ export declare function defaultProjectionFields(meta: TableMetadata, includePii:
|
|
|
166
167
|
* resolver so the two strategies refuse identically, word for word.
|
|
167
168
|
*/
|
|
168
169
|
export declare function assertProjectionShape(table: string, select: Record<string, boolean> | undefined, omit: Record<string, boolean> | undefined): void;
|
|
169
|
-
export declare function includeKeysForBatching(
|
|
170
|
+
export declare function includeKeysForBatching(
|
|
171
|
+
/**
|
|
172
|
+
* The table the projection is compiled against. `select` / `omit` keys are
|
|
173
|
+
* the CALLER's, and a column has two legal spellings there, so matching by
|
|
174
|
+
* raw key made "is the correlation key already projected?" depend on which
|
|
175
|
+
* was used: `select: { user_id: true }` with a `userId` key looked
|
|
176
|
+
* unprojected, so the key was force-added AND marked stitch-only and
|
|
177
|
+
* `stripFields` deleted the very column the caller asked for, while
|
|
178
|
+
* `omit: { user_id: true }` failed to un-omit and tripped
|
|
179
|
+
* `assertCorrelationKeyProjected`'s "bug in turbine" path on a legal query.
|
|
180
|
+
*/
|
|
181
|
+
meta: ColumnNameSource, select: Record<string, boolean> | undefined, omit: Record<string, boolean> | undefined, fields: string[],
|
|
170
182
|
/**
|
|
171
183
|
* The default projection for this table when it is NOT `select`/`omit`-driven:
|
|
172
184
|
* `hidden` are fields the default projection leaves out (today: PII-tagged
|
|
@@ -163,8 +163,12 @@ function partitionOrderBy(meta, orderBy) {
|
|
|
163
163
|
return null;
|
|
164
164
|
const out = [];
|
|
165
165
|
for (const [key, value] of entries) {
|
|
166
|
-
|
|
167
|
-
|
|
166
|
+
// The one key-resolution rule. A bare `columnMap` read knows only the
|
|
167
|
+
// FIELD spelling, so a snake-spelled orderBy declined the partition
|
|
168
|
+
// pushdown and silently fell back to fetching every child row and slicing
|
|
169
|
+
// client-side: two strategies, different bytes over the wire, one query.
|
|
170
|
+
const column = (0, utils_js_1.resolveColumnName)(meta, key);
|
|
171
|
+
if (column === undefined)
|
|
168
172
|
return null;
|
|
169
173
|
let sort;
|
|
170
174
|
let nulls;
|
|
@@ -275,7 +279,18 @@ function assertProjectionShape(table, select, omit) {
|
|
|
275
279
|
throw new errors_js_1.ValidationError((0, utils_js_1.selectOmitExclusiveMessage)(table));
|
|
276
280
|
}
|
|
277
281
|
}
|
|
278
|
-
function includeKeysForBatching(
|
|
282
|
+
function includeKeysForBatching(
|
|
283
|
+
/**
|
|
284
|
+
* The table the projection is compiled against. `select` / `omit` keys are
|
|
285
|
+
* the CALLER's, and a column has two legal spellings there, so matching by
|
|
286
|
+
* raw key made "is the correlation key already projected?" depend on which
|
|
287
|
+
* was used: `select: { user_id: true }` with a `userId` key looked
|
|
288
|
+
* unprojected, so the key was force-added AND marked stitch-only and
|
|
289
|
+
* `stripFields` deleted the very column the caller asked for, while
|
|
290
|
+
* `omit: { user_id: true }` failed to un-omit and tripped
|
|
291
|
+
* `assertCorrelationKeyProjected`'s "bug in turbine" path on a legal query.
|
|
292
|
+
*/
|
|
293
|
+
meta, select, omit, fields,
|
|
279
294
|
/**
|
|
280
295
|
* The default projection for this table when it is NOT `select`/`omit`-driven:
|
|
281
296
|
* `hidden` are fields the default projection leaves out (today: PII-tagged
|
|
@@ -289,23 +304,43 @@ function includeKeysForBatching(select, omit, fields,
|
|
|
289
304
|
*/
|
|
290
305
|
defaultProjection) {
|
|
291
306
|
const unique = [...new Set(fields)];
|
|
307
|
+
/**
|
|
308
|
+
* The caller's projection keys indexed by the column each resolves to, so a
|
|
309
|
+
* correlation key is recognized under either spelling. A key resolving to no
|
|
310
|
+
* column is left out: the projection build raises E003, where it belongs.
|
|
311
|
+
*/
|
|
312
|
+
const keyByColumn = (projection) => {
|
|
313
|
+
const byColumn = new Map();
|
|
314
|
+
for (const key of Object.keys(projection)) {
|
|
315
|
+
const column = (0, utils_js_1.resolveColumnName)(meta, key);
|
|
316
|
+
if (column !== undefined)
|
|
317
|
+
byColumn.set(column, key);
|
|
318
|
+
}
|
|
319
|
+
return byColumn;
|
|
320
|
+
};
|
|
321
|
+
/** `fields` are canonical field names, but resolve them anyway rather than assume. */
|
|
322
|
+
const columnOf = (field) => (0, utils_js_1.resolveColumnName)(meta, field) ?? field;
|
|
292
323
|
if (select) {
|
|
293
324
|
const next = { ...select };
|
|
294
325
|
const strip = [];
|
|
326
|
+
const selected = keyByColumn(select);
|
|
295
327
|
for (const f of unique) {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
328
|
+
const existing = selected.get(columnOf(f));
|
|
329
|
+
if (existing !== undefined && next[existing])
|
|
330
|
+
continue; // already projected, under either spelling
|
|
331
|
+
next[f] = true;
|
|
332
|
+
strip.push(f); // not requested by the caller, added only to stitch
|
|
300
333
|
}
|
|
301
334
|
return { select: next, omit, strip };
|
|
302
335
|
}
|
|
303
336
|
if (omit) {
|
|
304
337
|
const next = { ...omit };
|
|
305
338
|
const strip = [];
|
|
339
|
+
const omitted = keyByColumn(omit);
|
|
306
340
|
for (const f of unique) {
|
|
307
|
-
|
|
308
|
-
|
|
341
|
+
const existing = omitted.get(columnOf(f));
|
|
342
|
+
if (existing !== undefined && next[existing]) {
|
|
343
|
+
delete next[existing]; // un-omit so the key is present; the caller wanted it gone
|
|
309
344
|
strip.push(f);
|
|
310
345
|
}
|
|
311
346
|
}
|
|
@@ -633,7 +668,7 @@ async function loadToOneOrMany(ctx, parents, rel, relName, options, timeout, dep
|
|
|
633
668
|
const orderFields = pushDownLimit
|
|
634
669
|
? (windowOrder ?? []).map((o) => targetMeta.reverseColumnMap[o.column] ?? o.column)
|
|
635
670
|
: [];
|
|
636
|
-
const proj = includeKeysForBatching(options.select, options.omit, [childKeyField, ...orderFields, ...neededParentKeyFields(targetMeta, (options.with ?? {}))], defaultProjectionFields(targetMeta, ctx.includePii));
|
|
671
|
+
const proj = includeKeysForBatching(targetMeta, options.select, options.omit, [childKeyField, ...orderFields, ...neededParentKeyFields(targetMeta, (options.with ?? {}))], defaultProjectionFields(targetMeta, ctx.includePii));
|
|
637
672
|
const child = ctx.makeChild(rel.to);
|
|
638
673
|
const buildChunk = (chunk) => child.buildFindMany({
|
|
639
674
|
where: mergeChildWhere(options.where, childKeyField, chunk),
|
|
@@ -836,7 +871,7 @@ async function loadManyToMany(ctx, parents, rel, relName, options, timeout, dept
|
|
|
836
871
|
// reason as the to-many loader above: this level's PK is not the only key the
|
|
837
872
|
// recursion below will ask these rows for.
|
|
838
873
|
assertProjectionShape(targetMeta.name, options.select, options.omit);
|
|
839
|
-
const proj = includeKeysForBatching(options.select, options.omit, [targetPkField, ...neededParentKeyFields(targetMeta, (options.with ?? {}))], defaultProjectionFields(targetMeta, ctx.includePii));
|
|
874
|
+
const proj = includeKeysForBatching(targetMeta, options.select, options.omit, [targetPkField, ...neededParentKeyFields(targetMeta, (options.with ?? {}))], defaultProjectionFields(targetMeta, ctx.includePii));
|
|
840
875
|
const child = ctx.makeChild(rel.to);
|
|
841
876
|
const buildTargetChunk = (chunk) => child.buildFindMany({
|
|
842
877
|
where: mergeChildWhere(options.where, targetPkField, chunk),
|
|
@@ -538,6 +538,19 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
538
538
|
* so a warm template can never serve a call the cold path would refuse.
|
|
539
539
|
*/
|
|
540
540
|
private resolveJsonEncoding;
|
|
541
|
+
/**
|
|
542
|
+
* `args` with every `with` relation key replaced by the relation's DECLARED
|
|
543
|
+
* spelling, so a snake_case relation name resolves the way a snake_case
|
|
544
|
+
* column name already does.
|
|
545
|
+
*
|
|
546
|
+
* Returns `args` by reference when nothing needed rewriting, which is every
|
|
547
|
+
* query that already spells its relations the declared way. Runs before the
|
|
548
|
+
* stable-order pass and before `withFingerprint`, so the whole pipeline, and
|
|
549
|
+
* the SQL cache key with it, sees one spelling. The rule and the reason it is
|
|
550
|
+
* applied ONCE here rather than at each of the six `with` walkers are in
|
|
551
|
+
* query/relation-names.ts.
|
|
552
|
+
*/
|
|
553
|
+
private withDeclaredRelationNames;
|
|
541
554
|
/**
|
|
542
555
|
* Fill a PK-ascending `orderBy` into every to-many `with` relation that has no
|
|
543
556
|
* explicit one, recursing into nested `with`. Returns a CLONED clause (user
|
|
@@ -57,6 +57,7 @@ const aggMod = __importStar(require("./aggregates.js"));
|
|
|
57
57
|
const batched_loader_js_1 = require("./batched-loader.js");
|
|
58
58
|
const compound_unique_js_1 = require("./compound-unique.js");
|
|
59
59
|
const filters_js_1 = require("./filters.js");
|
|
60
|
+
const relation_names_js_1 = require("./relation-names.js");
|
|
60
61
|
const relationsMod = __importStar(require("./relations.js"));
|
|
61
62
|
const types_js_1 = require("./types.js");
|
|
62
63
|
const utils_js_1 = require("./utils.js");
|
|
@@ -1102,6 +1103,24 @@ class QueryInterface {
|
|
|
1102
1103
|
this.currentJsonEncoding = argEncoding;
|
|
1103
1104
|
return argEncoding;
|
|
1104
1105
|
}
|
|
1106
|
+
/**
|
|
1107
|
+
* `args` with every `with` relation key replaced by the relation's DECLARED
|
|
1108
|
+
* spelling, so a snake_case relation name resolves the way a snake_case
|
|
1109
|
+
* column name already does.
|
|
1110
|
+
*
|
|
1111
|
+
* Returns `args` by reference when nothing needed rewriting, which is every
|
|
1112
|
+
* query that already spells its relations the declared way. Runs before the
|
|
1113
|
+
* stable-order pass and before `withFingerprint`, so the whole pipeline, and
|
|
1114
|
+
* the SQL cache key with it, sees one spelling. The rule and the reason it is
|
|
1115
|
+
* applied ONCE here rather than at each of the six `with` walkers are in
|
|
1116
|
+
* query/relation-names.ts.
|
|
1117
|
+
*/
|
|
1118
|
+
withDeclaredRelationNames(args) {
|
|
1119
|
+
if (!args?.with)
|
|
1120
|
+
return args;
|
|
1121
|
+
const normalized = (0, relation_names_js_1.normalizeWithClause)(this.schema, this.table, args.with);
|
|
1122
|
+
return normalized === args.with ? args : { ...args, with: normalized };
|
|
1123
|
+
}
|
|
1105
1124
|
/**
|
|
1106
1125
|
* Fill a PK-ascending `orderBy` into every to-many `with` relation that has no
|
|
1107
1126
|
* explicit one, recursing into nested `with`. Returns a CLONED clause (user
|
|
@@ -1122,7 +1141,7 @@ class QueryInterface {
|
|
|
1122
1141
|
for (const [relName, spec] of Object.entries(withClause)) {
|
|
1123
1142
|
if (relName === '_count' || !spec)
|
|
1124
1143
|
continue; // `_count` is a count, not a row load
|
|
1125
|
-
const rel = (0, utils_js_1.
|
|
1144
|
+
const rel = (0, utils_js_1.resolveRelationDef)(meta.relations, relName);
|
|
1126
1145
|
if (!rel)
|
|
1127
1146
|
continue; // unknown relation, let the build path surface E005
|
|
1128
1147
|
const options = spec === true ? {} : spec;
|
|
@@ -1509,7 +1528,7 @@ class QueryInterface {
|
|
|
1509
1528
|
}
|
|
1510
1529
|
continue;
|
|
1511
1530
|
}
|
|
1512
|
-
const rel = (0, utils_js_1.
|
|
1531
|
+
const rel = (0, utils_js_1.resolveRelationDef)(this.tableMeta.relations, key);
|
|
1513
1532
|
if (!rel) {
|
|
1514
1533
|
joinWith[key] = spec; // unknown relation, let the join path surface E005
|
|
1515
1534
|
continue;
|
|
@@ -1619,7 +1638,7 @@ class QueryInterface {
|
|
|
1619
1638
|
const includePii = (0, types_js_1.resolveUnsafeFlag)(args.includePii, 'includePii');
|
|
1620
1639
|
const needed = (0, batched_loader_js_1.neededParentKeyFields)(this.tableMeta, batchedWith);
|
|
1621
1640
|
(0, batched_loader_js_1.assertProjectionShape)(this.table, args.select, args.omit);
|
|
1622
|
-
const proj = (0, batched_loader_js_1.includeKeysForBatching)(args.select, args.omit, needed, (0, batched_loader_js_1.defaultProjectionFields)(this.tableMeta, includePii));
|
|
1641
|
+
const proj = (0, batched_loader_js_1.includeKeysForBatching)(this.tableMeta, args.select, args.omit, needed, (0, batched_loader_js_1.defaultProjectionFields)(this.tableMeta, includePii));
|
|
1623
1642
|
const hasJoin = Object.keys(joinWith).length > 0;
|
|
1624
1643
|
// Force the residual `with` onto the join plan so the base query never
|
|
1625
1644
|
// re-enters this auto planning.
|
|
@@ -1757,6 +1776,10 @@ class QueryInterface {
|
|
|
1757
1776
|
* from the returned rows, so the shape matches the join strategy exactly.
|
|
1758
1777
|
*/
|
|
1759
1778
|
async runFindManyBatched(args) {
|
|
1779
|
+
// Declared relation spellings first, exactly as the join path does in
|
|
1780
|
+
// buildFindMany: the loader reads `args.with` itself, so without this the
|
|
1781
|
+
// two strategies would disagree about which relation names are valid.
|
|
1782
|
+
args = this.withDeclaredRelationNames(args);
|
|
1760
1783
|
// Stable relation order (opt-in): the batched loader forwards each relation's
|
|
1761
1784
|
// orderBy into its follow-up query, so filling the synthesized PK order here
|
|
1762
1785
|
// makes the batched output deterministic exactly like the join path.
|
|
@@ -1791,7 +1814,7 @@ class QueryInterface {
|
|
|
1791
1814
|
prepareBatchedBase(args, withClause) {
|
|
1792
1815
|
const needed = (0, batched_loader_js_1.neededParentKeyFields)(this.tableMeta, withClause);
|
|
1793
1816
|
(0, batched_loader_js_1.assertProjectionShape)(this.table, args.select, args.omit);
|
|
1794
|
-
const proj = (0, batched_loader_js_1.includeKeysForBatching)(args.select, args.omit, needed, (0, batched_loader_js_1.defaultProjectionFields)(this.tableMeta, (0, types_js_1.resolveUnsafeFlag)(args.includePii, 'includePii')));
|
|
1817
|
+
const proj = (0, batched_loader_js_1.includeKeysForBatching)(this.tableMeta, args.select, args.omit, needed, (0, batched_loader_js_1.defaultProjectionFields)(this.tableMeta, (0, types_js_1.resolveUnsafeFlag)(args.includePii, 'includePii')));
|
|
1795
1818
|
const baseArgs = {
|
|
1796
1819
|
...args,
|
|
1797
1820
|
with: undefined,
|
|
@@ -2163,6 +2186,15 @@ class QueryInterface {
|
|
|
2163
2186
|
// findUnique
|
|
2164
2187
|
// -------------------------------------------------------------------------
|
|
2165
2188
|
async findUnique(args) {
|
|
2189
|
+
// BEFORE the strategy branch, not only inside the builders, so the
|
|
2190
|
+
// join-vs-batched decision and its dev warning see the same declared name
|
|
2191
|
+
// every other stage does. `planAuto` splits `with` on the caller's keys and
|
|
2192
|
+
// `runAutoSplit` carries them onward; each downstream consumer normalizes
|
|
2193
|
+
// too, so this is not load-bearing for correctness (measured: removing it
|
|
2194
|
+
// changes the warning text from the declared name back to the caller's and
|
|
2195
|
+
// nothing else). It is here so the invariant holds at the seam rather than
|
|
2196
|
+
// depending on every consumer to re-establish it.
|
|
2197
|
+
args = this.withDeclaredRelationNames(args);
|
|
2166
2198
|
return this.executeWithMiddleware('findUnique', args, async () => {
|
|
2167
2199
|
if (args.with) {
|
|
2168
2200
|
const strategy = this.resolveLoadStrategy(args.relationLoadStrategy);
|
|
@@ -2190,6 +2222,8 @@ class QueryInterface {
|
|
|
2190
2222
|
* strategy's shape for the one row.
|
|
2191
2223
|
*/
|
|
2192
2224
|
async runFindUniqueBatched(args) {
|
|
2225
|
+
// Declared relation spellings first, see runFindManyBatched.
|
|
2226
|
+
args = this.withDeclaredRelationNames(args);
|
|
2193
2227
|
// Stable relation order (opt-in), see runFindManyBatched.
|
|
2194
2228
|
const withClause = this.resolveStableOrder(args.stableRelationOrder)
|
|
2195
2229
|
? this.applyStableRelationOrder(args.with, this.table)
|
|
@@ -2198,7 +2232,7 @@ class QueryInterface {
|
|
|
2198
2232
|
(0, batched_loader_js_1.rejectNestedPickOrder)(withClause);
|
|
2199
2233
|
const needed = (0, batched_loader_js_1.neededParentKeyFields)(this.tableMeta, withClause);
|
|
2200
2234
|
(0, batched_loader_js_1.assertProjectionShape)(this.table, args.select, args.omit);
|
|
2201
|
-
const proj = (0, batched_loader_js_1.includeKeysForBatching)(args.select, args.omit, needed, (0, batched_loader_js_1.defaultProjectionFields)(this.tableMeta, (0, types_js_1.resolveUnsafeFlag)(args.includePii, 'includePii')));
|
|
2235
|
+
const proj = (0, batched_loader_js_1.includeKeysForBatching)(this.tableMeta, args.select, args.omit, needed, (0, batched_loader_js_1.defaultProjectionFields)(this.tableMeta, (0, types_js_1.resolveUnsafeFlag)(args.includePii, 'includePii')));
|
|
2202
2236
|
const baseArgs = { ...args, with: undefined, select: proj.select, omit: proj.omit };
|
|
2203
2237
|
const deferred = this.buildFindUnique(baseArgs);
|
|
2204
2238
|
const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
|
|
@@ -2243,6 +2277,9 @@ class QueryInterface {
|
|
|
2243
2277
|
'A key whose value is `undefined` does not count, check that the value you are looking up is defined. ' +
|
|
2244
2278
|
'If you meant "any row matching an optional filter", use `findFirst`.');
|
|
2245
2279
|
}
|
|
2280
|
+
// Declared relation spellings first, before stable-order and the
|
|
2281
|
+
// fingerprint (see buildFindMany).
|
|
2282
|
+
args = this.withDeclaredRelationNames(args);
|
|
2246
2283
|
// Stable relation order (opt-in): fill PK-asc orderBy into unordered to-many
|
|
2247
2284
|
// relations before fingerprinting (see buildFindMany).
|
|
2248
2285
|
if (args.with && this.resolveStableOrder(args.stableRelationOrder)) {
|
|
@@ -2285,7 +2322,9 @@ class QueryInterface {
|
|
|
2285
2322
|
!whereObj.NOT &&
|
|
2286
2323
|
whereKeys.every((k) => {
|
|
2287
2324
|
const v = whereObj[k];
|
|
2288
|
-
|
|
2325
|
+
// Resolved, not looked up: a relation filter spelled the snake_case
|
|
2326
|
+
// way must not read as a plain equality and take the simple path.
|
|
2327
|
+
return v !== null && !(0, filters_js_1.isWhereOperator)(v) && !(0, utils_js_1.resolveRelationDef)(this.tableMeta.relations, k);
|
|
2289
2328
|
});
|
|
2290
2329
|
// Simple path: plain equality, no operators/null/OR.
|
|
2291
2330
|
//
|
|
@@ -2381,6 +2420,15 @@ class QueryInterface {
|
|
|
2381
2420
|
// findMany
|
|
2382
2421
|
// -------------------------------------------------------------------------
|
|
2383
2422
|
async findMany(args) {
|
|
2423
|
+
// BEFORE the strategy branch, not only inside the builders, so the
|
|
2424
|
+
// join-vs-batched decision and its dev warning see the same declared name
|
|
2425
|
+
// every other stage does. `planAuto` splits `with` on the caller's keys and
|
|
2426
|
+
// `runAutoSplit` carries them onward; each downstream consumer normalizes
|
|
2427
|
+
// too, so this is not load-bearing for correctness (measured: removing it
|
|
2428
|
+
// changes the warning text from the declared name back to the caller's and
|
|
2429
|
+
// nothing else). It is here so the invariant holds at the seam rather than
|
|
2430
|
+
// depending on every consumer to re-establish it.
|
|
2431
|
+
args = this.withDeclaredRelationNames(args);
|
|
2384
2432
|
this.maybeWarnUnlimited(args);
|
|
2385
2433
|
this.maybeWarnUnorderedPage(args);
|
|
2386
2434
|
// Dev-only: warn on deeply nested with clauses
|
|
@@ -2531,8 +2579,12 @@ class QueryInterface {
|
|
|
2531
2579
|
const isScalarEquality = value !== null && (typeof value !== 'object' || value instanceof Date) && typeof value !== 'function';
|
|
2532
2580
|
if (!isScalarEquality)
|
|
2533
2581
|
return false;
|
|
2534
|
-
|
|
2535
|
-
|
|
2582
|
+
// The one key-resolution rule: a bare `columnMap` read knows only the
|
|
2583
|
+
// FIELD spelling, so `where: { user_id: 1 }` on a unique column did not
|
|
2584
|
+
// count as pinning it and drew a spurious unlimited-read warning the
|
|
2585
|
+
// camelCase spelling of the same query did not.
|
|
2586
|
+
const column = (0, utils_js_1.resolveColumnName)(this.tableMeta, field);
|
|
2587
|
+
if (column === undefined)
|
|
2536
2588
|
return false;
|
|
2537
2589
|
pinned.add(column);
|
|
2538
2590
|
}
|
|
@@ -2559,6 +2611,11 @@ class QueryInterface {
|
|
|
2559
2611
|
this.currentSkip = (0, types_js_1.resolveSkipGlobalFilters)(args?.skipGlobalFilters);
|
|
2560
2612
|
// Pinned before the flatten plan and the cache key, both of which read it.
|
|
2561
2613
|
const jsonEncoding = this.resolveJsonEncoding(args?.jsonEncoding);
|
|
2614
|
+
// Relation names to their declared spelling FIRST, before the stable-order
|
|
2615
|
+
// pass and the fingerprint below and before any of the six `with` walkers,
|
|
2616
|
+
// so none of them needs to know a relation has two accepted spellings and
|
|
2617
|
+
// both spellings share one cache entry. See query/relation-names.ts.
|
|
2618
|
+
args = this.withDeclaredRelationNames(args);
|
|
2562
2619
|
// Stable relation order (opt-in): fill PK-asc orderBy into unordered to-many
|
|
2563
2620
|
// relations BEFORE fingerprinting, so the two orderings get distinct cache
|
|
2564
2621
|
// entries and every downstream path (SQL build, collect, parser) inherits it.
|
|
@@ -2670,7 +2727,13 @@ class QueryInterface {
|
|
|
2670
2727
|
// distinct key (correct, it emits a different ORDER BY).
|
|
2671
2728
|
const orderFp = args?.orderBy
|
|
2672
2729
|
? (0, filters_js_1.orderByEntries)(args.orderBy)
|
|
2673
|
-
|
|
2730
|
+
// Keyed by the DECLARED relation name (falling back to the key
|
|
2731
|
+
// itself for a column), so the two spellings of one orderBy share a
|
|
2732
|
+
// cache entry instead of minting two templates for one query.
|
|
2733
|
+
.map(([k, d]) => {
|
|
2734
|
+
const rel = (0, utils_js_1.resolveRelation)(this.tableMeta.relations, k);
|
|
2735
|
+
return `${rel?.name ?? k}:${this.orderByEntryFingerprint(d, rel?.def.to)}`;
|
|
2736
|
+
})
|
|
2674
2737
|
.join(',')
|
|
2675
2738
|
: '';
|
|
2676
2739
|
const cursorFp = args?.cursor
|
|
@@ -2792,12 +2855,27 @@ class QueryInterface {
|
|
|
2792
2855
|
// Resolve the seek direction per cursor field from the flattened
|
|
2793
2856
|
// orderBy entries (last wins, matching object-key semantics), so both
|
|
2794
2857
|
// the object and array orderBy forms drive the cursor comparison.
|
|
2795
|
-
|
|
2858
|
+
//
|
|
2859
|
+
// Indexed by the RESOLVED COLUMN, never the caller's key: both
|
|
2860
|
+
// `cursor` and `orderBy` take either spelling, so a cursor written
|
|
2861
|
+
// `{ created_at }` against `orderBy: { createdAt: 'desc' }` missed
|
|
2862
|
+
// this lookup, defaulted to ascending, and emitted `created_at > $n`
|
|
2863
|
+
// under `ORDER BY created_at DESC` — the wrong page, silently. Same
|
|
2864
|
+
// failure the `{ sort, nulls }` normalization below prevents, reached
|
|
2865
|
+
// through the spelling instead of the value shape. A relation /
|
|
2866
|
+
// JSON-path / vector key resolves to no column and is skipped.
|
|
2867
|
+
const orderDirByColumn = new Map();
|
|
2868
|
+
for (const [ok, od] of (0, filters_js_1.orderByEntries)(args.orderBy)) {
|
|
2869
|
+
const ocol = (0, utils_js_1.resolveColumnName)(this.tableMeta, ok);
|
|
2870
|
+
if (ocol !== undefined)
|
|
2871
|
+
orderDirByColumn.set(ocol, od);
|
|
2872
|
+
}
|
|
2796
2873
|
const cursorConditions = cursorEntries.map(([k, v]) => {
|
|
2797
|
-
const
|
|
2874
|
+
const rawCol = this.toColumn(k);
|
|
2875
|
+
const col = this.q(rawCol);
|
|
2798
2876
|
// orderBy values can be the { sort, nulls } spec form: normalize
|
|
2799
2877
|
// before comparing, or a desc spec would seek the ascending side.
|
|
2800
|
-
const dir =
|
|
2878
|
+
const dir = orderDirByColumn.get(rawCol);
|
|
2801
2879
|
const desc = (0, filters_js_1.isOrderBySpec)(dir) ? dir.sort === 'desc' : dir === 'desc';
|
|
2802
2880
|
const op = desc ? '<' : '>';
|
|
2803
2881
|
freshParams.push(v);
|
|
@@ -3128,6 +3206,15 @@ class QueryInterface {
|
|
|
3128
3206
|
// findFirst, like findMany but returns a single row or null
|
|
3129
3207
|
// -------------------------------------------------------------------------
|
|
3130
3208
|
async findFirst(args) {
|
|
3209
|
+
// BEFORE the strategy branch, not only inside the builders, so the
|
|
3210
|
+
// join-vs-batched decision and its dev warning see the same declared name
|
|
3211
|
+
// every other stage does. `planAuto` splits `with` on the caller's keys and
|
|
3212
|
+
// `runAutoSplit` carries them onward; each downstream consumer normalizes
|
|
3213
|
+
// too, so this is not load-bearing for correctness (measured: removing it
|
|
3214
|
+
// changes the warning text from the declared name back to the caller's and
|
|
3215
|
+
// nothing else). It is here so the invariant holds at the seam rather than
|
|
3216
|
+
// depending on every consumer to re-establish it.
|
|
3217
|
+
args = this.withDeclaredRelationNames(args);
|
|
3131
3218
|
return this.executeWithMiddleware('findFirst', (args ?? {}), async () => {
|
|
3132
3219
|
if (args?.with) {
|
|
3133
3220
|
const strategy = this.resolveLoadStrategy(args.relationLoadStrategy);
|
|
@@ -3705,9 +3792,12 @@ class QueryInterface {
|
|
|
3705
3792
|
// pick.where / pick.orderBy paths). To-one relation orderBy carries the
|
|
3706
3793
|
// target's global filter once per ordered column.
|
|
3707
3794
|
if (this.isRelationOrderByValue(dir)) {
|
|
3708
|
-
|
|
3795
|
+
// Mirrors the build path's resolution, so a cache HIT binds params for
|
|
3796
|
+
// the same relation the cached SQL was built from.
|
|
3797
|
+
const resolvedRel = (0, utils_js_1.resolveRelation)(this.tableMeta.relations, key);
|
|
3798
|
+
const relDef = resolvedRel?.def;
|
|
3709
3799
|
if (relDef && (0, filters_js_1.isRelationPickOrderBy)(dir)) {
|
|
3710
|
-
this.collectRelationPickOrderParams(
|
|
3800
|
+
this.collectRelationPickOrderParams(resolvedRel.name, relDef, dir, params);
|
|
3711
3801
|
}
|
|
3712
3802
|
else if (relDef && (relDef.type === 'hasMany' || relDef.type === 'manyToMany')) {
|
|
3713
3803
|
this.collectRelationCountParams(relDef, params);
|
|
@@ -113,7 +113,9 @@ function isRealKey(meta, key) {
|
|
|
113
113
|
return ((0, utils_js_1.ownLookup)(meta.columnMap, key) !== undefined ||
|
|
114
114
|
(0, utils_js_1.ownLookup)(meta.reverseColumnMap, key) !== undefined ||
|
|
115
115
|
meta.allColumns.includes(key) ||
|
|
116
|
-
|
|
116
|
+
// Resolved, so a relation named the snake_case way still reads as a real
|
|
117
|
+
// key here and is not mistaken for a compound-unique selector.
|
|
118
|
+
(0, utils_js_1.resolveRelationDef)(meta.relations, key) !== undefined);
|
|
117
119
|
}
|
|
118
120
|
/**
|
|
119
121
|
* A candidate compound-unique selector value: a plain object that is not a
|
|
@@ -155,16 +157,38 @@ function expandCompoundUniqueWhere(meta, where) {
|
|
|
155
157
|
continue; // unknown key, falls through to the standard E003
|
|
156
158
|
const selector = value;
|
|
157
159
|
const provided = Object.keys(selector).filter((k) => selector[k] !== undefined);
|
|
158
|
-
|
|
159
|
-
|
|
160
|
+
// Matched by the COLUMN each member name resolves to, not by the literal
|
|
161
|
+
// key. The selector's own NAME is registered under both spellings (see
|
|
162
|
+
// `register` above), so `{ org_id_user_id: { org_id, user_id } }` found the
|
|
163
|
+
// selector and was then refused for its members. Insertion order is
|
|
164
|
+
// `fields` order, keeping the expansion (and the SQL) stable.
|
|
165
|
+
const expected = new Map();
|
|
166
|
+
for (const f of fields)
|
|
167
|
+
expected.set((0, utils_js_1.resolveColumnName)(meta, f) ?? f, f);
|
|
168
|
+
/** column → the key the caller actually wrote for it. */
|
|
169
|
+
const providedByColumn = new Map();
|
|
170
|
+
let ambiguous = false;
|
|
171
|
+
for (const k of provided) {
|
|
172
|
+
const column = (0, utils_js_1.resolveColumnName)(meta, k);
|
|
173
|
+
// Unresolvable, or two spellings of one column: fall through to the
|
|
174
|
+
// same refusal an incomplete member set gets.
|
|
175
|
+
if (column === undefined || providedByColumn.has(column)) {
|
|
176
|
+
ambiguous = true;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
providedByColumn.set(column, k);
|
|
180
|
+
}
|
|
181
|
+
const exact = !ambiguous &&
|
|
182
|
+
providedByColumn.size === expected.size &&
|
|
183
|
+
[...providedByColumn.keys()].every((c) => expected.has(c));
|
|
160
184
|
if (!exact) {
|
|
161
185
|
throw new errors_js_1.ValidationError(`[turbine] Compound unique selector "${key}" on table "${meta.name}" must supply exactly ` +
|
|
162
186
|
`{ ${fields.join(', ')} }, received { ${provided.join(', ') || '(none)'} }.`);
|
|
163
187
|
}
|
|
164
188
|
result ??= { ...where };
|
|
165
189
|
delete result[key];
|
|
166
|
-
for (const field of
|
|
167
|
-
const v = selector[
|
|
190
|
+
for (const [column, field] of expected) {
|
|
191
|
+
const v = selector[providedByColumn.get(column)];
|
|
168
192
|
if (Object.hasOwn(result, field)) {
|
|
169
193
|
// A member field is ALSO given directly in the outer where: wrap the
|
|
170
194
|
// expansion in AND so neither value is clobbered.
|