turbine-orm 0.61.0 → 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.
Files changed (82) hide show
  1. package/README.md +65 -21
  2. package/dist/cjs/cli/config.d.ts +40 -0
  3. package/dist/cjs/cli/config.js +74 -2
  4. package/dist/cjs/cli/index.d.ts +85 -1
  5. package/dist/cjs/cli/index.js +323 -24
  6. package/dist/cjs/cli/mcp.d.ts +8 -0
  7. package/dist/cjs/cli/mcp.js +448 -29
  8. package/dist/cjs/cli/pii-tags.d.ts +64 -9
  9. package/dist/cjs/cli/pii-tags.js +218 -39
  10. package/dist/cjs/cli/studio-ui.generated.js +1 -1
  11. package/dist/cjs/cli/studio.d.ts +23 -0
  12. package/dist/cjs/cli/studio.js +126 -53
  13. package/dist/cjs/cli/ui.d.ts +15 -1
  14. package/dist/cjs/cli/ui.js +19 -5
  15. package/dist/cjs/client.js +186 -3
  16. package/dist/cjs/errors.d.ts +38 -1
  17. package/dist/cjs/errors.js +235 -24
  18. package/dist/cjs/index.d.ts +2 -2
  19. package/dist/cjs/index.js +7 -2
  20. package/dist/cjs/pipeline.js +15 -2
  21. package/dist/cjs/powql.d.ts +12 -0
  22. package/dist/cjs/powql.js +46 -21
  23. package/dist/cjs/prisma-compat.d.ts +15 -5
  24. package/dist/cjs/prisma-compat.js +273 -78
  25. package/dist/cjs/query/aggregates.d.ts +1 -1
  26. package/dist/cjs/query/aggregates.js +24 -10
  27. package/dist/cjs/query/batched-loader.d.ts +9 -4
  28. package/dist/cjs/query/batched-loader.js +4 -1
  29. package/dist/cjs/query/builder.d.ts +47 -0
  30. package/dist/cjs/query/builder.js +125 -21
  31. package/dist/cjs/query/index.d.ts +3 -1
  32. package/dist/cjs/query/index.js +7 -1
  33. package/dist/cjs/query/option-surface.d.ts +11 -0
  34. package/dist/cjs/query/option-surface.js +13 -0
  35. package/dist/cjs/query/relations.d.ts +8 -0
  36. package/dist/cjs/query/relations.js +21 -1
  37. package/dist/cjs/query/types.d.ts +152 -18
  38. package/dist/cjs/query/types.js +212 -1
  39. package/dist/cjs/query/where.d.ts +3 -3
  40. package/dist/cjs/query/where.js +8 -2
  41. package/dist/cjs/query/writes.js +10 -9
  42. package/dist/cli/config.d.ts +40 -0
  43. package/dist/cli/config.js +73 -2
  44. package/dist/cli/index.d.ts +85 -1
  45. package/dist/cli/index.js +321 -26
  46. package/dist/cli/mcp.d.ts +8 -0
  47. package/dist/cli/mcp.js +448 -29
  48. package/dist/cli/pii-tags.d.ts +64 -9
  49. package/dist/cli/pii-tags.js +217 -39
  50. package/dist/cli/studio-ui.generated.js +1 -1
  51. package/dist/cli/studio.d.ts +23 -0
  52. package/dist/cli/studio.js +125 -53
  53. package/dist/cli/ui.d.ts +15 -1
  54. package/dist/cli/ui.js +18 -4
  55. package/dist/client.js +187 -4
  56. package/dist/errors.d.ts +38 -1
  57. package/dist/errors.js +234 -23
  58. package/dist/index.d.ts +2 -2
  59. package/dist/index.js +5 -2
  60. package/dist/pipeline.js +15 -2
  61. package/dist/powql.d.ts +12 -0
  62. package/dist/powql.js +46 -21
  63. package/dist/prisma-compat.d.ts +15 -5
  64. package/dist/prisma-compat.js +274 -79
  65. package/dist/query/aggregates.d.ts +1 -1
  66. package/dist/query/aggregates.js +24 -10
  67. package/dist/query/batched-loader.d.ts +9 -4
  68. package/dist/query/batched-loader.js +4 -1
  69. package/dist/query/builder.d.ts +47 -0
  70. package/dist/query/builder.js +124 -21
  71. package/dist/query/index.d.ts +3 -1
  72. package/dist/query/index.js +2 -0
  73. package/dist/query/option-surface.d.ts +11 -0
  74. package/dist/query/option-surface.js +13 -0
  75. package/dist/query/relations.d.ts +8 -0
  76. package/dist/query/relations.js +21 -1
  77. package/dist/query/types.d.ts +152 -18
  78. package/dist/query/types.js +207 -2
  79. package/dist/query/where.d.ts +3 -3
  80. package/dist/query/where.js +8 -2
  81. package/dist/query/writes.js +10 -9
  82. package/package.json +13 -3
@@ -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;
@@ -46,6 +46,7 @@ var __importStar = (this && this.__importStar) || (function () {
46
46
  })();
47
47
  Object.defineProperty(exports, "__esModule", { value: true });
48
48
  exports.QueryInterface = exports.AUTO_COUNT_BATCH_MIN_PARENT_ROWS = exports.AUTO_TO_ONE_JOIN_ROWS_MAX = exports.AUTO_TO_ONE_JOIN_ROWS_MIN = exports.AUTO_TO_ONE_JOIN_MAX_ROWS = exports.AUTO_ASSUMED_ROUND_TRIP_MS = exports.AUTO_JOIN_PENALTY_MS_PER_ROW = void 0;
49
+ exports.unlockNestedWriteTx = unlockNestedWriteTx;
49
50
  const dialect_js_1 = require("../dialect.js");
50
51
  const errors_js_1 = require("../errors.js");
51
52
  const index_advisor_js_1 = require("../index-advisor.js");
@@ -56,6 +57,7 @@ const batched_loader_js_1 = require("./batched-loader.js");
56
57
  const compound_unique_js_1 = require("./compound-unique.js");
57
58
  const filters_js_1 = require("./filters.js");
58
59
  const relationsMod = __importStar(require("./relations.js"));
60
+ const types_js_1 = require("./types.js");
59
61
  const utils_js_1 = require("./utils.js");
60
62
  const warn_registry_js_1 = require("./warn-registry.js");
61
63
  const whereMod = __importStar(require("./where.js"));
@@ -336,6 +338,49 @@ function maybeExpandCompoundUnique(meta, args) {
336
338
  const expanded = (0, compound_unique_js_1.expandCompoundUniqueWhere)(meta, where);
337
339
  return expanded === where ? args : { ...args, where: expanded };
338
340
  }
341
+ /**
342
+ * Bridge the nested-write engine's table handles onto the privilege sentinel.
343
+ *
344
+ * `nested-write.ts` clears a `set` relation by calling
345
+ * `updateMany({ where, data, allowFullTableScan: true })`: an INTERNAL,
346
+ * fully-derived predicate (the parent's own FK match, guarded there against a
347
+ * null reference key), not caller input, and the flag is what lets that
348
+ * unconditional-looking statement through the empty-`where` guard. Since
349
+ * `allowFullTableScan` now only accepts {@link UNSAFE}, that literal `true`
350
+ * would throw and every nested `set` would fail.
351
+ *
352
+ * Rather than hand the nested-write engine the sentinel (which would put the
353
+ * privilege value on a second, less obvious import path), the translation
354
+ * happens HERE, at the single seam where core hands it a table accessor. The
355
+ * rewrite is deliberately narrow: `updateMany` only, and only when the value is
356
+ * exactly `true`, so nothing else about the call is touched and no other
357
+ * operation gains an escape hatch.
358
+ *
359
+ * @internal Exported for its unit test; not part of the public surface.
360
+ */
361
+ function unlockNestedWriteTx(tx) {
362
+ const wrapped = {
363
+ table(name) {
364
+ const handle = tx.table(name);
365
+ // A Proxy rather than a copied object: the handle is a class instance
366
+ // whose methods live on the prototype, so a spread would produce an
367
+ // object with no methods, and re-listing them by hand would silently
368
+ // drop any method added later.
369
+ return new Proxy(handle, {
370
+ get(target, prop) {
371
+ const value = Reflect.get(target, prop, target);
372
+ if (typeof value !== 'function')
373
+ return value;
374
+ const method = value;
375
+ if (prop !== 'updateMany')
376
+ return method.bind(target);
377
+ return (args) => method.call(target, args?.allowFullTableScan === true ? { ...args, allowFullTableScan: types_js_1.UNSAFE } : args);
378
+ },
379
+ });
380
+ },
381
+ };
382
+ return wrapped;
383
+ }
339
384
  /**
340
385
  * Whether a relation `with`-clause `orderBy` carries no actual ordering: an
341
386
  * empty array, or an object with no non-`undefined` own keys. Used by
@@ -480,6 +525,12 @@ class QueryInterface {
480
525
  * `orderBy` all see it without threading it through dozens of signatures.
481
526
  * Only load-bearing when {@link globalFilters} is configured; build+collect are
482
527
  * synchronous per call, so this transient is never observed across an await.
528
+ *
529
+ * Holds the RESOLVED form ({@link ResolvedSkipGlobalFilters}), never the raw
530
+ * arg: every assignment goes through `resolveSkipGlobalFilters`, which is
531
+ * where a non-sentinel value (`true`, `['users']`, anything a JSON body can
532
+ * carry) is refused. Keeping the raw value here would push that decision out
533
+ * to each reader, and one reader forgetting it is the whole bug class.
483
534
  */
484
535
  currentSkip;
485
536
  /**
@@ -1371,8 +1422,13 @@ class QueryInterface {
1371
1422
  // on the batched subset up front.
1372
1423
  (0, batched_loader_js_1.rejectNestedPickOrder)(batchedWith);
1373
1424
  const skip = args.skipGlobalFilters;
1425
+ // Resolve the sentinel HERE, not just in buildFindMany below: this method
1426
+ // decides the base projection before it ever builds SQL, so a literal
1427
+ // `true` would otherwise shape the projection (truthily) and only be
1428
+ // refused one step later.
1429
+ const includePii = (0, types_js_1.resolveUnsafeFlag)(args.includePii, 'includePii');
1374
1430
  const needed = (0, batched_loader_js_1.neededParentKeyFields)(this.tableMeta, batchedWith);
1375
- const proj = (0, batched_loader_js_1.includeKeysForBatching)(args.select, args.omit, needed, (0, batched_loader_js_1.defaultProjectionFields)(this.tableMeta, args.includePii));
1431
+ const proj = (0, batched_loader_js_1.includeKeysForBatching)(args.select, args.omit, needed, (0, batched_loader_js_1.defaultProjectionFields)(this.tableMeta, includePii));
1376
1432
  const hasJoin = Object.keys(joinWith).length > 0;
1377
1433
  // Force the residual `with` onto the join plan so the base query never
1378
1434
  // re-enters this auto planning.
@@ -1392,7 +1448,7 @@ class QueryInterface {
1392
1448
  const rows = deferred.transform(result);
1393
1449
  const entities = single ? (rows ? [rows] : []) : rows;
1394
1450
  if (entities.length > 0) {
1395
- await (0, batched_loader_js_1.loadRelationsBatched)(this.batchedContext(args.timeout, skip, args.includePii === true, args.forceCustomPlan === true), entities, batchedWith, args.timeout);
1451
+ await (0, batched_loader_js_1.loadRelationsBatched)(this.batchedContext(args.timeout, skip, args.includePii, args.forceCustomPlan === true), entities, batchedWith, args.timeout);
1396
1452
  }
1397
1453
  (0, batched_loader_js_1.stripFields)(entities, proj.strip);
1398
1454
  return single ? (entities[0] ?? null) : entities;
@@ -1408,7 +1464,17 @@ class QueryInterface {
1408
1464
  * and unlimited-warnings silenced, a relation load must fetch every matching
1409
1465
  * child, and the per-relation `limit` is applied client-side by the loader.
1410
1466
  */
1411
- batchedContext(timeout, skip, includePii, forceCustomPlan = false) {
1467
+ batchedContext(timeout, skip,
1468
+ // BOTH privilege values stay in their RAW (sentinel) form here, and that is
1469
+ // load-bearing rather than laziness: the batched loader re-enters
1470
+ // `buildFindMany` one level down with these exact values on the child args,
1471
+ // where they are resolved (and refused) again. Handing it a plain `true`
1472
+ // would make every relation follow-up throw. The caller has already
1473
+ // resolved them once for its own projection decisions, so an invalid value
1474
+ // never reaches this point.
1475
+ includePii, forceCustomPlan = false) {
1476
+ // The loader's own global-filter callback below needs the RESOLVED form.
1477
+ const resolvedSkip = (0, types_js_1.resolveSkipGlobalFilters)(skip);
1412
1478
  const childOptions = {
1413
1479
  ...this.options,
1414
1480
  defaultLimit: undefined,
@@ -1432,7 +1498,7 @@ class QueryInterface {
1432
1498
  // so a batched load excludes/includes PII exactly as the join strategy.
1433
1499
  includePii,
1434
1500
  tableGlobalFilter: (table, alias, precedingParams) => {
1435
- const gf = this.resolveGlobalFilter(table, skip);
1501
+ const gf = this.resolveGlobalFilter(table, resolvedSkip);
1436
1502
  if (!gf)
1437
1503
  return null;
1438
1504
  const meta = this.schema.tables[table];
@@ -1477,7 +1543,7 @@ class QueryInterface {
1477
1543
  const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
1478
1544
  const entities = deferred.transform(result);
1479
1545
  if (entities.length > 0) {
1480
- await (0, batched_loader_js_1.loadRelationsBatched)(this.batchedContext(args.timeout, skip, args.includePii === true, args.forceCustomPlan === true), entities, withClause, args.timeout);
1546
+ await (0, batched_loader_js_1.loadRelationsBatched)(this.batchedContext(args.timeout, skip, args.includePii, args.forceCustomPlan === true), entities, withClause, args.timeout);
1481
1547
  }
1482
1548
  (0, batched_loader_js_1.stripFields)(entities, strip);
1483
1549
  return entities;
@@ -1489,7 +1555,7 @@ class QueryInterface {
1489
1555
  */
1490
1556
  prepareBatchedBase(args, withClause) {
1491
1557
  const needed = (0, batched_loader_js_1.neededParentKeyFields)(this.tableMeta, withClause);
1492
- const proj = (0, batched_loader_js_1.includeKeysForBatching)(args.select, args.omit, needed, (0, batched_loader_js_1.defaultProjectionFields)(this.tableMeta, args.includePii));
1558
+ 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')));
1493
1559
  const baseArgs = {
1494
1560
  ...args,
1495
1561
  with: undefined,
@@ -1862,20 +1928,20 @@ class QueryInterface {
1862
1928
  // Same scope-rule parity as runFindManyBatched: reject before querying.
1863
1929
  (0, batched_loader_js_1.rejectNestedPickOrder)(withClause);
1864
1930
  const needed = (0, batched_loader_js_1.neededParentKeyFields)(this.tableMeta, withClause);
1865
- const proj = (0, batched_loader_js_1.includeKeysForBatching)(args.select, args.omit, needed, (0, batched_loader_js_1.defaultProjectionFields)(this.tableMeta, args.includePii));
1931
+ 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')));
1866
1932
  const baseArgs = { ...args, with: undefined, select: proj.select, omit: proj.omit };
1867
1933
  const deferred = this.buildFindUnique(baseArgs);
1868
1934
  const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
1869
1935
  const entity = deferred.transform(result);
1870
1936
  if (!entity)
1871
1937
  return null;
1872
- await (0, batched_loader_js_1.loadRelationsBatched)(this.batchedContext(args.timeout, args.skipGlobalFilters, args.includePii === true, args.forceCustomPlan === true), [entity], withClause, args.timeout);
1938
+ await (0, batched_loader_js_1.loadRelationsBatched)(this.batchedContext(args.timeout, args.skipGlobalFilters, args.includePii, args.forceCustomPlan === true), [entity], withClause, args.timeout);
1873
1939
  (0, batched_loader_js_1.stripFields)([entity], proj.strip);
1874
1940
  return entity;
1875
1941
  }
1876
1942
  // biome-ignore lint/complexity/noBannedTypes: {} means "no with clause", matches TypedWithClause default
1877
1943
  buildFindUnique(args) {
1878
- this.currentSkip = args.skipGlobalFilters;
1944
+ this.currentSkip = (0, types_js_1.resolveSkipGlobalFilters)(args.skipGlobalFilters);
1879
1945
  // Prisma compound-unique selector expansion (before global-filter merge and
1880
1946
  // fingerprinting, so the cache only ever sees the canonical expanded where).
1881
1947
  args = maybeExpandCompoundUnique(this.tableMeta, args);
@@ -1910,7 +1976,8 @@ class QueryInterface {
1910
1976
  if (normalized !== args.with)
1911
1977
  args = { ...args, with: normalized };
1912
1978
  }
1913
- const includePii = args.includePii === true;
1979
+ // Resolved once, see buildFindMany.
1980
+ const includePii = (0, types_js_1.resolveUnsafeFlag)(args.includePii, 'includePii');
1914
1981
  // findUnique is never flatten-planned: it reads ONE parent row, so the
1915
1982
  // correlated subquery already runs exactly once and a join buys nothing.
1916
1983
  // Say so, because the caller did ask for a strategy that is not running.
@@ -2021,7 +2088,7 @@ class QueryInterface {
2021
2088
  this.collectWhereParams(whereObj, params);
2022
2089
  this.collectWithParams(args.with, params);
2023
2090
  this.crossCheckCache('findUnique', ck, entry, buildSql, params);
2024
- const parseWith = this.makeNestedParser(args.with, args.includePii === true);
2091
+ const parseWith = this.makeNestedParser(args.with, includePii);
2025
2092
  return {
2026
2093
  sql: entry.sql,
2027
2094
  params,
@@ -2213,7 +2280,7 @@ class QueryInterface {
2213
2280
  }
2214
2281
  // biome-ignore lint/complexity/noBannedTypes: {} means "no with clause", matches TypedWithClause default
2215
2282
  buildFindMany(args) {
2216
- this.currentSkip = args?.skipGlobalFilters;
2283
+ this.currentSkip = (0, types_js_1.resolveSkipGlobalFilters)(args?.skipGlobalFilters);
2217
2284
  // Stable relation order (opt-in): fill PK-asc orderBy into unordered to-many
2218
2285
  // relations BEFORE fingerprinting, so the two orderings get distinct cache
2219
2286
  // entries and every downstream path (SQL build, collect, parser) inherits it.
@@ -2254,7 +2321,10 @@ class QueryInterface {
2254
2321
  }
2255
2322
  }
2256
2323
  }
2257
- const includePii = args?.includePii === true;
2324
+ // Resolve the PII opt-in ONCE, before any projection decision. The value is
2325
+ // the UNSAFE sentinel or nothing: a literal `true` (what a spread request
2326
+ // body carries) throws here rather than unlocking the projection.
2327
+ const includePii = (0, types_js_1.resolveUnsafeFlag)(args?.includePii, 'includePii');
2258
2328
  const columnsList = this.resolveColumns(args?.select, args?.omit, includePii);
2259
2329
  const colKey = columnsList ? columnsList.join(',') : '*';
2260
2330
  // AND-merge this table's global filter into the user where; `hasWhere` gates
@@ -2505,9 +2575,14 @@ class QueryInterface {
2505
2575
  // function of the schema, the `with` shape and `includePii`, never of
2506
2576
  // `limit`, so the batch-size override the speculative fetch applies cannot
2507
2577
  // change it, and the stream's parser matches the emitted SQL.
2508
- const streamFlattenPlan = hasRelations ? this.planFlatten(args, args?.includePii === true) : null;
2578
+ // Resolved once for the whole stream: the flatten plan and the row parser
2579
+ // MUST agree with the SQL buildFindMany emits below, and reading the raw
2580
+ // sentinel with `=== true` here would have quietly planned a no-PII parser
2581
+ // over a with-PII statement.
2582
+ const streamPii = (0, types_js_1.resolveUnsafeFlag)(args?.includePii, 'includePii');
2583
+ const streamFlattenPlan = hasRelations ? this.planFlatten(args, streamPii) : null;
2509
2584
  const parseWith = hasRelations
2510
- ? this.makeNestedParser(args.with, args?.includePii === true, streamFlattenPlan)
2585
+ ? this.makeNestedParser(args.with, streamPii, streamFlattenPlan)
2511
2586
  : null;
2512
2587
  // --- Speculative first fetch: try to satisfy the entire drain in one RTT ---
2513
2588
  const speculativeDeferred = this.buildFindMany({
@@ -2541,7 +2616,7 @@ class QueryInterface {
2541
2616
  // connection, so `pool.query` already IS that connection. The stream rides
2542
2617
  // on it, is never released here, and the dialect is told to emit no
2543
2618
  // transaction control of its own (`ambientTransaction`).
2544
- const client = this.txScoped ? null : await this.pool.connect();
2619
+ const client = this.txScoped ? null : await this.acquireConnection();
2545
2620
  const conn = client ?? {
2546
2621
  query: async (text, values) => (await this.pool.query(text, values)),
2547
2622
  };
@@ -2723,8 +2798,33 @@ class QueryInterface {
2723
2798
  return result;
2724
2799
  });
2725
2800
  }
2801
+ /**
2802
+ * Check out a pooled connection, translating a driver failure into a typed
2803
+ * Turbine error.
2804
+ *
2805
+ * `pool.connect()` is where the first-run failures land: wrong password
2806
+ * (SQLSTATE 28P01), no such database (3D000), nothing listening
2807
+ * (ECONNREFUSED), an unverifiable TLS certificate. Unwrapped, every one of
2808
+ * those surfaces from an ordinary `db.users.create({ data: { ...nested } })`
2809
+ * as a raw pg `DatabaseError` whose `.code` is a SQLSTATE, on the same
2810
+ * property Turbine puts `TURBINE_E0NN` in.
2811
+ *
2812
+ * client.ts has its own copy for `$transaction` / `connect()`; this one
2813
+ * exists because `query/` must not import client.ts (circular dependency).
2814
+ * The query paths need no equivalent: `pool.query()` opens the connection
2815
+ * itself and rejects with the connect error, which the query boundary
2816
+ * already wraps.
2817
+ */
2818
+ async acquireConnection() {
2819
+ try {
2820
+ return await this.pool.connect();
2821
+ }
2822
+ catch (err) {
2823
+ throw (0, errors_js_1.wrapPgError)(err);
2824
+ }
2825
+ }
2726
2826
  async runInImplicitTx(fn) {
2727
- const client = await this.pool.connect();
2827
+ const client = await this.acquireConnection();
2728
2828
  let began = false;
2729
2829
  try {
2730
2830
  await client.query(this.dialect.beginStatement());
@@ -2741,8 +2841,12 @@ class QueryInterface {
2741
2841
  // older-engine client falls back to the full capability set inside the
2742
2842
  // implicit transaction.
2743
2843
  this.pool);
2744
- // biome-ignore lint/suspicious/noExplicitAny: TransactionClient satisfies NestedWriteContext['tx'] at runtime
2745
- const ctx = { schema: this.schema, tx: tx, scopedConnect: this.scopedConnect };
2844
+ const ctx = {
2845
+ schema: this.schema,
2846
+ // biome-ignore lint/suspicious/noExplicitAny: TransactionClient satisfies NestedWriteContext['tx'] at runtime
2847
+ tx: unlockNestedWriteTx(tx),
2848
+ scopedConnect: this.scopedConnect,
2849
+ };
2746
2850
  const result = await fn(ctx);
2747
2851
  await client.query(this.dialect.commitStatement());
2748
2852
  return result;
@@ -2771,7 +2875,7 @@ class QueryInterface {
2771
2875
  const opts = { ...this.options, _txScoped: true };
2772
2876
  return {
2773
2877
  schema,
2774
- tx: this.makeTxProxy(pool, schema, middlewares, opts),
2878
+ tx: unlockNestedWriteTx(this.makeTxProxy(pool, schema, middlewares, opts)),
2775
2879
  };
2776
2880
  }
2777
2881
  // biome-ignore lint/suspicious/noExplicitAny: bridges MiddlewareFn[] ↔ Middleware[] and QI ↔ NestedWriteContext type gap
@@ -2829,7 +2933,7 @@ class QueryInterface {
2829
2933
  });
2830
2934
  }
2831
2935
  buildCount(args) {
2832
- this.currentSkip = args?.skipGlobalFilters;
2936
+ this.currentSkip = (0, types_js_1.resolveSkipGlobalFilters)(args?.skipGlobalFilters);
2833
2937
  const effWhere = this.mergeGlobalFilter(args?.where);
2834
2938
  const hasWhere = effWhere !== undefined;
2835
2939
  const whereObj = (effWhere ?? {});
@@ -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';
@@ -7,7 +7,7 @@
7
7
  * former monolithic `import { … } from './query.js'`.
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.QueryInterface = exports.AUTO_TO_ONE_JOIN_ROWS_MIN = exports.AUTO_TO_ONE_JOIN_ROWS_MAX = exports.AUTO_TO_ONE_JOIN_MAX_ROWS = exports.AUTO_JOIN_PENALTY_MS_PER_ROW = exports.AUTO_COUNT_BATCH_MIN_PARENT_ROWS = exports.AUTO_ASSUMED_ROUND_TRIP_MS = exports.sqlToPreparedName = exports.quoteIdent = exports.OPERATOR_KEYS = exports.LRUCache = exports.fnv1a64Hex = exports.escSingleQuote = exports.escapeLike = exports.buildCorrelation = exports.UPSERT_OPTIONS = exports.UPDATE_OPTIONS = exports.UPDATE_MANY_OPTIONS = exports.optionKeysOfKind = exports.GROUP_BY_OPTIONS = exports.FIND_UNIQUE_OPTIONS = exports.FIND_MANY_STREAM_OPTIONS = exports.FIND_MANY_OPTIONS = exports.DELETE_OPTIONS = exports.DELETE_MANY_OPTIONS = exports.CREATE_OPTIONS = exports.CREATE_MANY_OPTIONS = exports.COUNT_OPTIONS = exports.applyNativeOptions = exports.ALL_OPTION_TABLES = exports.AGGREGATE_OPTIONS = exports.postgresDialect = void 0;
10
+ exports.QueryInterface = exports.AUTO_TO_ONE_JOIN_ROWS_MIN = exports.AUTO_TO_ONE_JOIN_ROWS_MAX = exports.AUTO_TO_ONE_JOIN_MAX_ROWS = exports.AUTO_JOIN_PENALTY_MS_PER_ROW = exports.AUTO_COUNT_BATCH_MIN_PARENT_ROWS = exports.AUTO_ASSUMED_ROUND_TRIP_MS = exports.sqlToPreparedName = exports.quoteIdent = exports.OPERATOR_KEYS = exports.LRUCache = exports.fnv1a64Hex = exports.escSingleQuote = exports.escapeLike = exports.buildCorrelation = exports.UNSAFE = exports.resolveUnsafeFlag = exports.resolveSkipGlobalFilters = exports.assertOrderDirection = exports.UPSERT_OPTIONS = exports.UPDATE_OPTIONS = exports.UPDATE_MANY_OPTIONS = exports.optionKeysOfKind = exports.GROUP_BY_OPTIONS = exports.FIND_UNIQUE_OPTIONS = exports.FIND_MANY_STREAM_OPTIONS = exports.FIND_MANY_OPTIONS = exports.DELETE_OPTIONS = exports.DELETE_MANY_OPTIONS = exports.CREATE_OPTIONS = exports.CREATE_MANY_OPTIONS = exports.COUNT_OPTIONS = exports.applyNativeOptions = exports.ALL_OPTION_TABLES = exports.AGGREGATE_OPTIONS = exports.postgresDialect = void 0;
11
11
  var dialect_js_1 = require("../dialect.js");
12
12
  Object.defineProperty(exports, "postgresDialect", { enumerable: true, get: function () { return dialect_js_1.postgresDialect; } });
13
13
  var option_surface_js_1 = require("./option-surface.js");
@@ -27,6 +27,12 @@ Object.defineProperty(exports, "optionKeysOfKind", { enumerable: true, get: func
27
27
  Object.defineProperty(exports, "UPDATE_MANY_OPTIONS", { enumerable: true, get: function () { return option_surface_js_1.UPDATE_MANY_OPTIONS; } });
28
28
  Object.defineProperty(exports, "UPDATE_OPTIONS", { enumerable: true, get: function () { return option_surface_js_1.UPDATE_OPTIONS; } });
29
29
  Object.defineProperty(exports, "UPSERT_OPTIONS", { enumerable: true, get: function () { return option_surface_js_1.UPSERT_OPTIONS; } });
30
+ // The privilege sentinel: a runtime value, not a type (see types.ts).
31
+ var types_js_1 = require("./types.js");
32
+ Object.defineProperty(exports, "assertOrderDirection", { enumerable: true, get: function () { return types_js_1.assertOrderDirection; } });
33
+ Object.defineProperty(exports, "resolveSkipGlobalFilters", { enumerable: true, get: function () { return types_js_1.resolveSkipGlobalFilters; } });
34
+ Object.defineProperty(exports, "resolveUnsafeFlag", { enumerable: true, get: function () { return types_js_1.resolveUnsafeFlag; } });
35
+ Object.defineProperty(exports, "UNSAFE", { enumerable: true, get: function () { return types_js_1.UNSAFE; } });
30
36
  var utils_js_1 = require("./utils.js");
31
37
  Object.defineProperty(exports, "buildCorrelation", { enumerable: true, get: function () { return utils_js_1.buildCorrelation; } });
32
38
  Object.defineProperty(exports, "escapeLike", { enumerable: true, get: function () { return utils_js_1.escapeLike; } });
@@ -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';
@@ -39,6 +39,17 @@
39
39
  * column, or model NAME. If the value names anything in the schema, it is
40
40
  * `'prisma'`: a name-translating consumer has to walk it by hand.
41
41
  *
42
+ * ## The privilege options
43
+ *
44
+ * `skipGlobalFilters`, `includePii` and `allowFullTableScan` are `'native'` and
45
+ * MUST stay that way. Their values are the `UNSAFE` sentinel (plus, for
46
+ * skipGlobalFilters, table ACCESSOR names, which the rule above explicitly
47
+ * allows), and forwarding them verbatim is what lets core be the single place
48
+ * that decides whether a value is a real opt-in. Hand-translating them here
49
+ * would mean a second copy of that decision, and a consumer's
50
+ * `includePii: true` would then be judged by whichever copy it happened to
51
+ * reach first.
52
+ *
42
53
  * @module
43
54
  */
44
55
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -57,6 +68,8 @@ exports.FIND_UNIQUE_OPTIONS = {
57
68
  relationLoadStrategy: 'prisma',
58
69
  timeout: 'native',
59
70
  stableRelationOrder: 'native',
71
+ // Privilege options: forwarded verbatim, refused by core unless the value is
72
+ // the UNSAFE sentinel. See the note at the top of this file.
60
73
  skipGlobalFilters: 'native',
61
74
  includePii: 'native',
62
75
  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;
@@ -94,6 +94,7 @@ const index_advisor_js_1 = require("../index-advisor.js");
94
94
  const schema_js_1 = require("../schema.js");
95
95
  const batched_loader_js_1 = require("./batched-loader.js");
96
96
  const filters_js_1 = require("./filters.js");
97
+ const types_js_1 = require("./types.js");
97
98
  const utils_js_1 = require("./utils.js");
98
99
  const warn_registry_js_1 = require("./warn-registry.js");
99
100
  const whereMod = __importStar(require("./where.js"));
@@ -119,7 +120,7 @@ function resolveColumns(qi, select, omit, includePii) {
119
120
  .map(([k]) => qi.toColumn(k));
120
121
  }
121
122
  // Default / omit-only projection: PII-tagged columns are excluded unless the
122
- // caller passed `includePii: true`. An empty set (untagged schema) keeps the
123
+ // caller opted in with `includePii: UNSAFE`. An empty set (untagged schema) keeps the
123
124
  // `null`/`*` fast path so the emitted SQL is byte-identical to before.
124
125
  const piiCols = includePii ? undefined : writesMod.piiColumns(qi, qi.tableMeta);
125
126
  const hasPii = piiCols !== undefined && piiCols.size > 0;
@@ -341,6 +342,14 @@ function collectRelationSubqueryParams(qi, relDef, spec, params, _parentRef, dep
341
342
  * ORDER BY differs only in nulls placement, vector metric, or relation-count
342
343
  * vs relation-column never collide on one cached SQL string. Captures the
343
344
  * SQL-shaping bits (direction, nulls, metric, relation keys), never values.
345
+ *
346
+ * NOTE for the direction guard (see {@link assertOrderDirection}): every
347
+ * direction is embedded here BYTE-FOR-BYTE (`String(d)` for the plain form,
348
+ * `d.sort` / `d.direction` for the spec forms), never normalized to ASC/DESC.
349
+ * That is what lets the direction validation live on the BUILD side alone: a
350
+ * bad direction has a fingerprint no good direction can produce, so it can
351
+ * never be served by a warm cache entry whose build already validated. Do not
352
+ * "tidy" this into a normalized token.
344
353
  */
345
354
  function orderByEntryFingerprint(qi, d, targetTable) {
346
355
  // Vector KNN ordering changes the emitted operator by metric and adds a
@@ -424,6 +433,7 @@ function buildOrderBy(qi, orderBy, params, lateralSink) {
424
433
  const rawColumn = qi.toColumn(key);
425
434
  const operator = whereMod.vectorOperator(qi, key, rawColumn, value.distance.metric);
426
435
  const placeholder = whereMod.pushVectorParam(qi, key, rawColumn, value.distance.to, params);
436
+ (0, types_js_1.assertDirectionToken)(value.distance.direction, `vector distance orderBy on "${key}"`);
427
437
  const safeDir = value.distance.direction?.toLowerCase() === 'desc' ? 'DESC' : 'ASC';
428
438
  return `${qi.q(rawColumn)} ${operator} ${placeholder} ${safeDir}`;
429
439
  }
@@ -443,6 +453,10 @@ function buildOrderBy(qi, orderBy, params, lateralSink) {
443
453
  throw new errors_js_1.ValidationError(`[turbine] Unknown field "${key}" in orderBy on table "${qi.table}". ` +
444
454
  `Known fields: ${Object.keys(meta.columnMap).join(', ') || '(none)'}.`);
445
455
  }
456
+ // Refuse a direction that is neither asc nor desc. normalizeOrderBy is
457
+ // `=== 'desc' ? DESC : ASC`, so without this every typo sorted ASCENDING
458
+ // and returned a correct-looking page in the reverse order.
459
+ (0, types_js_1.assertOrderDirection)(value, `orderBy "${key}" on table "${qi.table}"`);
446
460
  const { dir, nulls } = (0, filters_js_1.normalizeOrderBy)(value);
447
461
  return `${qi.toSqlColumn(key)} ${dir}${nullsSuffix(qi, nulls)}`;
448
462
  })
@@ -529,6 +543,7 @@ function buildJsonPathOrderEntry(qi, table, meta, field, spec, prefix, params) {
529
543
  params.push(whereMod.jsonPathParam(qi, spec.path));
530
544
  const extract = qi.dialect.buildJsonPathExtract(`${prefix}${qi.q(col)}`, qi.p(params.length));
531
545
  const lhs = spec.type === 'numeric' ? whereMod.castJsonNumeric(qi, extract) : extract;
546
+ (0, types_js_1.assertDirectionToken)(spec.direction, `JSON-path orderBy on "${field}"`);
532
547
  const dir = spec.direction?.toLowerCase() === 'desc' ? 'DESC' : 'ASC';
533
548
  // Rows whose document lacks the path extract to NULL. Without a nulls
534
549
  // clause, Postgres DESC defaults to NULLS FIRST, which both diverges from
@@ -623,6 +638,7 @@ function buildChainedToOneOrderBy(qi, head, nextRelName, nextValue, params) {
623
638
  if (!currentMeta.allColumns.includes(snakeCol)) {
624
639
  throw new errors_js_1.ValidationError(`[turbine] Unknown column "${key}" in orderBy on relation "${path.join('.')}" (table "${currentMeta.name}").`);
625
640
  }
641
+ (0, types_js_1.assertOrderDirection)(entryValue, `orderBy on relation path "${path.join('.')}"`);
626
642
  const { dir, nulls } = (0, filters_js_1.normalizeOrderBy)(entryValue);
627
643
  let where = head.correlation;
628
644
  if (params) {
@@ -685,6 +701,7 @@ function buildRelationOrderBy(qi, relName, value, alias, params, ctx, lateralSin
685
701
  throw new errors_js_1.ValidationError(`[turbine] orderBy on to-many relation "${relName}" only supports "_count" ` +
686
702
  `or a pick-row ordering ({ pick, by }) (got: ${keys.join(', ') || '(empty)'}).`);
687
703
  }
704
+ (0, types_js_1.assertOrderDirection)(value._count, `orderBy { ${relName}: { _count } }`);
688
705
  const { dir } = (0, filters_js_1.normalizeOrderBy)(value._count);
689
706
  return `${buildRelationCountExpr(qi, relDef, parentRef, alias, params)} ${dir}`;
690
707
  }
@@ -722,6 +739,7 @@ function buildRelationOrderBy(qi, relName, value, alias, params, ctx, lateralSin
722
739
  : '';
723
740
  throw new errors_js_1.ValidationError(`[turbine] Unknown column "${col}" in orderBy on relation "${relName}" (table "${relDef.to}").${relationHint}`);
724
741
  }
742
+ (0, types_js_1.assertOrderDirection)(dirValue, `orderBy "${col}" on relation "${relName}"`);
725
743
  const { dir, nulls } = (0, filters_js_1.normalizeOrderBy)(dirValue);
726
744
  // Target's global filter applies here too, otherwise ordering keys off
727
745
  // a soft-deleted / other-tenant related row's value (matches the with
@@ -823,6 +841,7 @@ function buildRelationPickOrderBy(qi, relName, relDef, spec, alias, parentRef, p
823
841
  const targetMeta = qi.schema.tables[relDef.to];
824
842
  if (!targetMeta)
825
843
  throw new errors_js_1.RelationError(`[turbine] Unknown relation target "${relDef.to}"`);
844
+ (0, types_js_1.assertDirectionToken)(spec.direction, `pick-row orderBy on relation "${relName}"`);
826
845
  const dir = spec.direction?.toLowerCase() === 'desc' ? 'DESC' : 'ASC';
827
846
  const limitOne = qi.buildPagination('1', undefined, true);
828
847
  // Parents with ZERO surviving related rows have no row to pick: the
@@ -957,6 +976,7 @@ function buildRelationOrderClause(qi, targetTable, targetMeta, alias, orderEntri
957
976
  return buildRelationOrderBy(qi, key, dirValue, `${alias}ord${relOrdCounter++}`, params, { meta: targetMeta, table: targetTable, parentRef: alias });
958
977
  }
959
978
  const col = resolveOrderByColumn(qi, targetTable, targetMeta, key);
979
+ (0, types_js_1.assertOrderDirection)(dirValue, `orderBy "${key}" in the \`with\` clause for "${targetTable}"`);
960
980
  const { dir, nulls } = (0, filters_js_1.normalizeOrderBy)(dirValue);
961
981
  return `${alias}.${qi.q(col)} ${dir}${nullsSuffix(qi, nulls)}`;
962
982
  })