turbine-orm 0.28.0 → 0.28.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 +4 -4
- package/dist/cjs/errors.js +15 -1
- package/dist/cjs/powdb.js +1 -1
- package/dist/cjs/powql.js +1 -1
- package/dist/cjs/query/builder.js +75 -294
- package/dist/cjs/query/deferred.js +7 -0
- package/dist/cjs/query/filters.js +251 -0
- package/dist/errors.js +15 -1
- package/dist/powdb.d.ts +1 -1
- package/dist/powdb.js +1 -1
- package/dist/powql.d.ts +1 -1
- package/dist/powql.js +1 -1
- package/dist/query/builder.d.ts +3 -122
- package/dist/query/builder.js +1 -220
- package/dist/query/deferred.d.ts +130 -0
- package/dist/query/deferred.js +6 -0
- package/dist/query/filters.d.ts +120 -0
- package/dist/query/filters.js +232 -0
- package/package.json +2 -2
package/dist/query/builder.js
CHANGED
|
@@ -16,229 +16,10 @@ import { missingIndexForRelation } from '../index-advisor.js';
|
|
|
16
16
|
import { executeNestedCreate, executeNestedUpdate, hasRelationFields, } from '../nested-write.js';
|
|
17
17
|
import { camelToSnake, normalizeKeyColumns, snakeToCamel } from '../schema.js';
|
|
18
18
|
import { includeKeysForBatching, loadRelationsBatched, neededParentKeyFields, resolveCountRelations, stripFields, } from './batched-loader.js';
|
|
19
|
+
import { assertBindableEqualsOperand, findArrayUniqueKey, findJsonUniqueKey, fingerprintOperatorShape, isArrayFilter, isJsonFilter, isOrderBySpec, isTextSearchFilter, isUnmatchedPlainObject, isVectorFilter, isVectorOrderBy, isWhereOperator, normalizeOrderBy, sortedEntries, sortedKeys, UPDATE_OPERATOR_KEYS, VECTOR_DISTANCE_COMPARATORS, VECTOR_METRIC_OPERATORS, validateTextSearchConfig, } from './filters.js';
|
|
19
20
|
import { escapeLike, LRUCache, OPERATOR_KEYS, parseDbDate, sqlToPreparedName } from './utils.js';
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
// Internal detection helpers — used by QueryInterface
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
/** Check if a value is a where operator object (has at least one known operator key) */
|
|
24
|
-
function isWhereOperator(value) {
|
|
25
|
-
if (value === null ||
|
|
26
|
-
value === undefined ||
|
|
27
|
-
typeof value !== 'object' ||
|
|
28
|
-
Array.isArray(value) ||
|
|
29
|
-
value instanceof Date) {
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
const keys = Object.keys(value);
|
|
33
|
-
return keys.length > 0 && keys.every((k) => OPERATOR_KEYS.has(k));
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* True for a *plain object literal* that reached an equality fallthrough
|
|
37
|
-
* without matching any known filter shape — the misspelled-operator case.
|
|
38
|
-
* Class instances (Buffer for bytea, Decimal wrappers, ...) are legitimate
|
|
39
|
-
* bind values and return false, as do arrays and Dates.
|
|
40
|
-
*/
|
|
41
|
-
function isUnmatchedPlainObject(value) {
|
|
42
|
-
if (typeof value !== 'object' || value === null || Array.isArray(value) || value instanceof Date)
|
|
43
|
-
return false;
|
|
44
|
-
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value))
|
|
45
|
-
return false;
|
|
46
|
-
const proto = Object.getPrototypeOf(value);
|
|
47
|
-
return proto === Object.prototype || proto === null;
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Fingerprint the SHAPE of a where-operator object. Null-valued `equals` /
|
|
51
|
-
* `not` compile to parameterless `IS NULL` / `IS NOT NULL` (different SQL, no
|
|
52
|
-
* param pushed), so null-ness is part of the shape — without it a cache entry
|
|
53
|
-
* warmed by `{ not: 5 }` would serve `{ not: null }` with a desynced param list.
|
|
54
|
-
*/
|
|
55
|
-
function fingerprintOperatorShape(value) {
|
|
56
|
-
const obj = value;
|
|
57
|
-
const opKeys = Object.keys(obj)
|
|
58
|
-
.filter((k) => k !== 'mode')
|
|
59
|
-
.map((k) => ((k === 'equals' || k === 'not') && obj[k] === null ? `${k}:null` : k))
|
|
60
|
-
.sort();
|
|
61
|
-
const modeStr = value.mode === 'insensitive' ? ':i' : '';
|
|
62
|
-
return `op(${opKeys.join(',')}${modeStr})`;
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Guard for the value of an `equals` operator reaching the plain-equality
|
|
66
|
-
* operator path. A plain object literal can only legitimately be an equality
|
|
67
|
-
* value on a json/jsonb column — and those route to the JSONB filter branch
|
|
68
|
-
* BEFORE the operator branch, so any plain object that reaches here is a
|
|
69
|
-
* mistake (e.g. `{ equals: { foo: 1 } }` on a text column). Shared by the
|
|
70
|
-
* SQL-build path and the cache-hit param-collect path so a warmed cache can
|
|
71
|
-
* never skip the check.
|
|
72
|
-
*/
|
|
73
|
-
function assertBindableEqualsOperand(value, column) {
|
|
74
|
-
if (!isUnmatchedPlainObject(value))
|
|
75
|
-
return;
|
|
76
|
-
throw new ValidationError(`[turbine] Plain-object value for operator 'equals' on ${column}: ` +
|
|
77
|
-
`objects are only valid 'equals' values on JSON (json/jsonb) columns, ` +
|
|
78
|
-
`where 'equals' is the JSONB containment filter.`);
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Object keys in sorted order, mirroring the canonical order used by every
|
|
82
|
-
* cache fingerprint. The SQL-build and cache-hit param-collect paths MUST
|
|
83
|
-
* enumerate object keys in this exact order: fingerprints sort keys, so two
|
|
84
|
-
* where clauses with the same fields in different insertion order share one
|
|
85
|
-
* cache entry — if build/collect iterated insertion order, the cached SQL's
|
|
86
|
-
* `$N` placeholders would bind the wrong values (cross-tenant-leak class).
|
|
87
|
-
* Array order (OR/AND members) is positional and is never sorted.
|
|
88
|
-
*/
|
|
89
|
-
function sortedKeys(obj) {
|
|
90
|
-
return Object.keys(obj).sort();
|
|
91
|
-
}
|
|
92
|
-
/** {@link sortedKeys}, but yielding `[key, value]` pairs. */
|
|
93
|
-
function sortedEntries(obj) {
|
|
94
|
-
return Object.entries(obj).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
95
|
-
}
|
|
96
|
-
/** Known atomic-update operator keys — used to detect operator objects vs plain JSON values */
|
|
97
21
|
/** Relations already warned about missing FK indexes (once per process, dev only). */
|
|
98
22
|
const unindexedRelationWarned = new Set();
|
|
99
|
-
const UPDATE_OPERATOR_KEYS = new Set(['set', 'increment', 'decrement', 'multiply', 'divide']);
|
|
100
|
-
/** Known JSONB operator keys */
|
|
101
|
-
const JSONB_OPERATOR_KEYS = new Set(['path', 'equals', 'contains', 'hasKey']);
|
|
102
|
-
/**
|
|
103
|
-
* JSONB operator keys that are *unique* to {@link JsonFilter} — they cannot
|
|
104
|
-
* appear in any other where-filter shape, so the presence of one of these is
|
|
105
|
-
* an unambiguous signal that the user meant a JSON filter. Used by the
|
|
106
|
-
* strict-validation path so that `{ contains: 'foo' }` (which is also a valid
|
|
107
|
-
* `WhereOperator` for LIKE) is not misclassified. Note `equals` is NOT in this
|
|
108
|
-
* set: on non-JSON columns it is a plain equality operator (`WhereOperator`),
|
|
109
|
-
* so it must fall through instead of throwing.
|
|
110
|
-
*/
|
|
111
|
-
const JSONB_UNIQUE_KEYS = new Set(['path', 'hasKey']);
|
|
112
|
-
/** Check if a value is a JSONB filter object */
|
|
113
|
-
function isJsonFilter(value) {
|
|
114
|
-
if (value === null ||
|
|
115
|
-
value === undefined ||
|
|
116
|
-
typeof value !== 'object' ||
|
|
117
|
-
Array.isArray(value) ||
|
|
118
|
-
value instanceof Date) {
|
|
119
|
-
return false;
|
|
120
|
-
}
|
|
121
|
-
const keys = Object.keys(value);
|
|
122
|
-
return keys.length > 0 && keys.some((k) => JSONB_OPERATOR_KEYS.has(k));
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Returns the first JSON-unique key found in `value`, or `null` if none.
|
|
126
|
-
* Used to drive the strict-validation error message.
|
|
127
|
-
*/
|
|
128
|
-
function findJsonUniqueKey(value) {
|
|
129
|
-
for (const k of Object.keys(value)) {
|
|
130
|
-
if (JSONB_UNIQUE_KEYS.has(k))
|
|
131
|
-
return k;
|
|
132
|
-
}
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
/** Known Array operator keys */
|
|
136
|
-
const ARRAY_OPERATOR_KEYS = new Set(['has', 'hasEvery', 'hasSome', 'isEmpty']);
|
|
137
|
-
/**
|
|
138
|
-
* Array operator keys that are *unique* to {@link ArrayFilter}. None of the
|
|
139
|
-
* array operators currently overlap with `WhereOperator` or `JsonFilter`, so
|
|
140
|
-
* this set equals {@link ARRAY_OPERATOR_KEYS}; it is kept as a separate
|
|
141
|
-
* constant so a future overlap (e.g. a `contains` for arrays) is easy to
|
|
142
|
-
* carve out.
|
|
143
|
-
*/
|
|
144
|
-
const ARRAY_UNIQUE_KEYS = new Set(['has', 'hasEvery', 'hasSome', 'isEmpty']);
|
|
145
|
-
/** Check if a value is an Array filter object */
|
|
146
|
-
function isArrayFilter(value) {
|
|
147
|
-
if (value === null ||
|
|
148
|
-
value === undefined ||
|
|
149
|
-
typeof value !== 'object' ||
|
|
150
|
-
Array.isArray(value) ||
|
|
151
|
-
value instanceof Date) {
|
|
152
|
-
return false;
|
|
153
|
-
}
|
|
154
|
-
const keys = Object.keys(value);
|
|
155
|
-
return keys.length > 0 && keys.some((k) => ARRAY_OPERATOR_KEYS.has(k));
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
* Returns the first array-unique key found in `value`, or `null` if none.
|
|
159
|
-
* Used to drive the strict-validation error message.
|
|
160
|
-
*/
|
|
161
|
-
function findArrayUniqueKey(value) {
|
|
162
|
-
for (const k of Object.keys(value)) {
|
|
163
|
-
if (ARRAY_UNIQUE_KEYS.has(k))
|
|
164
|
-
return k;
|
|
165
|
-
}
|
|
166
|
-
return null;
|
|
167
|
-
}
|
|
168
|
-
/** Known text search operator keys */
|
|
169
|
-
const TEXT_SEARCH_KEYS = new Set(['search', 'config']);
|
|
170
|
-
/** Check if a value is a TextSearchFilter object */
|
|
171
|
-
function isTextSearchFilter(value) {
|
|
172
|
-
if (value === null ||
|
|
173
|
-
value === undefined ||
|
|
174
|
-
typeof value !== 'object' ||
|
|
175
|
-
Array.isArray(value) ||
|
|
176
|
-
value instanceof Date) {
|
|
177
|
-
return false;
|
|
178
|
-
}
|
|
179
|
-
const keys = Object.keys(value);
|
|
180
|
-
// Must have 'search' key and only known text search keys
|
|
181
|
-
return keys.includes('search') && keys.every((k) => TEXT_SEARCH_KEYS.has(k));
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* Validate a text search config name. Only alphanumeric characters and
|
|
185
|
-
* underscores are allowed to prevent SQL injection via the config parameter.
|
|
186
|
-
*/
|
|
187
|
-
function validateTextSearchConfig(config) {
|
|
188
|
-
return /^[a-zA-Z0-9_]+$/.test(config);
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* pgvector distance metric → operator allow-list. This is the ONLY mapping
|
|
192
|
-
* from a user-supplied metric token to a SQL operator; any token not present
|
|
193
|
-
* here is rejected, so a user value can never become an arbitrary operator.
|
|
194
|
-
*
|
|
195
|
-
* - `l2` → `<->` (Euclidean / L2 distance)
|
|
196
|
-
* - `cosine` → `<=>` (cosine distance)
|
|
197
|
-
* - `ip` → `<#>` (negative inner product)
|
|
198
|
-
*/
|
|
199
|
-
const VECTOR_METRIC_OPERATORS = {
|
|
200
|
-
l2: '<->',
|
|
201
|
-
cosine: '<=>',
|
|
202
|
-
ip: '<#>',
|
|
203
|
-
};
|
|
204
|
-
/** Comparison keys allowed on a {@link VectorDistanceFilter}. */
|
|
205
|
-
const VECTOR_DISTANCE_COMPARATORS = {
|
|
206
|
-
lt: '<',
|
|
207
|
-
lte: '<=',
|
|
208
|
-
gt: '>',
|
|
209
|
-
gte: '>=',
|
|
210
|
-
};
|
|
211
|
-
/** Check if a value is a vector distance WHERE filter: `{ distance: { to, metric } }` */
|
|
212
|
-
function isVectorFilter(value) {
|
|
213
|
-
if (value === null || typeof value !== 'object' || Array.isArray(value) || value instanceof Date) {
|
|
214
|
-
return false;
|
|
215
|
-
}
|
|
216
|
-
const dist = value.distance;
|
|
217
|
-
return (typeof dist === 'object' &&
|
|
218
|
-
dist !== null &&
|
|
219
|
-
!Array.isArray(dist) &&
|
|
220
|
-
'to' in dist &&
|
|
221
|
-
'metric' in dist);
|
|
222
|
-
}
|
|
223
|
-
/** Check if an orderBy value is a vector KNN ordering: `{ distance: { to, metric } }` */
|
|
224
|
-
function isVectorOrderBy(value) {
|
|
225
|
-
return isVectorFilter(value);
|
|
226
|
-
}
|
|
227
|
-
/** Check if an orderBy value is an explicit `{ sort, nulls? }` spec. */
|
|
228
|
-
function isOrderBySpec(value) {
|
|
229
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value) && 'sort' in value;
|
|
230
|
-
}
|
|
231
|
-
/**
|
|
232
|
-
* Normalize an orderBy value into `{ direction, nulls }`. Accepts a plain
|
|
233
|
-
* direction string or an {@link OrderBySpec}. Used by every ORDER BY compile
|
|
234
|
-
* path (findMany, groupBy, relation inner subqueries).
|
|
235
|
-
*/
|
|
236
|
-
function normalizeOrderBy(value) {
|
|
237
|
-
if (isOrderBySpec(value)) {
|
|
238
|
-
return { dir: value.sort.toLowerCase() === 'desc' ? 'DESC' : 'ASC', nulls: value.nulls };
|
|
239
|
-
}
|
|
240
|
-
return { dir: String(value).toLowerCase() === 'desc' ? 'DESC' : 'ASC' };
|
|
241
|
-
}
|
|
242
23
|
// biome-ignore lint/complexity/noBannedTypes: {} means "no relations known" — intentional for untyped table access
|
|
243
24
|
export class QueryInterface {
|
|
244
25
|
pool;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turbine-orm — Deferred query + QueryInterface option types
|
|
3
|
+
*
|
|
4
|
+
* Split from builder.ts so the class file focuses on SQL assembly / execution.
|
|
5
|
+
*/
|
|
6
|
+
import type pg from 'pg';
|
|
7
|
+
import type { Dialect } from '../dialect.js';
|
|
8
|
+
import type { SchemaMetadata } from '../schema.js';
|
|
9
|
+
import type { QueryInterface } from './builder.js';
|
|
10
|
+
import type { GlobalFilters, RelationLoadStrategy } from './types.js';
|
|
11
|
+
/**
|
|
12
|
+
* Runs a SQL statement and resolves its raw result. Passed to a
|
|
13
|
+
* {@link DeferredQuery.reselect} plan so it can run the write and the follow-up
|
|
14
|
+
* SELECT through the same timeout/instrumentation path as the primary query.
|
|
15
|
+
*/
|
|
16
|
+
export type ReselectExecutor = (sql: string, params: unknown[], preparedName?: string) => Promise<pg.QueryResult>;
|
|
17
|
+
export interface DeferredQuery<T> {
|
|
18
|
+
/** SQL text with $1, $2 placeholders */
|
|
19
|
+
sql: string;
|
|
20
|
+
/** Bound parameter values */
|
|
21
|
+
params: unknown[];
|
|
22
|
+
/** How to transform the raw pg.QueryResult into the final value */
|
|
23
|
+
transform: (result: pg.QueryResult) => T;
|
|
24
|
+
/** Tag for debugging / logging */
|
|
25
|
+
tag: string;
|
|
26
|
+
/** Prepared statement name (t_<16hex>). Set when SQL cache is enabled. */
|
|
27
|
+
preparedName?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Execution plan for dialects whose {@link Dialect.resultStrategy} is
|
|
30
|
+
* `'reselect'` (no RETURNING — e.g. MySQL). Owns the statement ordering: it
|
|
31
|
+
* runs the write and the follow-up row-fetching SELECT(s) via `exec`, and
|
|
32
|
+
* resolves the result whose rows {@link DeferredQuery.transform} consumes.
|
|
33
|
+
* Absent for `'returning'`/`'output'` dialects (the statement returns its own
|
|
34
|
+
* rows), so the PostgreSQL path never allocates or consults it.
|
|
35
|
+
*/
|
|
36
|
+
reselect?: (exec: ReselectExecutor) => Promise<pg.QueryResult>;
|
|
37
|
+
}
|
|
38
|
+
/** Middleware function type — imported from client to avoid circular deps */
|
|
39
|
+
export type MiddlewareFn = (params: {
|
|
40
|
+
model: string;
|
|
41
|
+
action: string;
|
|
42
|
+
args: Record<string, unknown>;
|
|
43
|
+
}, next: (params: {
|
|
44
|
+
model: string;
|
|
45
|
+
action: string;
|
|
46
|
+
args: Record<string, unknown>;
|
|
47
|
+
}) => Promise<unknown>) => Promise<unknown>;
|
|
48
|
+
/** Emitted after every query execution (success or failure). */
|
|
49
|
+
export interface QueryEvent {
|
|
50
|
+
sql: string;
|
|
51
|
+
params: unknown[];
|
|
52
|
+
duration: number;
|
|
53
|
+
model: string;
|
|
54
|
+
action: string;
|
|
55
|
+
rows: number;
|
|
56
|
+
timestamp: Date;
|
|
57
|
+
error?: Error;
|
|
58
|
+
}
|
|
59
|
+
export type QueryEventListener = (event: QueryEvent) => void;
|
|
60
|
+
/** Options passed from TurbineClient to QueryInterface */
|
|
61
|
+
export interface QueryInterfaceOptions {
|
|
62
|
+
/** Default LIMIT applied to findMany() when no limit is specified */
|
|
63
|
+
defaultLimit?: number;
|
|
64
|
+
/**
|
|
65
|
+
* Log a one-time warning when {@link QueryInterface.findMany} is called
|
|
66
|
+
* without a `limit`. Defaults to `true` so that accidental unbounded
|
|
67
|
+
* queries are surfaced loudly during development. Pass `false` to silence
|
|
68
|
+
* the warning entirely (e.g. for CLI tooling that intentionally streams
|
|
69
|
+
* full tables).
|
|
70
|
+
*/
|
|
71
|
+
warnOnUnlimited?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Enable prepared statements. When true, queries are submitted with a
|
|
74
|
+
* `{ name, text, values }` object to the pg driver, which caches the
|
|
75
|
+
* parse+plan on the server per connection.
|
|
76
|
+
*
|
|
77
|
+
* Default: `true` for Turbine-owned pools, `false` for external pools
|
|
78
|
+
* (serverless drivers may not support named statements).
|
|
79
|
+
*/
|
|
80
|
+
preparedStatements?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Enable the SQL template cache. When true, repeated queries with the
|
|
83
|
+
* same shape (same keys, operators, relations — different values) reuse
|
|
84
|
+
* cached SQL text instead of rebuilding from scratch.
|
|
85
|
+
*
|
|
86
|
+
* Default: `true`. Set to `false` as a nuclear kill switch.
|
|
87
|
+
*/
|
|
88
|
+
sqlCache?: boolean;
|
|
89
|
+
/** SQL dialect implementation. Defaults to PostgreSQL. */
|
|
90
|
+
dialect?: Dialect;
|
|
91
|
+
/**
|
|
92
|
+
* Interpret offset-less timestamp strings (Postgres `timestamp` without
|
|
93
|
+
* time zone, and the JSON emitted by nested-relation subqueries) as UTC.
|
|
94
|
+
* This is the Prisma/Rails/Django convention and makes results independent
|
|
95
|
+
* of the server's local time zone. Default: `true`. Set `false` to restore
|
|
96
|
+
* the pre-0.26 behavior (JS local-time interpretation).
|
|
97
|
+
*/
|
|
98
|
+
utcTimestamps?: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Client-level default relation-loading strategy for `with` clauses. Per-query
|
|
101
|
+
* `relationLoadStrategy` args override this; both default to `'join'`.
|
|
102
|
+
*/
|
|
103
|
+
relationLoadStrategy?: RelationLoadStrategy;
|
|
104
|
+
/**
|
|
105
|
+
* How nested-relation subqueries encode each row's JSON: `'object'` (default,
|
|
106
|
+
* `json_build_object`) or `'positional'` (`json_build_array`, key-less — see
|
|
107
|
+
* {@link Dialect.buildJsonArray}). Positional is Postgres-only in v1; a
|
|
108
|
+
* `with` clause on any other dialect throws `UnsupportedFeatureError` (E017).
|
|
109
|
+
*/
|
|
110
|
+
jsonEncoding?: 'object' | 'positional';
|
|
111
|
+
/**
|
|
112
|
+
* Automatic WHERE filters keyed by table accessor, AND-merged into every
|
|
113
|
+
* query on that table and every relation subquery targeting it (soft-delete /
|
|
114
|
+
* multi-tenancy). Function values are evaluated at query-build time. See
|
|
115
|
+
* {@link GlobalFilters}.
|
|
116
|
+
*/
|
|
117
|
+
globalFilters?: GlobalFilters;
|
|
118
|
+
/** @internal Set by TransactionClient — signals that this QI runs inside an active transaction. */
|
|
119
|
+
_txScoped?: boolean;
|
|
120
|
+
/** @internal Callback from TurbineClient for query event emission. */
|
|
121
|
+
_onQuery?: (event: QueryEvent) => void;
|
|
122
|
+
/**
|
|
123
|
+
* @internal Factory that builds the per-table query interface. Defaults to
|
|
124
|
+
* `new QueryInterface` (the SQL path). Non-SQL backends (PowDB) supply a
|
|
125
|
+
* factory returning a structurally-compatible interface that generates their
|
|
126
|
+
* own query language instead of SQL. The SQL dialects never set this, so their
|
|
127
|
+
* `table()` behavior is byte-identical.
|
|
128
|
+
*/
|
|
129
|
+
queryInterfaceFactory?: (pool: pg.Pool, table: string, schema: SchemaMetadata, middlewares: MiddlewareFn[], options: QueryInterfaceOptions) => QueryInterface<object>;
|
|
130
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turbine-orm — Where-filter type guards and shape helpers
|
|
3
|
+
*
|
|
4
|
+
* Pure detection / fingerprint utilities used by the query builder's WHERE
|
|
5
|
+
* compiler. Kept out of builder.ts so the class file stays about SQL assembly
|
|
6
|
+
* and execution rather than filter-shape bookkeeping.
|
|
7
|
+
*/
|
|
8
|
+
import type { ArrayFilter, JsonFilter, OrderBySpec, OrderDirection, TextSearchFilter, VectorFilter, VectorOrderBy, WhereOperator } from './types.js';
|
|
9
|
+
/** Check if a value is a where operator object (has at least one known operator key) */
|
|
10
|
+
export declare function isWhereOperator(value: unknown): value is WhereOperator;
|
|
11
|
+
/**
|
|
12
|
+
* True for a *plain object literal* that reached an equality fallthrough
|
|
13
|
+
* without matching any known filter shape — the misspelled-operator case.
|
|
14
|
+
* Class instances (Buffer for bytea, Decimal wrappers, ...) are legitimate
|
|
15
|
+
* bind values and return false, as do arrays and Dates.
|
|
16
|
+
*/
|
|
17
|
+
export declare function isUnmatchedPlainObject(value: unknown): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Fingerprint the SHAPE of a where-operator object. Null-valued `equals` /
|
|
20
|
+
* `not` compile to parameterless `IS NULL` / `IS NOT NULL` (different SQL, no
|
|
21
|
+
* param pushed), so null-ness is part of the shape — without it a cache entry
|
|
22
|
+
* warmed by `{ not: 5 }` would serve `{ not: null }` with a desynced param list.
|
|
23
|
+
*/
|
|
24
|
+
export declare function fingerprintOperatorShape(value: WhereOperator): string;
|
|
25
|
+
/**
|
|
26
|
+
* Guard for the value of an `equals` operator reaching the plain-equality
|
|
27
|
+
* operator path. A plain object literal can only legitimately be an equality
|
|
28
|
+
* value on a json/jsonb column — and those route to the JSONB filter branch
|
|
29
|
+
* BEFORE the operator branch, so any plain object that reaches here is a
|
|
30
|
+
* mistake (e.g. `{ equals: { foo: 1 } }` on a text column). Shared by the
|
|
31
|
+
* SQL-build path and the cache-hit param-collect path so a warmed cache can
|
|
32
|
+
* never skip the check.
|
|
33
|
+
*/
|
|
34
|
+
export declare function assertBindableEqualsOperand(value: unknown, column: string): void;
|
|
35
|
+
/**
|
|
36
|
+
* Object keys in sorted order, mirroring the canonical order used by every
|
|
37
|
+
* cache fingerprint. The SQL-build and cache-hit param-collect paths MUST
|
|
38
|
+
* enumerate object keys in this exact order: fingerprints sort keys, so two
|
|
39
|
+
* where clauses with the same fields in different insertion order share one
|
|
40
|
+
* cache entry — if build/collect iterated insertion order, the cached SQL's
|
|
41
|
+
* `$N` placeholders would bind the wrong values (cross-tenant-leak class).
|
|
42
|
+
* Array order (OR/AND members) is positional and is never sorted.
|
|
43
|
+
*/
|
|
44
|
+
export declare function sortedKeys(obj: Record<string, unknown>): string[];
|
|
45
|
+
/** {@link sortedKeys}, but yielding `[key, value]` pairs. */
|
|
46
|
+
export declare function sortedEntries<V>(obj: Record<string, V>): [string, V][];
|
|
47
|
+
/** Known atomic-update operator keys — used to detect operator objects vs plain JSON values */
|
|
48
|
+
export declare const UPDATE_OPERATOR_KEYS: Set<string>;
|
|
49
|
+
/** Known JSONB operator keys */
|
|
50
|
+
export declare const JSONB_OPERATOR_KEYS: Set<string>;
|
|
51
|
+
/**
|
|
52
|
+
* JSONB operator keys that are *unique* to {@link JsonFilter} — they cannot
|
|
53
|
+
* appear in any other where-filter shape, so the presence of one of these is
|
|
54
|
+
* an unambiguous signal that the user meant a JSON filter. Used by the
|
|
55
|
+
* strict-validation path so that `{ contains: 'foo' }` (which is also a valid
|
|
56
|
+
* `WhereOperator` for LIKE) is not misclassified. Note `equals` is NOT in this
|
|
57
|
+
* set: on non-JSON columns it is a plain equality operator (`WhereOperator`),
|
|
58
|
+
* so it must fall through instead of throwing.
|
|
59
|
+
*/
|
|
60
|
+
export declare const JSONB_UNIQUE_KEYS: Set<string>;
|
|
61
|
+
/** Check if a value is a JSONB filter object */
|
|
62
|
+
export declare function isJsonFilter(value: unknown): value is JsonFilter;
|
|
63
|
+
/**
|
|
64
|
+
* Returns the first JSON-unique key found in `value`, or `null` if none.
|
|
65
|
+
* Used to drive the strict-validation error message.
|
|
66
|
+
*/
|
|
67
|
+
export declare function findJsonUniqueKey(value: object): string | null;
|
|
68
|
+
/** Known Array operator keys */
|
|
69
|
+
export declare const ARRAY_OPERATOR_KEYS: Set<string>;
|
|
70
|
+
/**
|
|
71
|
+
* Array operator keys that are *unique* to {@link ArrayFilter}. None of the
|
|
72
|
+
* array operators currently overlap with `WhereOperator` or `JsonFilter`, so
|
|
73
|
+
* this set equals {@link ARRAY_OPERATOR_KEYS}; it is kept as a separate
|
|
74
|
+
* constant so a future overlap (e.g. a `contains` for arrays) is easy to
|
|
75
|
+
* carve out.
|
|
76
|
+
*/
|
|
77
|
+
export declare const ARRAY_UNIQUE_KEYS: Set<string>;
|
|
78
|
+
/** Check if a value is an Array filter object */
|
|
79
|
+
export declare function isArrayFilter(value: unknown): value is ArrayFilter;
|
|
80
|
+
/**
|
|
81
|
+
* Returns the first array-unique key found in `value`, or `null` if none.
|
|
82
|
+
* Used to drive the strict-validation error message.
|
|
83
|
+
*/
|
|
84
|
+
export declare function findArrayUniqueKey(value: object): string | null;
|
|
85
|
+
/** Known text search operator keys */
|
|
86
|
+
export declare const TEXT_SEARCH_KEYS: Set<string>;
|
|
87
|
+
/** Check if a value is a TextSearchFilter object */
|
|
88
|
+
export declare function isTextSearchFilter(value: unknown): value is TextSearchFilter;
|
|
89
|
+
/**
|
|
90
|
+
* Validate a text search config name. Only alphanumeric characters and
|
|
91
|
+
* underscores are allowed to prevent SQL injection via the config parameter.
|
|
92
|
+
*/
|
|
93
|
+
export declare function validateTextSearchConfig(config: string): boolean;
|
|
94
|
+
/**
|
|
95
|
+
* pgvector distance metric → operator allow-list. This is the ONLY mapping
|
|
96
|
+
* from a user-supplied metric token to a SQL operator; any token not present
|
|
97
|
+
* here is rejected, so a user value can never become an arbitrary operator.
|
|
98
|
+
*
|
|
99
|
+
* - `l2` → `<->` (Euclidean / L2 distance)
|
|
100
|
+
* - `cosine` → `<=>` (cosine distance)
|
|
101
|
+
* - `ip` → `<#>` (negative inner product)
|
|
102
|
+
*/
|
|
103
|
+
export declare const VECTOR_METRIC_OPERATORS: Record<string, string>;
|
|
104
|
+
/** Comparison keys allowed on a {@link VectorDistanceFilter}. */
|
|
105
|
+
export declare const VECTOR_DISTANCE_COMPARATORS: Record<string, string>;
|
|
106
|
+
/** Check if a value is a vector distance WHERE filter: `{ distance: { to, metric } }` */
|
|
107
|
+
export declare function isVectorFilter(value: unknown): value is VectorFilter;
|
|
108
|
+
/** Check if an orderBy value is a vector KNN ordering: `{ distance: { to, metric } }` */
|
|
109
|
+
export declare function isVectorOrderBy(value: unknown): value is VectorOrderBy;
|
|
110
|
+
/** Check if an orderBy value is an explicit `{ sort, nulls? }` spec. */
|
|
111
|
+
export declare function isOrderBySpec(value: unknown): value is OrderBySpec;
|
|
112
|
+
/**
|
|
113
|
+
* Normalize an orderBy value into `{ direction, nulls }`. Accepts a plain
|
|
114
|
+
* direction string or an {@link OrderBySpec}. Used by every ORDER BY compile
|
|
115
|
+
* path (findMany, groupBy, relation inner subqueries).
|
|
116
|
+
*/
|
|
117
|
+
export declare function normalizeOrderBy(value: OrderDirection | OrderBySpec): {
|
|
118
|
+
dir: 'ASC' | 'DESC';
|
|
119
|
+
nulls?: 'first' | 'last';
|
|
120
|
+
};
|