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.
- package/README.md +65 -21
- 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 +323 -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 +186 -3
- 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.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 +125 -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 +321 -26
- 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 +187 -4
- 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.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 +124 -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
|
@@ -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,
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
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
|
-
|
|
2745
|
-
|
|
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';
|
package/dist/cjs/query/index.js
CHANGED
|
@@ -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
|
|
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
|
})
|