turbine-orm 0.19.0 → 0.19.2
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 +83 -15
- package/dist/adapters/index.d.ts +3 -2
- package/dist/cjs/cli/index.js +43 -13
- package/dist/cjs/cli/loader.js +62 -7
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.js +25 -35
- package/dist/cjs/client.js +20 -13
- package/dist/cjs/query/builder.js +342 -104
- package/dist/cjs/query/utils.js +1 -0
- package/dist/cli/index.js +45 -15
- package/dist/cli/loader.d.ts +22 -5
- package/dist/cli/loader.js +61 -7
- package/dist/cli/migrate.d.ts +2 -2
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +9 -14
- package/dist/cli/studio.js +25 -34
- package/dist/client.d.ts +12 -13
- package/dist/client.js +20 -13
- package/dist/index.d.ts +1 -1
- package/dist/query/builder.d.ts +43 -6
- package/dist/query/builder.js +342 -104
- package/dist/query/index.d.ts +1 -1
- package/dist/query/types.d.ts +62 -12
- package/dist/query/utils.js +1 -0
- package/package.json +4 -4
- package/dist/cjs/query.js +0 -2711
- package/dist/query.d.ts +0 -878
- package/dist/query.js +0 -2705
package/dist/cli/studio.d.ts
CHANGED
|
@@ -2,15 +2,20 @@
|
|
|
2
2
|
* turbine-orm CLI — Studio
|
|
3
3
|
*
|
|
4
4
|
* A local, read-only web UI for browsing databases, exploring relations,
|
|
5
|
-
* and
|
|
6
|
-
*
|
|
5
|
+
* and composing queries visually. ORM-native since v0.19: there is no
|
|
6
|
+
* raw-SQL input surface — the Query tab builds `findMany` args that are
|
|
7
|
+
* validated against introspected metadata and compiled by QueryInterface
|
|
8
|
+
* (`/api/builder`). Pure Node (built-in `http` module), no runtime
|
|
9
|
+
* dependencies beyond `pg`, bound to 127.0.0.1 only.
|
|
7
10
|
*
|
|
8
11
|
* Security model:
|
|
9
12
|
* • Bind 127.0.0.1 only (never 0.0.0.0 — no LAN exposure)
|
|
10
13
|
* • Random auth token generated per process, required in Cookie header
|
|
11
|
-
* •
|
|
14
|
+
* • No SQL input surface at all — every identifier in a builder request is
|
|
15
|
+
* validated against the introspected schema; all values are $N params
|
|
12
16
|
* • Every query runs in a READ ONLY transaction (belt-and-suspenders)
|
|
13
|
-
* • 30s statement timeout
|
|
17
|
+
* • 30s statement timeout via parameterized set_config()
|
|
18
|
+
* • Per-session rate limiting, CSP + security headers, cross-origin refusal
|
|
14
19
|
*
|
|
15
20
|
* Not implemented (deliberately): row editing, DDL, destructive operations.
|
|
16
21
|
* Studio is for inspection. Use the CLI, migrate, or raw SQL for writes.
|
|
@@ -74,14 +79,4 @@ export declare function apiBuilder(req: IncomingMessage, res: ServerResponse, ct
|
|
|
74
79
|
export declare function apiListSavedQueries(res: ServerResponse, ctx: StudioContext, params: URLSearchParams): void;
|
|
75
80
|
export declare function apiCreateSavedQuery(req: IncomingMessage, res: ServerResponse, ctx: StudioContext): Promise<void>;
|
|
76
81
|
export declare function apiDeleteSavedQuery(res: ServerResponse, ctx: StudioContext, id: string): void;
|
|
77
|
-
/**
|
|
78
|
-
* Accept only SELECT or WITH (CTE) statements. Reject any statement that
|
|
79
|
-
* contains a semicolon followed by non-whitespace (prevents statement
|
|
80
|
-
* stacking), and require the first non-comment keyword to be SELECT or WITH.
|
|
81
|
-
*
|
|
82
|
-
* This is a first-line filter — the transaction's READ ONLY mode is the
|
|
83
|
-
* second line of defense. Both must fail before a destructive statement
|
|
84
|
-
* could run.
|
|
85
|
-
*/
|
|
86
|
-
export declare function isReadOnlyStatement(sql: string): boolean;
|
|
87
82
|
//# sourceMappingURL=studio.d.ts.map
|
package/dist/cli/studio.js
CHANGED
|
@@ -2,15 +2,20 @@
|
|
|
2
2
|
* turbine-orm CLI — Studio
|
|
3
3
|
*
|
|
4
4
|
* A local, read-only web UI for browsing databases, exploring relations,
|
|
5
|
-
* and
|
|
6
|
-
*
|
|
5
|
+
* and composing queries visually. ORM-native since v0.19: there is no
|
|
6
|
+
* raw-SQL input surface — the Query tab builds `findMany` args that are
|
|
7
|
+
* validated against introspected metadata and compiled by QueryInterface
|
|
8
|
+
* (`/api/builder`). Pure Node (built-in `http` module), no runtime
|
|
9
|
+
* dependencies beyond `pg`, bound to 127.0.0.1 only.
|
|
7
10
|
*
|
|
8
11
|
* Security model:
|
|
9
12
|
* • Bind 127.0.0.1 only (never 0.0.0.0 — no LAN exposure)
|
|
10
13
|
* • Random auth token generated per process, required in Cookie header
|
|
11
|
-
* •
|
|
14
|
+
* • No SQL input surface at all — every identifier in a builder request is
|
|
15
|
+
* validated against the introspected schema; all values are $N params
|
|
12
16
|
* • Every query runs in a READ ONLY transaction (belt-and-suspenders)
|
|
13
|
-
* • 30s statement timeout
|
|
17
|
+
* • 30s statement timeout via parameterized set_config()
|
|
18
|
+
* • Per-session rate limiting, CSP + security headers, cross-origin refusal
|
|
14
19
|
*
|
|
15
20
|
* Not implemented (deliberately): row editing, DDL, destructive operations.
|
|
16
21
|
* Studio is for inspection. Use the CLI, migrate, or raw SQL for writes.
|
|
@@ -405,6 +410,11 @@ export async function apiBuilder(req, res, ctx) {
|
|
|
405
410
|
try {
|
|
406
411
|
await client.query('BEGIN READ ONLY');
|
|
407
412
|
await client.query(ctx.statementTimeout.sql, ctx.statementTimeout.params);
|
|
413
|
+
// QueryInterface emits unqualified table identifiers, which resolve via
|
|
414
|
+
// the connection's search_path. Pin it to the configured --schema so the
|
|
415
|
+
// Query tab reads the same schema as the Data tab (set_config is
|
|
416
|
+
// transaction-local and fully parameterized).
|
|
417
|
+
await client.query(`SELECT set_config('search_path', $1, true)`, [ctx.options.schema]);
|
|
408
418
|
const started = Date.now();
|
|
409
419
|
const result = await client.query(deferred.sql, deferred.params);
|
|
410
420
|
const elapsedMs = Date.now() - started;
|
|
@@ -433,6 +443,8 @@ export async function apiBuilder(req, res, ctx) {
|
|
|
433
443
|
function savedQueriesPath(ctx) {
|
|
434
444
|
return pathResolve(ctx.stateDir, 'studio-queries.json');
|
|
435
445
|
}
|
|
446
|
+
/** One-shot flag so the legacy saved-query notice isn't logged on every request. */
|
|
447
|
+
let legacyDropNoticeShown = false;
|
|
436
448
|
function loadSavedQueries(ctx) {
|
|
437
449
|
const file = savedQueriesPath(ctx);
|
|
438
450
|
if (!existsSync(file))
|
|
@@ -442,8 +454,16 @@ function loadSavedQueries(ctx) {
|
|
|
442
454
|
const parsed = JSON.parse(raw);
|
|
443
455
|
if (!parsed.queries || !Array.isArray(parsed.queries))
|
|
444
456
|
return { version: 1, queries: [] };
|
|
445
|
-
// Drop any legacy raw-SQL entries — Studio is builder-only now.
|
|
457
|
+
// Drop any legacy raw-SQL entries — Studio is builder-only now. Tell the
|
|
458
|
+
// user instead of silently discarding their saved work (the file on disk
|
|
459
|
+
// is only rewritten when a new query is saved, so this is recoverable).
|
|
446
460
|
const queries = parsed.queries.filter((q) => q && q.kind === 'builder');
|
|
461
|
+
const dropped = parsed.queries.length - queries.length;
|
|
462
|
+
if (dropped > 0 && !legacyDropNoticeShown) {
|
|
463
|
+
legacyDropNoticeShown = true;
|
|
464
|
+
console.warn(`[turbine studio] Ignoring ${dropped} legacy raw-SQL saved quer${dropped === 1 ? 'y' : 'ies'} in ${file} — ` +
|
|
465
|
+
'Studio is builder-only since v0.19. The entries remain in the file until a new query is saved.');
|
|
466
|
+
}
|
|
447
467
|
return { version: 1, queries };
|
|
448
468
|
}
|
|
449
469
|
catch {
|
|
@@ -515,35 +535,6 @@ function clampInt(value, fallback, min, max) {
|
|
|
515
535
|
return fallback;
|
|
516
536
|
return Math.min(Math.max(n, min), max);
|
|
517
537
|
}
|
|
518
|
-
/**
|
|
519
|
-
* Accept only SELECT or WITH (CTE) statements. Reject any statement that
|
|
520
|
-
* contains a semicolon followed by non-whitespace (prevents statement
|
|
521
|
-
* stacking), and require the first non-comment keyword to be SELECT or WITH.
|
|
522
|
-
*
|
|
523
|
-
* This is a first-line filter — the transaction's READ ONLY mode is the
|
|
524
|
-
* second line of defense. Both must fail before a destructive statement
|
|
525
|
-
* could run.
|
|
526
|
-
*/
|
|
527
|
-
export function isReadOnlyStatement(sql) {
|
|
528
|
-
const stripped = stripSqlComments(sql).trim();
|
|
529
|
-
if (!stripped)
|
|
530
|
-
return false;
|
|
531
|
-
// Disallow statement stacking. A single trailing `;` is fine.
|
|
532
|
-
const withoutTrailingSemi = stripped.replace(/;+\s*$/, '');
|
|
533
|
-
if (withoutTrailingSemi.includes(';'))
|
|
534
|
-
return false;
|
|
535
|
-
const firstWord = withoutTrailingSemi.slice(0, 6).toUpperCase();
|
|
536
|
-
if (firstWord.startsWith('SELECT'))
|
|
537
|
-
return true;
|
|
538
|
-
if (firstWord.startsWith('WITH'))
|
|
539
|
-
return true;
|
|
540
|
-
return false;
|
|
541
|
-
}
|
|
542
|
-
function stripSqlComments(sql) {
|
|
543
|
-
// Strip -- line comments and /* block comments */. Not a full SQL parser,
|
|
544
|
-
// but sufficient to catch the common bypass attempts.
|
|
545
|
-
return sql.replace(/--[^\n]*/g, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
546
|
-
}
|
|
547
538
|
function serializeRow(row) {
|
|
548
539
|
const out = {};
|
|
549
540
|
for (const [k, v] of Object.entries(row)) {
|
package/dist/client.d.ts
CHANGED
|
@@ -255,12 +255,14 @@ export declare class TurbineClient {
|
|
|
255
255
|
private readonly activeSubscriptions;
|
|
256
256
|
constructor(config: TurbineConfig | undefined, schema: SchemaMetadata);
|
|
257
257
|
/**
|
|
258
|
-
* Register a middleware function that runs
|
|
258
|
+
* Register a middleware function that runs around every query.
|
|
259
259
|
*
|
|
260
|
-
* Middleware can inspect and log query parameters,
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
260
|
+
* Middleware can inspect and log query parameters, measure timing, and
|
|
261
|
+
* transform the result returned by `next()`. Note: query SQL is generated
|
|
262
|
+
* BEFORE middleware runs — `params.args` is a read-only snapshot, and
|
|
263
|
+
* mutating it does NOT change the executed SQL. Cross-cutting filters
|
|
264
|
+
* (e.g. soft deletes) belong in the query itself: pass an explicit
|
|
265
|
+
* `where: { deletedAt: null }` or wrap the table accessor in a small helper.
|
|
264
266
|
*
|
|
265
267
|
* @example
|
|
266
268
|
* ```ts
|
|
@@ -272,16 +274,13 @@ export declare class TurbineClient {
|
|
|
272
274
|
* return result;
|
|
273
275
|
* });
|
|
274
276
|
*
|
|
275
|
-
* //
|
|
277
|
+
* // Result transformation middleware — redact a field on the way out
|
|
276
278
|
* db.$use(async (params, next) => {
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
* if (params.action === 'delete') {
|
|
281
|
-
* params.action = 'update';
|
|
282
|
-
* params.args = { where: params.args.where, data: { deletedAt: new Date() } };
|
|
279
|
+
* const result = await next(params);
|
|
280
|
+
* if (params.model === 'users' && Array.isArray(result)) {
|
|
281
|
+
* for (const row of result as { email?: string }[]) row.email = '[redacted]';
|
|
283
282
|
* }
|
|
284
|
-
* return
|
|
283
|
+
* return result;
|
|
285
284
|
* });
|
|
286
285
|
* ```
|
|
287
286
|
*/
|
package/dist/client.js
CHANGED
|
@@ -202,6 +202,14 @@ export class TurbineClient {
|
|
|
202
202
|
/** Active LISTEN subscriptions — torn down on disconnect() so it never hangs */
|
|
203
203
|
activeSubscriptions = new Set();
|
|
204
204
|
constructor(config = {}, schema) {
|
|
205
|
+
// Constructing without schema metadata previously crashed deep in the
|
|
206
|
+
// constructor with an opaque "Cannot read properties of undefined
|
|
207
|
+
// (reading 'tables')". Fail fast with an actionable message instead.
|
|
208
|
+
if (!schema || typeof schema !== 'object' || !schema.tables) {
|
|
209
|
+
throw new ValidationError('[turbine] TurbineClient requires schema metadata as its second argument. ' +
|
|
210
|
+
'Run `npx turbine generate` and use the generated client (`turbine()` from your output dir), ' +
|
|
211
|
+
'or pass the generated `schemaMetadata` object: new TurbineClient(config, schemaMetadata).');
|
|
212
|
+
}
|
|
205
213
|
/**
|
|
206
214
|
* Parse int8 (bigint, OID 20) as JavaScript number instead of string.
|
|
207
215
|
* Safe for values up to Number.MAX_SAFE_INTEGER (9,007,199,254,740,991).
|
|
@@ -315,12 +323,14 @@ export class TurbineClient {
|
|
|
315
323
|
// Middleware — intercept all queries
|
|
316
324
|
// -------------------------------------------------------------------------
|
|
317
325
|
/**
|
|
318
|
-
* Register a middleware function that runs
|
|
326
|
+
* Register a middleware function that runs around every query.
|
|
319
327
|
*
|
|
320
|
-
* Middleware can inspect and log query parameters,
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
328
|
+
* Middleware can inspect and log query parameters, measure timing, and
|
|
329
|
+
* transform the result returned by `next()`. Note: query SQL is generated
|
|
330
|
+
* BEFORE middleware runs — `params.args` is a read-only snapshot, and
|
|
331
|
+
* mutating it does NOT change the executed SQL. Cross-cutting filters
|
|
332
|
+
* (e.g. soft deletes) belong in the query itself: pass an explicit
|
|
333
|
+
* `where: { deletedAt: null }` or wrap the table accessor in a small helper.
|
|
324
334
|
*
|
|
325
335
|
* @example
|
|
326
336
|
* ```ts
|
|
@@ -332,16 +342,13 @@ export class TurbineClient {
|
|
|
332
342
|
* return result;
|
|
333
343
|
* });
|
|
334
344
|
*
|
|
335
|
-
* //
|
|
345
|
+
* // Result transformation middleware — redact a field on the way out
|
|
336
346
|
* db.$use(async (params, next) => {
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
* if (params.action === 'delete') {
|
|
341
|
-
* params.action = 'update';
|
|
342
|
-
* params.args = { where: params.args.where, data: { deletedAt: new Date() } };
|
|
347
|
+
* const result = await next(params);
|
|
348
|
+
* if (params.model === 'users' && Array.isArray(result)) {
|
|
349
|
+
* for (const row of result as { email?: string }[]) row.email = '[redacted]';
|
|
343
350
|
* }
|
|
344
|
-
* return
|
|
351
|
+
* return result;
|
|
345
352
|
* });
|
|
346
353
|
* ```
|
|
347
354
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -43,7 +43,7 @@ export { type IntrospectOptions, introspect } from './introspect.js';
|
|
|
43
43
|
export { executeNestedCreate, executeNestedUpdate, hasRelationFields, type NestedWriteContext, } from './nested-write.js';
|
|
44
44
|
export type { ObserveConfig, ObserveHandle } from './observe.js';
|
|
45
45
|
export { executePipeline, type PipelineOptions, type PipelineResults, pipelineSupported } from './pipeline.js';
|
|
46
|
-
export { type AggregateArgs, type AggregateResult, type ArrayFilter, type ConnectOrCreateOp, type CountArgs, type CreateArgs, type CreateManyArgs, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FieldResult, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GroupByArgs, type JsonFilter, type NestedCreateOp, type NestedUpdateOp, type OmitResult, type OrderByClause, type OrderDirection, type QueryEvent, type QueryEventListener, QueryInterface, type QueryResult, type RelationDescriptor, type RelationFilter, type SelectResult, type TextSearchFilter, type TypedWithClause, type UpdateArgs, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type VectorDistanceFilter, type VectorFilter, type VectorMetric, type VectorOrderBy, type VectorOrderByDistance, type WithClause, type WithOptions, type WithResult, } from './query/index.js';
|
|
46
|
+
export { type AggregateArgs, type AggregateResult, type ArrayFilter, type ConnectOrCreateOp, type CountArgs, type CreateArgs, type CreateDataInput, type CreateManyArgs, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FieldResult, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GroupByArgs, type HavingClause, type JsonFilter, type MiddlewareFn, type NestedCreateOp, type NestedUpdateOp, type NestedUpdateOpItem, type NestedUpsertOpItem, type OmitResult, type OrderByClause, type OrderDirection, type QueryEvent, type QueryEventListener, QueryInterface, type QueryResult, type RelationDescriptor, type RelationFilter, type SelectResult, type TextSearchFilter, type TypedWithClause, type UpdateArgs, type UpdateDataInput, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type VectorDistanceFilter, type VectorFilter, type VectorMetric, type VectorOrderBy, type VectorOrderByDistance, type WhereClause, type WhereOperator, type WhereValue, type WithClause, type WithOptions, type WithResult, } from './query/index.js';
|
|
47
47
|
export { type ActiveSubscription, type NotificationHandler, type Subscription, validateChannel } from './realtime.js';
|
|
48
48
|
export type { ColumnMetadata, IndexMetadata, RelationDef, SchemaMetadata, TableMetadata, } from './schema.js';
|
|
49
49
|
export { camelToSnake, isDateType, normalizeKeyColumns, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, } from './schema.js';
|
package/dist/query/builder.d.ts
CHANGED
|
@@ -168,10 +168,12 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
168
168
|
* Execute a query through the middleware chain.
|
|
169
169
|
* If no middlewares are registered, executes directly.
|
|
170
170
|
*
|
|
171
|
-
* Middleware can inspect and log query parameters,
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
171
|
+
* Middleware can inspect and log query parameters, measure timing, and
|
|
172
|
+
* transform the result returned by `next()`. Note: query SQL is generated
|
|
173
|
+
* BEFORE middleware runs — `params.args` is a read-only snapshot, and
|
|
174
|
+
* mutating it does NOT change the executed SQL. Cross-cutting filters
|
|
175
|
+
* (e.g. soft deletes) belong in the query itself: pass an explicit
|
|
176
|
+
* `where: { deletedAt: null }` or wrap the table accessor in a small helper.
|
|
175
177
|
*/
|
|
176
178
|
private executeWithMiddleware;
|
|
177
179
|
findUnique<W extends TypedWithClause<R> = {}, S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args: FindUniqueArgs<T, R, W, S, O>): Promise<QueryResult<T, R, W, S, O> | null>;
|
|
@@ -228,11 +230,11 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
228
230
|
buildFindFirstOrThrow<W extends TypedWithClause<R> = {}>(args?: FindManyArgs<T, R, W, Record<string, boolean> | undefined, Record<string, boolean> | undefined>): DeferredQuery<T>;
|
|
229
231
|
findUniqueOrThrow<W extends TypedWithClause<R> = {}, S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args: FindUniqueArgs<T, R, W, S, O>): Promise<QueryResult<T, R, W, S, O>>;
|
|
230
232
|
buildFindUniqueOrThrow<W extends TypedWithClause<R> = {}>(args: FindUniqueArgs<T, R, W, Record<string, boolean> | undefined, Record<string, boolean> | undefined>): DeferredQuery<T>;
|
|
231
|
-
create(args: CreateArgs<T>): Promise<T>;
|
|
233
|
+
create(args: CreateArgs<T, R>): Promise<T>;
|
|
232
234
|
buildCreate(args: CreateArgs<T>): DeferredQuery<T>;
|
|
233
235
|
createMany(args: CreateManyArgs<T>): Promise<T[]>;
|
|
234
236
|
buildCreateMany(args: CreateManyArgs<T>): DeferredQuery<T[]>;
|
|
235
|
-
update(args: UpdateArgs<T>): Promise<T>;
|
|
237
|
+
update(args: UpdateArgs<T, R>): Promise<T>;
|
|
236
238
|
buildUpdate(args: UpdateArgs<T>): DeferredQuery<T>;
|
|
237
239
|
private nestedCreate;
|
|
238
240
|
private nestedUpdate;
|
|
@@ -402,6 +404,41 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
402
404
|
* Uses the target table's column mapping to resolve field names.
|
|
403
405
|
*/
|
|
404
406
|
private buildSubWhereForRelation;
|
|
407
|
+
/**
|
|
408
|
+
* Resolve a column's Postgres type from an arbitrary table's metadata
|
|
409
|
+
* (relation targets, not just `this.table`).
|
|
410
|
+
*/
|
|
411
|
+
private pgTypeForColumn;
|
|
412
|
+
/**
|
|
413
|
+
* Equality-fallthrough guard shared by every SQL-build path AND every
|
|
414
|
+
* cache-hit param-collect path. A plain object literal that matched no known
|
|
415
|
+
* filter shape on a non-JSON column is almost always a misspelled operator
|
|
416
|
+
* (`startWith` for `startsWith`); binding it as `col = $1` silently returns
|
|
417
|
+
* wrong rows. Class instances (Buffer for bytea, Decimal wrappers, ...) are
|
|
418
|
+
* legitimate bind values and pass through, as do objects on json/jsonb
|
|
419
|
+
* columns (object equality).
|
|
420
|
+
*/
|
|
421
|
+
private assertBindableEqualityValue;
|
|
422
|
+
/**
|
|
423
|
+
* Build the user-supplied `where` filter of a relation `with` clause against
|
|
424
|
+
* the relation's table alias. Supports the same scalar surface as the
|
|
425
|
+
* top-level WHERE builder — equality, IS NULL, operator objects (incl.
|
|
426
|
+
* `mode: 'insensitive'`), and OR/AND/NOT combinators. Unknown operator
|
|
427
|
+
* objects throw via {@link assertBindableEqualityValue}.
|
|
428
|
+
*
|
|
429
|
+
* Param push order MUST mirror {@link collectAliasWhereParams} exactly, or
|
|
430
|
+
* cache hits and pipeline batching will desync.
|
|
431
|
+
*/
|
|
432
|
+
private buildAliasWhere;
|
|
433
|
+
/** Mirrors {@link buildAliasWhere} param-push order for the cache-hit collect path. */
|
|
434
|
+
private collectAliasWhereParams;
|
|
435
|
+
/**
|
|
436
|
+
* Value-invariant, shape-aware fingerprint for a relation `with` clause's
|
|
437
|
+
* `where` filter. Must distinguish every SQL shape {@link buildAliasWhere}
|
|
438
|
+
* can emit — equality vs null vs operator sets vs combinators — or two
|
|
439
|
+
* differently-shaped wheres would share one cached SQL string.
|
|
440
|
+
*/
|
|
441
|
+
private fingerprintAliasWhere;
|
|
405
442
|
/**
|
|
406
443
|
* Build SQL clauses for a single operator object on a column.
|
|
407
444
|
* Each operator key becomes its own clause, all ANDed together.
|