turbine-orm 0.60.1 → 0.62.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 +71 -27
- package/dist/cjs/cli/config.d.ts +40 -0
- package/dist/cjs/cli/config.js +74 -2
- package/dist/cjs/cli/index.d.ts +85 -1
- package/dist/cjs/cli/index.js +374 -24
- package/dist/cjs/cli/mcp.d.ts +8 -0
- package/dist/cjs/cli/mcp.js +448 -29
- package/dist/cjs/cli/pii-tags.d.ts +64 -9
- package/dist/cjs/cli/pii-tags.js +218 -39
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.d.ts +23 -0
- package/dist/cjs/cli/studio.js +126 -53
- package/dist/cjs/cli/ui.d.ts +15 -1
- package/dist/cjs/cli/ui.js +19 -5
- package/dist/cjs/client.js +248 -11
- package/dist/cjs/errors.d.ts +38 -1
- package/dist/cjs/errors.js +235 -24
- package/dist/cjs/index.d.ts +2 -2
- package/dist/cjs/index.js +7 -2
- package/dist/cjs/pipeline-submittable.js +26 -3
- package/dist/cjs/pipeline.js +15 -2
- package/dist/cjs/powql.d.ts +12 -0
- package/dist/cjs/powql.js +46 -21
- package/dist/cjs/prisma-compat.d.ts +15 -5
- package/dist/cjs/prisma-compat.js +273 -78
- package/dist/cjs/query/aggregates.d.ts +1 -1
- package/dist/cjs/query/aggregates.js +24 -10
- package/dist/cjs/query/batched-loader.d.ts +9 -4
- package/dist/cjs/query/batched-loader.js +4 -1
- package/dist/cjs/query/builder.d.ts +47 -0
- package/dist/cjs/query/builder.js +149 -21
- package/dist/cjs/query/index.d.ts +3 -1
- package/dist/cjs/query/index.js +7 -1
- package/dist/cjs/query/option-surface.d.ts +11 -0
- package/dist/cjs/query/option-surface.js +13 -0
- package/dist/cjs/query/relations.d.ts +8 -0
- package/dist/cjs/query/relations.js +21 -1
- package/dist/cjs/query/types.d.ts +152 -18
- package/dist/cjs/query/types.js +212 -1
- package/dist/cjs/query/where.d.ts +3 -3
- package/dist/cjs/query/where.js +8 -2
- package/dist/cjs/query/writes.js +10 -9
- package/dist/cli/config.d.ts +40 -0
- package/dist/cli/config.js +73 -2
- package/dist/cli/index.d.ts +85 -1
- package/dist/cli/index.js +373 -27
- package/dist/cli/mcp.d.ts +8 -0
- package/dist/cli/mcp.js +448 -29
- package/dist/cli/pii-tags.d.ts +64 -9
- package/dist/cli/pii-tags.js +217 -39
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +23 -0
- package/dist/cli/studio.js +125 -53
- package/dist/cli/ui.d.ts +15 -1
- package/dist/cli/ui.js +18 -4
- package/dist/client.js +250 -13
- package/dist/errors.d.ts +38 -1
- package/dist/errors.js +234 -23
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -2
- package/dist/pipeline-submittable.js +26 -3
- package/dist/pipeline.js +15 -2
- package/dist/powql.d.ts +12 -0
- package/dist/powql.js +46 -21
- package/dist/prisma-compat.d.ts +15 -5
- package/dist/prisma-compat.js +274 -79
- package/dist/query/aggregates.d.ts +1 -1
- package/dist/query/aggregates.js +24 -10
- package/dist/query/batched-loader.d.ts +9 -4
- package/dist/query/batched-loader.js +4 -1
- package/dist/query/builder.d.ts +47 -0
- package/dist/query/builder.js +148 -21
- package/dist/query/index.d.ts +3 -1
- package/dist/query/index.js +2 -0
- package/dist/query/option-surface.d.ts +11 -0
- package/dist/query/option-surface.js +13 -0
- package/dist/query/relations.d.ts +8 -0
- package/dist/query/relations.js +21 -1
- package/dist/query/types.d.ts +152 -18
- package/dist/query/types.js +207 -2
- package/dist/query/where.d.ts +3 -3
- package/dist/query/where.js +8 -2
- package/dist/query/writes.js +10 -9
- package/package.json +13 -3
package/dist/query/builder.d.ts
CHANGED
|
@@ -154,6 +154,29 @@ export declare const AUTO_TO_ONE_JOIN_ROWS_MAX = 100000;
|
|
|
154
154
|
* it.
|
|
155
155
|
*/
|
|
156
156
|
export declare const AUTO_COUNT_BATCH_MIN_PARENT_ROWS = 2;
|
|
157
|
+
/**
|
|
158
|
+
* Bridge the nested-write engine's table handles onto the privilege sentinel.
|
|
159
|
+
*
|
|
160
|
+
* `nested-write.ts` clears a `set` relation by calling
|
|
161
|
+
* `updateMany({ where, data, allowFullTableScan: true })`: an INTERNAL,
|
|
162
|
+
* fully-derived predicate (the parent's own FK match, guarded there against a
|
|
163
|
+
* null reference key), not caller input, and the flag is what lets that
|
|
164
|
+
* unconditional-looking statement through the empty-`where` guard. Since
|
|
165
|
+
* `allowFullTableScan` now only accepts {@link UNSAFE}, that literal `true`
|
|
166
|
+
* would throw and every nested `set` would fail.
|
|
167
|
+
*
|
|
168
|
+
* Rather than hand the nested-write engine the sentinel (which would put the
|
|
169
|
+
* privilege value on a second, less obvious import path), the translation
|
|
170
|
+
* happens HERE, at the single seam where core hands it a table accessor. The
|
|
171
|
+
* rewrite is deliberately narrow: `updateMany` only, and only when the value is
|
|
172
|
+
* exactly `true`, so nothing else about the call is touched and no other
|
|
173
|
+
* operation gains an escape hatch.
|
|
174
|
+
*
|
|
175
|
+
* @internal Exported for its unit test; not part of the public surface.
|
|
176
|
+
*/
|
|
177
|
+
export declare function unlockNestedWriteTx<X extends {
|
|
178
|
+
table(name: string): Record<string, unknown>;
|
|
179
|
+
}>(tx: X): X;
|
|
157
180
|
export type { DeferredQuery, MiddlewareFn, QueryEvent, QueryEventListener, QueryInterfaceOptions, ReselectExecutor, TemporalInfinityReading, } from './deferred.js';
|
|
158
181
|
import type { DeferredQuery, MiddlewareFn, QueryInterfaceOptions } from './deferred.js';
|
|
159
182
|
export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
@@ -284,6 +307,12 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
284
307
|
* `orderBy` all see it without threading it through dozens of signatures.
|
|
285
308
|
* Only load-bearing when {@link globalFilters} is configured; build+collect are
|
|
286
309
|
* synchronous per call, so this transient is never observed across an await.
|
|
310
|
+
*
|
|
311
|
+
* Holds the RESOLVED form ({@link ResolvedSkipGlobalFilters}), never the raw
|
|
312
|
+
* arg: every assignment goes through `resolveSkipGlobalFilters`, which is
|
|
313
|
+
* where a non-sentinel value (`true`, `['users']`, anything a JSON body can
|
|
314
|
+
* carry) is refused. Keeping the raw value here would push that decision out
|
|
315
|
+
* to each reader, and one reader forgetting it is the whole bug class.
|
|
287
316
|
*/
|
|
288
317
|
private currentSkip;
|
|
289
318
|
/**
|
|
@@ -927,6 +956,24 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
927
956
|
update(args: UpdateArgs<T, R>): Promise<T>;
|
|
928
957
|
private nestedCreate;
|
|
929
958
|
private nestedUpdate;
|
|
959
|
+
/**
|
|
960
|
+
* Check out a pooled connection, translating a driver failure into a typed
|
|
961
|
+
* Turbine error.
|
|
962
|
+
*
|
|
963
|
+
* `pool.connect()` is where the first-run failures land: wrong password
|
|
964
|
+
* (SQLSTATE 28P01), no such database (3D000), nothing listening
|
|
965
|
+
* (ECONNREFUSED), an unverifiable TLS certificate. Unwrapped, every one of
|
|
966
|
+
* those surfaces from an ordinary `db.users.create({ data: { ...nested } })`
|
|
967
|
+
* as a raw pg `DatabaseError` whose `.code` is a SQLSTATE, on the same
|
|
968
|
+
* property Turbine puts `TURBINE_E0NN` in.
|
|
969
|
+
*
|
|
970
|
+
* client.ts has its own copy for `$transaction` / `connect()`; this one
|
|
971
|
+
* exists because `query/` must not import client.ts (circular dependency).
|
|
972
|
+
* The query paths need no equivalent: `pool.query()` opens the connection
|
|
973
|
+
* itself and rejects with the connect error, which the query boundary
|
|
974
|
+
* already wraps.
|
|
975
|
+
*/
|
|
976
|
+
private acquireConnection;
|
|
930
977
|
private runInImplicitTx;
|
|
931
978
|
private buildNestedCtx;
|
|
932
979
|
private makeTxProxy;
|
package/dist/query/builder.js
CHANGED
|
@@ -20,6 +20,7 @@ import { defaultProjectionFields, includeKeysForBatching, loadRelationsBatched,
|
|
|
20
20
|
import { expandCompoundUniqueWhere } from './compound-unique.js';
|
|
21
21
|
import { isJsonPathOrderBy, isOrderBySpec, isRelationPickOrderBy, isVectorOrderBy, isWhereOperator, orderByEntries, sortedEntries, } from './filters.js';
|
|
22
22
|
import * as relationsMod from './relations.js';
|
|
23
|
+
import { resolveSkipGlobalFilters, resolveUnsafeFlag, UNSAFE } from './types.js';
|
|
23
24
|
import { isTemporalInfinity, LRUCache, ownLookup, parseDbDate, resolveColumnName, sqlToPreparedName, unknownFieldMessage, } from './utils.js';
|
|
24
25
|
import { shouldWarnOnce, WARN_NS } from './warn-registry.js';
|
|
25
26
|
import * as whereMod from './where.js';
|
|
@@ -300,6 +301,49 @@ function maybeExpandCompoundUnique(meta, args) {
|
|
|
300
301
|
const expanded = expandCompoundUniqueWhere(meta, where);
|
|
301
302
|
return expanded === where ? args : { ...args, where: expanded };
|
|
302
303
|
}
|
|
304
|
+
/**
|
|
305
|
+
* Bridge the nested-write engine's table handles onto the privilege sentinel.
|
|
306
|
+
*
|
|
307
|
+
* `nested-write.ts` clears a `set` relation by calling
|
|
308
|
+
* `updateMany({ where, data, allowFullTableScan: true })`: an INTERNAL,
|
|
309
|
+
* fully-derived predicate (the parent's own FK match, guarded there against a
|
|
310
|
+
* null reference key), not caller input, and the flag is what lets that
|
|
311
|
+
* unconditional-looking statement through the empty-`where` guard. Since
|
|
312
|
+
* `allowFullTableScan` now only accepts {@link UNSAFE}, that literal `true`
|
|
313
|
+
* would throw and every nested `set` would fail.
|
|
314
|
+
*
|
|
315
|
+
* Rather than hand the nested-write engine the sentinel (which would put the
|
|
316
|
+
* privilege value on a second, less obvious import path), the translation
|
|
317
|
+
* happens HERE, at the single seam where core hands it a table accessor. The
|
|
318
|
+
* rewrite is deliberately narrow: `updateMany` only, and only when the value is
|
|
319
|
+
* exactly `true`, so nothing else about the call is touched and no other
|
|
320
|
+
* operation gains an escape hatch.
|
|
321
|
+
*
|
|
322
|
+
* @internal Exported for its unit test; not part of the public surface.
|
|
323
|
+
*/
|
|
324
|
+
export function unlockNestedWriteTx(tx) {
|
|
325
|
+
const wrapped = {
|
|
326
|
+
table(name) {
|
|
327
|
+
const handle = tx.table(name);
|
|
328
|
+
// A Proxy rather than a copied object: the handle is a class instance
|
|
329
|
+
// whose methods live on the prototype, so a spread would produce an
|
|
330
|
+
// object with no methods, and re-listing them by hand would silently
|
|
331
|
+
// drop any method added later.
|
|
332
|
+
return new Proxy(handle, {
|
|
333
|
+
get(target, prop) {
|
|
334
|
+
const value = Reflect.get(target, prop, target);
|
|
335
|
+
if (typeof value !== 'function')
|
|
336
|
+
return value;
|
|
337
|
+
const method = value;
|
|
338
|
+
if (prop !== 'updateMany')
|
|
339
|
+
return method.bind(target);
|
|
340
|
+
return (args) => method.call(target, args?.allowFullTableScan === true ? { ...args, allowFullTableScan: UNSAFE } : args);
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
return wrapped;
|
|
346
|
+
}
|
|
303
347
|
/**
|
|
304
348
|
* Whether a relation `with`-clause `orderBy` carries no actual ordering: an
|
|
305
349
|
* empty array, or an object with no non-`undefined` own keys. Used by
|
|
@@ -444,6 +488,12 @@ export class QueryInterface {
|
|
|
444
488
|
* `orderBy` all see it without threading it through dozens of signatures.
|
|
445
489
|
* Only load-bearing when {@link globalFilters} is configured; build+collect are
|
|
446
490
|
* synchronous per call, so this transient is never observed across an await.
|
|
491
|
+
*
|
|
492
|
+
* Holds the RESOLVED form ({@link ResolvedSkipGlobalFilters}), never the raw
|
|
493
|
+
* arg: every assignment goes through `resolveSkipGlobalFilters`, which is
|
|
494
|
+
* where a non-sentinel value (`true`, `['users']`, anything a JSON body can
|
|
495
|
+
* carry) is refused. Keeping the raw value here would push that decision out
|
|
496
|
+
* to each reader, and one reader forgetting it is the whole bug class.
|
|
447
497
|
*/
|
|
448
498
|
currentSkip;
|
|
449
499
|
/**
|
|
@@ -1335,8 +1385,13 @@ export class QueryInterface {
|
|
|
1335
1385
|
// on the batched subset up front.
|
|
1336
1386
|
rejectNestedPickOrder(batchedWith);
|
|
1337
1387
|
const skip = args.skipGlobalFilters;
|
|
1388
|
+
// Resolve the sentinel HERE, not just in buildFindMany below: this method
|
|
1389
|
+
// decides the base projection before it ever builds SQL, so a literal
|
|
1390
|
+
// `true` would otherwise shape the projection (truthily) and only be
|
|
1391
|
+
// refused one step later.
|
|
1392
|
+
const includePii = resolveUnsafeFlag(args.includePii, 'includePii');
|
|
1338
1393
|
const needed = neededParentKeyFields(this.tableMeta, batchedWith);
|
|
1339
|
-
const proj = includeKeysForBatching(args.select, args.omit, needed, defaultProjectionFields(this.tableMeta,
|
|
1394
|
+
const proj = includeKeysForBatching(args.select, args.omit, needed, defaultProjectionFields(this.tableMeta, includePii));
|
|
1340
1395
|
const hasJoin = Object.keys(joinWith).length > 0;
|
|
1341
1396
|
// Force the residual `with` onto the join plan so the base query never
|
|
1342
1397
|
// re-enters this auto planning.
|
|
@@ -1356,7 +1411,7 @@ export class QueryInterface {
|
|
|
1356
1411
|
const rows = deferred.transform(result);
|
|
1357
1412
|
const entities = single ? (rows ? [rows] : []) : rows;
|
|
1358
1413
|
if (entities.length > 0) {
|
|
1359
|
-
await loadRelationsBatched(this.batchedContext(args.timeout, skip, args.includePii
|
|
1414
|
+
await loadRelationsBatched(this.batchedContext(args.timeout, skip, args.includePii, args.forceCustomPlan === true), entities, batchedWith, args.timeout);
|
|
1360
1415
|
}
|
|
1361
1416
|
stripFields(entities, proj.strip);
|
|
1362
1417
|
return single ? (entities[0] ?? null) : entities;
|
|
@@ -1372,7 +1427,17 @@ export class QueryInterface {
|
|
|
1372
1427
|
* and unlimited-warnings silenced, a relation load must fetch every matching
|
|
1373
1428
|
* child, and the per-relation `limit` is applied client-side by the loader.
|
|
1374
1429
|
*/
|
|
1375
|
-
batchedContext(timeout, skip,
|
|
1430
|
+
batchedContext(timeout, skip,
|
|
1431
|
+
// BOTH privilege values stay in their RAW (sentinel) form here, and that is
|
|
1432
|
+
// load-bearing rather than laziness: the batched loader re-enters
|
|
1433
|
+
// `buildFindMany` one level down with these exact values on the child args,
|
|
1434
|
+
// where they are resolved (and refused) again. Handing it a plain `true`
|
|
1435
|
+
// would make every relation follow-up throw. The caller has already
|
|
1436
|
+
// resolved them once for its own projection decisions, so an invalid value
|
|
1437
|
+
// never reaches this point.
|
|
1438
|
+
includePii, forceCustomPlan = false) {
|
|
1439
|
+
// The loader's own global-filter callback below needs the RESOLVED form.
|
|
1440
|
+
const resolvedSkip = resolveSkipGlobalFilters(skip);
|
|
1376
1441
|
const childOptions = {
|
|
1377
1442
|
...this.options,
|
|
1378
1443
|
defaultLimit: undefined,
|
|
@@ -1396,7 +1461,7 @@ export class QueryInterface {
|
|
|
1396
1461
|
// so a batched load excludes/includes PII exactly as the join strategy.
|
|
1397
1462
|
includePii,
|
|
1398
1463
|
tableGlobalFilter: (table, alias, precedingParams) => {
|
|
1399
|
-
const gf = this.resolveGlobalFilter(table,
|
|
1464
|
+
const gf = this.resolveGlobalFilter(table, resolvedSkip);
|
|
1400
1465
|
if (!gf)
|
|
1401
1466
|
return null;
|
|
1402
1467
|
const meta = this.schema.tables[table];
|
|
@@ -1441,7 +1506,7 @@ export class QueryInterface {
|
|
|
1441
1506
|
const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
|
|
1442
1507
|
const entities = deferred.transform(result);
|
|
1443
1508
|
if (entities.length > 0) {
|
|
1444
|
-
await loadRelationsBatched(this.batchedContext(args.timeout, skip, args.includePii
|
|
1509
|
+
await loadRelationsBatched(this.batchedContext(args.timeout, skip, args.includePii, args.forceCustomPlan === true), entities, withClause, args.timeout);
|
|
1445
1510
|
}
|
|
1446
1511
|
stripFields(entities, strip);
|
|
1447
1512
|
return entities;
|
|
@@ -1453,7 +1518,7 @@ export class QueryInterface {
|
|
|
1453
1518
|
*/
|
|
1454
1519
|
prepareBatchedBase(args, withClause) {
|
|
1455
1520
|
const needed = neededParentKeyFields(this.tableMeta, withClause);
|
|
1456
|
-
const proj = includeKeysForBatching(args.select, args.omit, needed, defaultProjectionFields(this.tableMeta, args.includePii));
|
|
1521
|
+
const proj = includeKeysForBatching(args.select, args.omit, needed, defaultProjectionFields(this.tableMeta, resolveUnsafeFlag(args.includePii, 'includePii')));
|
|
1457
1522
|
const baseArgs = {
|
|
1458
1523
|
...args,
|
|
1459
1524
|
with: undefined,
|
|
@@ -1826,23 +1891,47 @@ export class QueryInterface {
|
|
|
1826
1891
|
// Same scope-rule parity as runFindManyBatched: reject before querying.
|
|
1827
1892
|
rejectNestedPickOrder(withClause);
|
|
1828
1893
|
const needed = neededParentKeyFields(this.tableMeta, withClause);
|
|
1829
|
-
const proj = includeKeysForBatching(args.select, args.omit, needed, defaultProjectionFields(this.tableMeta, args.includePii));
|
|
1894
|
+
const proj = includeKeysForBatching(args.select, args.omit, needed, defaultProjectionFields(this.tableMeta, resolveUnsafeFlag(args.includePii, 'includePii')));
|
|
1830
1895
|
const baseArgs = { ...args, with: undefined, select: proj.select, omit: proj.omit };
|
|
1831
1896
|
const deferred = this.buildFindUnique(baseArgs);
|
|
1832
1897
|
const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
|
|
1833
1898
|
const entity = deferred.transform(result);
|
|
1834
1899
|
if (!entity)
|
|
1835
1900
|
return null;
|
|
1836
|
-
await loadRelationsBatched(this.batchedContext(args.timeout, args.skipGlobalFilters, args.includePii
|
|
1901
|
+
await loadRelationsBatched(this.batchedContext(args.timeout, args.skipGlobalFilters, args.includePii, args.forceCustomPlan === true), [entity], withClause, args.timeout);
|
|
1837
1902
|
stripFields([entity], proj.strip);
|
|
1838
1903
|
return entity;
|
|
1839
1904
|
}
|
|
1840
1905
|
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause", matches TypedWithClause default
|
|
1841
1906
|
buildFindUnique(args) {
|
|
1842
|
-
this.currentSkip = args.skipGlobalFilters;
|
|
1907
|
+
this.currentSkip = resolveSkipGlobalFilters(args.skipGlobalFilters);
|
|
1843
1908
|
// Prisma compound-unique selector expansion (before global-filter merge and
|
|
1844
1909
|
// fingerprinting, so the cache only ever sees the canonical expanded where).
|
|
1845
1910
|
args = maybeExpandCompoundUnique(this.tableMeta, args);
|
|
1911
|
+
// A findUnique whose `where` carries NO predicate must not run.
|
|
1912
|
+
//
|
|
1913
|
+
// `{ id: undefined }` is what `{ id: req.params.id }` becomes on a request
|
|
1914
|
+
// that omitted the parameter, or on a typo'd one. Undefined keys are dropped
|
|
1915
|
+
// downstream, so that used to emit `SELECT … FROM t LIMIT 1`: no predicate,
|
|
1916
|
+
// and the caller gets an ARBITRARY row where it asked for a specific one and
|
|
1917
|
+
// would have handled `null`. On an authorization check keyed by id that is a
|
|
1918
|
+
// silent cross-record read, and `findUniqueOrThrow` made it worse by
|
|
1919
|
+
// promising to throw when nothing matched and then returning a stranger's
|
|
1920
|
+
// row instead.
|
|
1921
|
+
//
|
|
1922
|
+
// Checked against the USER's where, before the global-filter merge, and
|
|
1923
|
+
// deliberately so: a tenant filter is not a unique selector, and letting it
|
|
1924
|
+
// satisfy this would still hand back an arbitrary row from inside the tenant.
|
|
1925
|
+
//
|
|
1926
|
+
// `findFirst` is intentionally NOT guarded. "The first row matching an
|
|
1927
|
+
// optional filter" is its whole contract and Prisma's `findFirst` behaves
|
|
1928
|
+
// the same way; ask for one row by identity with `findUnique`.
|
|
1929
|
+
if (whereMod.userPredicateIsEmpty(this.ctx, (args.where ?? {}))) {
|
|
1930
|
+
throw new ValidationError(`[turbine] findUnique on "${this.table}" refused: the \`where\` clause has no predicate, ` +
|
|
1931
|
+
'so this would return an arbitrary row rather than a specific one. ' +
|
|
1932
|
+
'A key whose value is `undefined` does not count, check that the value you are looking up is defined. ' +
|
|
1933
|
+
'If you meant "any row matching an optional filter", use `findFirst`.');
|
|
1934
|
+
}
|
|
1846
1935
|
// Stable relation order (opt-in): fill PK-asc orderBy into unordered to-many
|
|
1847
1936
|
// relations before fingerprinting (see buildFindMany).
|
|
1848
1937
|
if (args.with && this.resolveStableOrder(args.stableRelationOrder)) {
|
|
@@ -1850,7 +1939,8 @@ export class QueryInterface {
|
|
|
1850
1939
|
if (normalized !== args.with)
|
|
1851
1940
|
args = { ...args, with: normalized };
|
|
1852
1941
|
}
|
|
1853
|
-
|
|
1942
|
+
// Resolved once, see buildFindMany.
|
|
1943
|
+
const includePii = resolveUnsafeFlag(args.includePii, 'includePii');
|
|
1854
1944
|
// findUnique is never flatten-planned: it reads ONE parent row, so the
|
|
1855
1945
|
// correlated subquery already runs exactly once and a join buys nothing.
|
|
1856
1946
|
// Say so, because the caller did ask for a strategy that is not running.
|
|
@@ -1961,7 +2051,7 @@ export class QueryInterface {
|
|
|
1961
2051
|
this.collectWhereParams(whereObj, params);
|
|
1962
2052
|
this.collectWithParams(args.with, params);
|
|
1963
2053
|
this.crossCheckCache('findUnique', ck, entry, buildSql, params);
|
|
1964
|
-
const parseWith = this.makeNestedParser(args.with,
|
|
2054
|
+
const parseWith = this.makeNestedParser(args.with, includePii);
|
|
1965
2055
|
return {
|
|
1966
2056
|
sql: entry.sql,
|
|
1967
2057
|
params,
|
|
@@ -2153,7 +2243,7 @@ export class QueryInterface {
|
|
|
2153
2243
|
}
|
|
2154
2244
|
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause", matches TypedWithClause default
|
|
2155
2245
|
buildFindMany(args) {
|
|
2156
|
-
this.currentSkip = args?.skipGlobalFilters;
|
|
2246
|
+
this.currentSkip = resolveSkipGlobalFilters(args?.skipGlobalFilters);
|
|
2157
2247
|
// Stable relation order (opt-in): fill PK-asc orderBy into unordered to-many
|
|
2158
2248
|
// relations BEFORE fingerprinting, so the two orderings get distinct cache
|
|
2159
2249
|
// entries and every downstream path (SQL build, collect, parser) inherits it.
|
|
@@ -2194,7 +2284,10 @@ export class QueryInterface {
|
|
|
2194
2284
|
}
|
|
2195
2285
|
}
|
|
2196
2286
|
}
|
|
2197
|
-
|
|
2287
|
+
// Resolve the PII opt-in ONCE, before any projection decision. The value is
|
|
2288
|
+
// the UNSAFE sentinel or nothing: a literal `true` (what a spread request
|
|
2289
|
+
// body carries) throws here rather than unlocking the projection.
|
|
2290
|
+
const includePii = resolveUnsafeFlag(args?.includePii, 'includePii');
|
|
2198
2291
|
const columnsList = this.resolveColumns(args?.select, args?.omit, includePii);
|
|
2199
2292
|
const colKey = columnsList ? columnsList.join(',') : '*';
|
|
2200
2293
|
// AND-merge this table's global filter into the user where; `hasWhere` gates
|
|
@@ -2445,9 +2538,14 @@ export class QueryInterface {
|
|
|
2445
2538
|
// function of the schema, the `with` shape and `includePii`, never of
|
|
2446
2539
|
// `limit`, so the batch-size override the speculative fetch applies cannot
|
|
2447
2540
|
// change it, and the stream's parser matches the emitted SQL.
|
|
2448
|
-
|
|
2541
|
+
// Resolved once for the whole stream: the flatten plan and the row parser
|
|
2542
|
+
// MUST agree with the SQL buildFindMany emits below, and reading the raw
|
|
2543
|
+
// sentinel with `=== true` here would have quietly planned a no-PII parser
|
|
2544
|
+
// over a with-PII statement.
|
|
2545
|
+
const streamPii = resolveUnsafeFlag(args?.includePii, 'includePii');
|
|
2546
|
+
const streamFlattenPlan = hasRelations ? this.planFlatten(args, streamPii) : null;
|
|
2449
2547
|
const parseWith = hasRelations
|
|
2450
|
-
? this.makeNestedParser(args.with,
|
|
2548
|
+
? this.makeNestedParser(args.with, streamPii, streamFlattenPlan)
|
|
2451
2549
|
: null;
|
|
2452
2550
|
// --- Speculative first fetch: try to satisfy the entire drain in one RTT ---
|
|
2453
2551
|
const speculativeDeferred = this.buildFindMany({
|
|
@@ -2481,7 +2579,7 @@ export class QueryInterface {
|
|
|
2481
2579
|
// connection, so `pool.query` already IS that connection. The stream rides
|
|
2482
2580
|
// on it, is never released here, and the dialect is told to emit no
|
|
2483
2581
|
// transaction control of its own (`ambientTransaction`).
|
|
2484
|
-
const client = this.txScoped ? null : await this.
|
|
2582
|
+
const client = this.txScoped ? null : await this.acquireConnection();
|
|
2485
2583
|
const conn = client ?? {
|
|
2486
2584
|
query: async (text, values) => (await this.pool.query(text, values)),
|
|
2487
2585
|
};
|
|
@@ -2663,8 +2761,33 @@ export class QueryInterface {
|
|
|
2663
2761
|
return result;
|
|
2664
2762
|
});
|
|
2665
2763
|
}
|
|
2764
|
+
/**
|
|
2765
|
+
* Check out a pooled connection, translating a driver failure into a typed
|
|
2766
|
+
* Turbine error.
|
|
2767
|
+
*
|
|
2768
|
+
* `pool.connect()` is where the first-run failures land: wrong password
|
|
2769
|
+
* (SQLSTATE 28P01), no such database (3D000), nothing listening
|
|
2770
|
+
* (ECONNREFUSED), an unverifiable TLS certificate. Unwrapped, every one of
|
|
2771
|
+
* those surfaces from an ordinary `db.users.create({ data: { ...nested } })`
|
|
2772
|
+
* as a raw pg `DatabaseError` whose `.code` is a SQLSTATE, on the same
|
|
2773
|
+
* property Turbine puts `TURBINE_E0NN` in.
|
|
2774
|
+
*
|
|
2775
|
+
* client.ts has its own copy for `$transaction` / `connect()`; this one
|
|
2776
|
+
* exists because `query/` must not import client.ts (circular dependency).
|
|
2777
|
+
* The query paths need no equivalent: `pool.query()` opens the connection
|
|
2778
|
+
* itself and rejects with the connect error, which the query boundary
|
|
2779
|
+
* already wraps.
|
|
2780
|
+
*/
|
|
2781
|
+
async acquireConnection() {
|
|
2782
|
+
try {
|
|
2783
|
+
return await this.pool.connect();
|
|
2784
|
+
}
|
|
2785
|
+
catch (err) {
|
|
2786
|
+
throw wrapPgError(err);
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2666
2789
|
async runInImplicitTx(fn) {
|
|
2667
|
-
const client = await this.
|
|
2790
|
+
const client = await this.acquireConnection();
|
|
2668
2791
|
let began = false;
|
|
2669
2792
|
try {
|
|
2670
2793
|
await client.query(this.dialect.beginStatement());
|
|
@@ -2681,8 +2804,12 @@ export class QueryInterface {
|
|
|
2681
2804
|
// older-engine client falls back to the full capability set inside the
|
|
2682
2805
|
// implicit transaction.
|
|
2683
2806
|
this.pool);
|
|
2684
|
-
|
|
2685
|
-
|
|
2807
|
+
const ctx = {
|
|
2808
|
+
schema: this.schema,
|
|
2809
|
+
// biome-ignore lint/suspicious/noExplicitAny: TransactionClient satisfies NestedWriteContext['tx'] at runtime
|
|
2810
|
+
tx: unlockNestedWriteTx(tx),
|
|
2811
|
+
scopedConnect: this.scopedConnect,
|
|
2812
|
+
};
|
|
2686
2813
|
const result = await fn(ctx);
|
|
2687
2814
|
await client.query(this.dialect.commitStatement());
|
|
2688
2815
|
return result;
|
|
@@ -2711,7 +2838,7 @@ export class QueryInterface {
|
|
|
2711
2838
|
const opts = { ...this.options, _txScoped: true };
|
|
2712
2839
|
return {
|
|
2713
2840
|
schema,
|
|
2714
|
-
tx: this.makeTxProxy(pool, schema, middlewares, opts),
|
|
2841
|
+
tx: unlockNestedWriteTx(this.makeTxProxy(pool, schema, middlewares, opts)),
|
|
2715
2842
|
};
|
|
2716
2843
|
}
|
|
2717
2844
|
// biome-ignore lint/suspicious/noExplicitAny: bridges MiddlewareFn[] ↔ Middleware[] and QI ↔ NestedWriteContext type gap
|
|
@@ -2769,7 +2896,7 @@ export class QueryInterface {
|
|
|
2769
2896
|
});
|
|
2770
2897
|
}
|
|
2771
2898
|
buildCount(args) {
|
|
2772
|
-
this.currentSkip = args?.skipGlobalFilters;
|
|
2899
|
+
this.currentSkip = resolveSkipGlobalFilters(args?.skipGlobalFilters);
|
|
2773
2900
|
const effWhere = this.mergeGlobalFilter(args?.where);
|
|
2774
2901
|
const hasWhere = effWhere !== undefined;
|
|
2775
2902
|
const whereObj = (effWhere ?? {});
|
package/dist/query/index.d.ts
CHANGED
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
* `import { … } from './query/index.js'` is a drop-in replacement for the
|
|
6
6
|
* former monolithic `import { … } from './query.js'`.
|
|
7
7
|
*/
|
|
8
|
-
export type { AggregateArgs, AggregateResult, ArrayFilter, ColumnRef, ConnectOrCreateOp, CountArgs, CreateArgs, CreateDataInput, CreateManyArgs, DeleteArgs, DeleteManyArgs, FieldResult, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GlobalFilters, GroupByAggregateSpec, GroupByArgs, GroupByDistinctOn, GroupByResult, HavingClause, JsonFilter, JsonPathAggregateTarget, JsonPathGroupKey, JsonPathOrderBy, NestedCreateOp, NestedUpdateOp, NestedUpdateOpItem, NestedUpsertOpItem, OmitResult, OrderByClause, OrderByObject, OrderDirection, QueryResult, RelationDescriptor, RelationFilter, RelationLoadStrategy, RelationPickBy, RelationPickOrderBy, SelectResult, SkipGlobalFilters, TextSearchFilter, TypedWithClause, UpdateArgs, UpdateDataInput, UpdateInput, UpdateManyArgs, UpdateOperatorInput, UpsertArgs, VectorDistanceFilter, VectorFilter, VectorMetric, VectorOrderBy, VectorOrderByDistance, WhereClause, WhereOperator, WhereValue, WithClause, WithOptions, WithOrderByObject, WithResult, } from './types.js';
|
|
8
|
+
export type { AggregateArgs, AggregateResult, ArrayFilter, ColumnRef, ConnectOrCreateOp, CountArgs, CreateArgs, CreateDataInput, CreateManyArgs, DeleteArgs, DeleteManyArgs, FieldResult, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GlobalFilters, GroupByAggregateSpec, GroupByArgs, GroupByDistinctOn, GroupByResult, HavingClause, JsonFilter, JsonPathAggregateTarget, JsonPathGroupKey, JsonPathOrderBy, NestedCreateOp, NestedUpdateOp, NestedUpdateOpItem, NestedUpsertOpItem, OmitResult, OrderByClause, OrderByObject, OrderDirection, QueryResult, RelationDescriptor, RelationFilter, RelationLoadStrategy, RelationPickBy, RelationPickOrderBy, ResolvedSkipGlobalFilters, SelectResult, SkipGlobalFilters, TextSearchFilter, TypedWithClause, UpdateArgs, UpdateDataInput, UpdateInput, UpdateManyArgs, UpdateOperatorInput, UpsertArgs, VectorDistanceFilter, VectorFilter, VectorMetric, VectorOrderBy, VectorOrderByDistance, WhereClause, WhereOperator, WhereValue, WithClause, WithOptions, WithOrderByObject, WithResult, } from './types.js';
|
|
9
9
|
export type { BuiltStatement, BulkInsertStatementInput, ColumnDefinitionInput, ColumnTypeInput, CreateIndexStatementInput, CreateTableStatementInput, Dialect, InsertStatementInput, UpsertStatementInput, } from '../dialect.js';
|
|
10
10
|
export { postgresDialect } from '../dialect.js';
|
|
11
11
|
export type { OptionKind, OptionTable } from './option-surface.js';
|
|
12
12
|
export { AGGREGATE_OPTIONS, ALL_OPTION_TABLES, applyNativeOptions, COUNT_OPTIONS, CREATE_MANY_OPTIONS, CREATE_OPTIONS, DELETE_MANY_OPTIONS, DELETE_OPTIONS, FIND_MANY_OPTIONS, FIND_MANY_STREAM_OPTIONS, FIND_UNIQUE_OPTIONS, GROUP_BY_OPTIONS, optionKeysOfKind, UPDATE_MANY_OPTIONS, UPDATE_OPTIONS, UPSERT_OPTIONS, } from './option-surface.js';
|
|
13
|
+
export type { PrivilegeOption, Unsafe } from './types.js';
|
|
14
|
+
export { assertOrderDirection, resolveSkipGlobalFilters, resolveUnsafeFlag, UNSAFE } from './types.js';
|
|
13
15
|
export type { SqlCacheEntry } from './utils.js';
|
|
14
16
|
export { buildCorrelation, escapeLike, escSingleQuote, fnv1a64Hex, LRUCache, OPERATOR_KEYS, quoteIdent, sqlToPreparedName, } from './utils.js';
|
|
15
17
|
export type { DeferredQuery, MiddlewareFn, QueryEvent, QueryEventListener, QueryInterfaceOptions, ReselectExecutor, TemporalInfinityReading, } from './builder.js';
|
package/dist/query/index.js
CHANGED
|
@@ -7,5 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export { postgresDialect } from '../dialect.js';
|
|
9
9
|
export { AGGREGATE_OPTIONS, ALL_OPTION_TABLES, applyNativeOptions, COUNT_OPTIONS, CREATE_MANY_OPTIONS, CREATE_OPTIONS, DELETE_MANY_OPTIONS, DELETE_OPTIONS, FIND_MANY_OPTIONS, FIND_MANY_STREAM_OPTIONS, FIND_UNIQUE_OPTIONS, GROUP_BY_OPTIONS, optionKeysOfKind, UPDATE_MANY_OPTIONS, UPDATE_OPTIONS, UPSERT_OPTIONS, } from './option-surface.js';
|
|
10
|
+
// The privilege sentinel: a runtime value, not a type (see types.ts).
|
|
11
|
+
export { assertOrderDirection, resolveSkipGlobalFilters, resolveUnsafeFlag, UNSAFE } from './types.js';
|
|
10
12
|
export { buildCorrelation, escapeLike, escSingleQuote, fnv1a64Hex, LRUCache, OPERATOR_KEYS, quoteIdent, sqlToPreparedName, } from './utils.js';
|
|
11
13
|
export { AUTO_ASSUMED_ROUND_TRIP_MS, AUTO_COUNT_BATCH_MIN_PARENT_ROWS, AUTO_JOIN_PENALTY_MS_PER_ROW, AUTO_TO_ONE_JOIN_MAX_ROWS, AUTO_TO_ONE_JOIN_ROWS_MAX, AUTO_TO_ONE_JOIN_ROWS_MIN, QueryInterface, } from './builder.js';
|
|
@@ -38,6 +38,17 @@
|
|
|
38
38
|
* column, or model NAME. If the value names anything in the schema, it is
|
|
39
39
|
* `'prisma'`: a name-translating consumer has to walk it by hand.
|
|
40
40
|
*
|
|
41
|
+
* ## The privilege options
|
|
42
|
+
*
|
|
43
|
+
* `skipGlobalFilters`, `includePii` and `allowFullTableScan` are `'native'` and
|
|
44
|
+
* MUST stay that way. Their values are the `UNSAFE` sentinel (plus, for
|
|
45
|
+
* skipGlobalFilters, table ACCESSOR names, which the rule above explicitly
|
|
46
|
+
* allows), and forwarding them verbatim is what lets core be the single place
|
|
47
|
+
* that decides whether a value is a real opt-in. Hand-translating them here
|
|
48
|
+
* would mean a second copy of that decision, and a consumer's
|
|
49
|
+
* `includePii: true` would then be judged by whichever copy it happened to
|
|
50
|
+
* reach first.
|
|
51
|
+
*
|
|
41
52
|
* @module
|
|
42
53
|
*/
|
|
43
54
|
import type { AggregateArgs, CountArgs, CreateArgs, CreateManyArgs, DeleteArgs, DeleteManyArgs, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GroupByArgs, UpdateArgs, UpdateManyArgs, UpsertArgs } from './types.js';
|
|
@@ -38,6 +38,17 @@
|
|
|
38
38
|
* column, or model NAME. If the value names anything in the schema, it is
|
|
39
39
|
* `'prisma'`: a name-translating consumer has to walk it by hand.
|
|
40
40
|
*
|
|
41
|
+
* ## The privilege options
|
|
42
|
+
*
|
|
43
|
+
* `skipGlobalFilters`, `includePii` and `allowFullTableScan` are `'native'` and
|
|
44
|
+
* MUST stay that way. Their values are the `UNSAFE` sentinel (plus, for
|
|
45
|
+
* skipGlobalFilters, table ACCESSOR names, which the rule above explicitly
|
|
46
|
+
* allows), and forwarding them verbatim is what lets core be the single place
|
|
47
|
+
* that decides whether a value is a real opt-in. Hand-translating them here
|
|
48
|
+
* would mean a second copy of that decision, and a consumer's
|
|
49
|
+
* `includePii: true` would then be judged by whichever copy it happened to
|
|
50
|
+
* reach first.
|
|
51
|
+
*
|
|
41
52
|
* @module
|
|
42
53
|
*/
|
|
43
54
|
export const FIND_UNIQUE_OPTIONS = {
|
|
@@ -52,6 +63,8 @@ export const FIND_UNIQUE_OPTIONS = {
|
|
|
52
63
|
relationLoadStrategy: 'prisma',
|
|
53
64
|
timeout: 'native',
|
|
54
65
|
stableRelationOrder: 'native',
|
|
66
|
+
// Privilege options: forwarded verbatim, refused by core unless the value is
|
|
67
|
+
// the UNSAFE sentinel. See the note at the top of this file.
|
|
55
68
|
skipGlobalFilters: 'native',
|
|
56
69
|
includePii: 'native',
|
|
57
70
|
forceCustomPlan: 'native',
|
|
@@ -59,6 +59,14 @@ export declare function collectRelationSubqueryParams(qi: BuilderCtx, relDef: Re
|
|
|
59
59
|
* ORDER BY differs only in nulls placement, vector metric, or relation-count
|
|
60
60
|
* vs relation-column never collide on one cached SQL string. Captures the
|
|
61
61
|
* SQL-shaping bits (direction, nulls, metric, relation keys), never values.
|
|
62
|
+
*
|
|
63
|
+
* NOTE for the direction guard (see {@link assertOrderDirection}): every
|
|
64
|
+
* direction is embedded here BYTE-FOR-BYTE (`String(d)` for the plain form,
|
|
65
|
+
* `d.sort` / `d.direction` for the spec forms), never normalized to ASC/DESC.
|
|
66
|
+
* That is what lets the direction validation live on the BUILD side alone: a
|
|
67
|
+
* bad direction has a fingerprint no good direction can produce, so it can
|
|
68
|
+
* never be served by a warm cache entry whose build already validated. Do not
|
|
69
|
+
* "tidy" this into a normalized token.
|
|
62
70
|
*/
|
|
63
71
|
export declare function orderByEntryFingerprint(qi: BuilderCtx, d: unknown, targetTable?: string): string;
|
|
64
72
|
export declare function buildOrderBy(qi: BuilderCtx, orderBy: OrderByClause, params?: unknown[], lateralSink?: string[]): string;
|
package/dist/query/relations.js
CHANGED
|
@@ -17,6 +17,7 @@ import { missingIndexForRelation } from '../index-advisor.js';
|
|
|
17
17
|
import { camelToSnake, normalizeKeyColumns, snakeToCamel } from '../schema.js';
|
|
18
18
|
import { resolveCountRelations } from './batched-loader.js';
|
|
19
19
|
import { isJsonPathOrderBy, isOrderBySpec, isRelationPickOrderBy, isVectorOrderBy, normalizeOrderBy, orderByEntries, sortedEntries, } from './filters.js';
|
|
20
|
+
import { assertDirectionToken, assertOrderDirection } from './types.js';
|
|
20
21
|
import { ownLookup } from './utils.js';
|
|
21
22
|
import { hasWarnedOnce, shouldWarnOnce, WARN_NS } from './warn-registry.js';
|
|
22
23
|
import * as whereMod from './where.js';
|
|
@@ -42,7 +43,7 @@ export function resolveColumns(qi, select, omit, includePii) {
|
|
|
42
43
|
.map(([k]) => qi.toColumn(k));
|
|
43
44
|
}
|
|
44
45
|
// Default / omit-only projection: PII-tagged columns are excluded unless the
|
|
45
|
-
// caller
|
|
46
|
+
// caller opted in with `includePii: UNSAFE`. An empty set (untagged schema) keeps the
|
|
46
47
|
// `null`/`*` fast path so the emitted SQL is byte-identical to before.
|
|
47
48
|
const piiCols = includePii ? undefined : writesMod.piiColumns(qi, qi.tableMeta);
|
|
48
49
|
const hasPii = piiCols !== undefined && piiCols.size > 0;
|
|
@@ -264,6 +265,14 @@ export function collectRelationSubqueryParams(qi, relDef, spec, params, _parentR
|
|
|
264
265
|
* ORDER BY differs only in nulls placement, vector metric, or relation-count
|
|
265
266
|
* vs relation-column never collide on one cached SQL string. Captures the
|
|
266
267
|
* SQL-shaping bits (direction, nulls, metric, relation keys), never values.
|
|
268
|
+
*
|
|
269
|
+
* NOTE for the direction guard (see {@link assertOrderDirection}): every
|
|
270
|
+
* direction is embedded here BYTE-FOR-BYTE (`String(d)` for the plain form,
|
|
271
|
+
* `d.sort` / `d.direction` for the spec forms), never normalized to ASC/DESC.
|
|
272
|
+
* That is what lets the direction validation live on the BUILD side alone: a
|
|
273
|
+
* bad direction has a fingerprint no good direction can produce, so it can
|
|
274
|
+
* never be served by a warm cache entry whose build already validated. Do not
|
|
275
|
+
* "tidy" this into a normalized token.
|
|
267
276
|
*/
|
|
268
277
|
export function orderByEntryFingerprint(qi, d, targetTable) {
|
|
269
278
|
// Vector KNN ordering changes the emitted operator by metric and adds a
|
|
@@ -347,6 +356,7 @@ export function buildOrderBy(qi, orderBy, params, lateralSink) {
|
|
|
347
356
|
const rawColumn = qi.toColumn(key);
|
|
348
357
|
const operator = whereMod.vectorOperator(qi, key, rawColumn, value.distance.metric);
|
|
349
358
|
const placeholder = whereMod.pushVectorParam(qi, key, rawColumn, value.distance.to, params);
|
|
359
|
+
assertDirectionToken(value.distance.direction, `vector distance orderBy on "${key}"`);
|
|
350
360
|
const safeDir = value.distance.direction?.toLowerCase() === 'desc' ? 'DESC' : 'ASC';
|
|
351
361
|
return `${qi.q(rawColumn)} ${operator} ${placeholder} ${safeDir}`;
|
|
352
362
|
}
|
|
@@ -366,6 +376,10 @@ export function buildOrderBy(qi, orderBy, params, lateralSink) {
|
|
|
366
376
|
throw new ValidationError(`[turbine] Unknown field "${key}" in orderBy on table "${qi.table}". ` +
|
|
367
377
|
`Known fields: ${Object.keys(meta.columnMap).join(', ') || '(none)'}.`);
|
|
368
378
|
}
|
|
379
|
+
// Refuse a direction that is neither asc nor desc. normalizeOrderBy is
|
|
380
|
+
// `=== 'desc' ? DESC : ASC`, so without this every typo sorted ASCENDING
|
|
381
|
+
// and returned a correct-looking page in the reverse order.
|
|
382
|
+
assertOrderDirection(value, `orderBy "${key}" on table "${qi.table}"`);
|
|
369
383
|
const { dir, nulls } = normalizeOrderBy(value);
|
|
370
384
|
return `${qi.toSqlColumn(key)} ${dir}${nullsSuffix(qi, nulls)}`;
|
|
371
385
|
})
|
|
@@ -452,6 +466,7 @@ export function buildJsonPathOrderEntry(qi, table, meta, field, spec, prefix, pa
|
|
|
452
466
|
params.push(whereMod.jsonPathParam(qi, spec.path));
|
|
453
467
|
const extract = qi.dialect.buildJsonPathExtract(`${prefix}${qi.q(col)}`, qi.p(params.length));
|
|
454
468
|
const lhs = spec.type === 'numeric' ? whereMod.castJsonNumeric(qi, extract) : extract;
|
|
469
|
+
assertDirectionToken(spec.direction, `JSON-path orderBy on "${field}"`);
|
|
455
470
|
const dir = spec.direction?.toLowerCase() === 'desc' ? 'DESC' : 'ASC';
|
|
456
471
|
// Rows whose document lacks the path extract to NULL. Without a nulls
|
|
457
472
|
// clause, Postgres DESC defaults to NULLS FIRST, which both diverges from
|
|
@@ -546,6 +561,7 @@ function buildChainedToOneOrderBy(qi, head, nextRelName, nextValue, params) {
|
|
|
546
561
|
if (!currentMeta.allColumns.includes(snakeCol)) {
|
|
547
562
|
throw new ValidationError(`[turbine] Unknown column "${key}" in orderBy on relation "${path.join('.')}" (table "${currentMeta.name}").`);
|
|
548
563
|
}
|
|
564
|
+
assertOrderDirection(entryValue, `orderBy on relation path "${path.join('.')}"`);
|
|
549
565
|
const { dir, nulls } = normalizeOrderBy(entryValue);
|
|
550
566
|
let where = head.correlation;
|
|
551
567
|
if (params) {
|
|
@@ -608,6 +624,7 @@ export function buildRelationOrderBy(qi, relName, value, alias, params, ctx, lat
|
|
|
608
624
|
throw new ValidationError(`[turbine] orderBy on to-many relation "${relName}" only supports "_count" ` +
|
|
609
625
|
`or a pick-row ordering ({ pick, by }) (got: ${keys.join(', ') || '(empty)'}).`);
|
|
610
626
|
}
|
|
627
|
+
assertOrderDirection(value._count, `orderBy { ${relName}: { _count } }`);
|
|
611
628
|
const { dir } = normalizeOrderBy(value._count);
|
|
612
629
|
return `${buildRelationCountExpr(qi, relDef, parentRef, alias, params)} ${dir}`;
|
|
613
630
|
}
|
|
@@ -645,6 +662,7 @@ export function buildRelationOrderBy(qi, relName, value, alias, params, ctx, lat
|
|
|
645
662
|
: '';
|
|
646
663
|
throw new ValidationError(`[turbine] Unknown column "${col}" in orderBy on relation "${relName}" (table "${relDef.to}").${relationHint}`);
|
|
647
664
|
}
|
|
665
|
+
assertOrderDirection(dirValue, `orderBy "${col}" on relation "${relName}"`);
|
|
648
666
|
const { dir, nulls } = normalizeOrderBy(dirValue);
|
|
649
667
|
// Target's global filter applies here too, otherwise ordering keys off
|
|
650
668
|
// a soft-deleted / other-tenant related row's value (matches the with
|
|
@@ -746,6 +764,7 @@ export function buildRelationPickOrderBy(qi, relName, relDef, spec, alias, paren
|
|
|
746
764
|
const targetMeta = qi.schema.tables[relDef.to];
|
|
747
765
|
if (!targetMeta)
|
|
748
766
|
throw new RelationError(`[turbine] Unknown relation target "${relDef.to}"`);
|
|
767
|
+
assertDirectionToken(spec.direction, `pick-row orderBy on relation "${relName}"`);
|
|
749
768
|
const dir = spec.direction?.toLowerCase() === 'desc' ? 'DESC' : 'ASC';
|
|
750
769
|
const limitOne = qi.buildPagination('1', undefined, true);
|
|
751
770
|
// Parents with ZERO surviving related rows have no row to pick: the
|
|
@@ -880,6 +899,7 @@ export function buildRelationOrderClause(qi, targetTable, targetMeta, alias, ord
|
|
|
880
899
|
return buildRelationOrderBy(qi, key, dirValue, `${alias}ord${relOrdCounter++}`, params, { meta: targetMeta, table: targetTable, parentRef: alias });
|
|
881
900
|
}
|
|
882
901
|
const col = resolveOrderByColumn(qi, targetTable, targetMeta, key);
|
|
902
|
+
assertOrderDirection(dirValue, `orderBy "${key}" in the \`with\` clause for "${targetTable}"`);
|
|
883
903
|
const { dir, nulls } = normalizeOrderBy(dirValue);
|
|
884
904
|
return `${alias}.${qi.q(col)} ${dir}${nullsSuffix(qi, nulls)}`;
|
|
885
905
|
})
|