@atscript/db 0.1.146 → 0.1.147
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/dist/agg.cjs +1 -1
- package/dist/agg.d.cts +1 -1
- package/dist/agg.d.mts +1 -1
- package/dist/agg.mjs +1 -1
- package/dist/{aggregate-fns-CfsveE1w.mjs → aggregate-fns-C0BymEqa.mjs} +3 -1
- package/dist/{aggregate-fns-CGBv3E8S.cjs → aggregate-fns-CtsRbZn9.cjs} +8 -0
- package/dist/{buckets-DYFu0eZ8.d.cts → buckets-CjdPipCC.d.cts} +522 -17
- package/dist/{buckets-DRycmhOW.d.mts → buckets-DYopfd2Y.d.mts} +522 -17
- package/dist/{column-diff-e2oHc71_.mjs → column-diff-CO06iQvI.mjs} +862 -115
- package/dist/{column-diff-D_Kyuh0S.cjs → column-diff-Dt7EDJsA.cjs} +872 -113
- package/dist/{column-diff-Q9UmWn5x.d.mts → fk-diff-BtOmL4OW.d.mts} +48 -2
- package/dist/{column-diff-w-Mym_3w.d.cts → fk-diff-DLqwVkKG.d.cts} +48 -2
- package/dist/index.cjs +28 -33
- package/dist/index.d.cts +40 -15
- package/dist/index.d.mts +40 -15
- package/dist/index.mjs +5 -33
- package/dist/{nested-writer-CnOOAehr.mjs → nested-writer-D35UsDBT.mjs} +542 -2
- package/dist/{nested-writer-xfQwxplL.cjs → nested-writer-SqtYJHF0.cjs} +666 -0
- package/dist/plugin.cjs +266 -5
- package/dist/plugin.mjs +267 -6
- package/dist/rel.cjs +2 -2
- package/dist/rel.d.cts +2 -2
- package/dist/rel.d.mts +2 -2
- package/dist/rel.mjs +2 -2
- package/dist/{relation-helpers-B-0NRKat.d.mts → relation-helpers-C2bLtE-t.d.mts} +1 -1
- package/dist/{relation-helpers-BOMm_HUI.d.cts → relation-helpers-DH3LvwLV.d.cts} +1 -1
- package/dist/relation-loader-BFhVuJ-B.cjs +369 -0
- package/dist/relation-loader-C0qE8xgR.mjs +370 -0
- package/dist/shared.cjs +1 -1
- package/dist/shared.d.cts +4 -3
- package/dist/shared.d.mts +4 -3
- package/dist/shared.mjs +1 -1
- package/dist/sync.cjs +10 -6
- package/dist/sync.d.cts +2 -25
- package/dist/sync.d.mts +2 -25
- package/dist/sync.mjs +10 -6
- package/dist/{validation-utils-DOsB4e6G.cjs → validation-utils-Da2GjobR.cjs} +35 -24
- package/dist/{validation-utils-CMR4fe2M.mjs → validation-utils-Dq0uZ7ef.mjs} +35 -24
- package/dist/{validator-Bw6ks9Hy.d.mts → validator-CMnTI6r7.d.cts} +9 -6
- package/dist/{validator-Bw6ks9Hy.d.cts → validator-CMnTI6r7.d.mts} +9 -6
- package/dist/validator.d.cts +1 -1
- package/dist/validator.d.mts +1 -1
- package/package.json +8 -8
- package/dist/relation-loader-CBPY6kM7.cjs +0 -461
- package/dist/relation-loader-D9XuXaMv.mjs +0 -462
|
@@ -1,6 +1,100 @@
|
|
|
1
1
|
const require_db_error = require("./db-error-DTkkeu5b.cjs");
|
|
2
2
|
const require_object = require("./object-Djg28csK.cjs");
|
|
3
3
|
let _atscript_typescript_utils = require("@atscript/typescript/utils");
|
|
4
|
+
let _uniqu_core = require("@uniqu/core");
|
|
5
|
+
//#region src/query/query-tree.ts
|
|
6
|
+
/**
|
|
7
|
+
* `true` when a query-tree operand is a field reference (`{ field, type? }`)
|
|
8
|
+
* rather than a literal — e.g. the right side of a field-to-field comparison.
|
|
9
|
+
* @since 0.1.136
|
|
10
|
+
*/
|
|
11
|
+
function isFieldRef(value) {
|
|
12
|
+
return value !== null && typeof value === "object" && "field" in value;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Translates a JS-emitted query tree into a FilterExpr.
|
|
16
|
+
* Resolves field references (type + field path) to physical column names
|
|
17
|
+
* via the provided resolver function.
|
|
18
|
+
*/
|
|
19
|
+
function translateQueryTree(node, resolveField) {
|
|
20
|
+
if ("$and" in node) return { $and: node.$and.map((n) => translateQueryTree(n, resolveField)) };
|
|
21
|
+
if ("$or" in node) return { $or: node.$or.map((n) => translateQueryTree(n, resolveField)) };
|
|
22
|
+
if ("$not" in node) return { $not: translateQueryTree(node.$not, resolveField) };
|
|
23
|
+
const comp = node;
|
|
24
|
+
const leftField = resolveField(comp.left);
|
|
25
|
+
if (isFieldRef(comp.right)) {
|
|
26
|
+
const rightField = resolveField(comp.right);
|
|
27
|
+
return { [leftField]: { [comp.op]: { $field: rightField } } };
|
|
28
|
+
}
|
|
29
|
+
if (comp.op === "$exists") return { [leftField]: { $exists: comp.right !== false } };
|
|
30
|
+
return { [leftField]: { [comp.op]: comp.right } };
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Calls `leaf` with the field path of every field-reference leaf of a
|
|
34
|
+
* computed-column expression (`@db.compute`), in source order — leaves name
|
|
35
|
+
* the view's own fields.
|
|
36
|
+
* @since 0.1.147
|
|
37
|
+
*/
|
|
38
|
+
function walkViewExpr(expr, leaf) {
|
|
39
|
+
if (typeof expr === "number") return;
|
|
40
|
+
if ("field" in expr) {
|
|
41
|
+
leaf(expr.field);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
for (const arg of expr.args) walkViewExpr(arg, leaf);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Whether a computed-column expression may yield NULL: a `/` (division by
|
|
48
|
+
* zero is NULL), a leaf for which `nullableLeaf` holds, or an operation over
|
|
49
|
+
* a nullable operand — `coalesce` only when every argument is nullable.
|
|
50
|
+
* @since 0.1.147
|
|
51
|
+
*/
|
|
52
|
+
function viewExprNullable(expr, nullableLeaf) {
|
|
53
|
+
if (typeof expr === "number") return false;
|
|
54
|
+
if ("field" in expr) return nullableLeaf(expr.field);
|
|
55
|
+
if (expr.op === "/") return true;
|
|
56
|
+
if (expr.op === "coalesce") return expr.args.every((a) => viewExprNullable(a, nullableLeaf));
|
|
57
|
+
return expr.args.some((a) => viewExprNullable(a, nullableLeaf));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The `@db.compute` expression of a view field, if any.
|
|
61
|
+
* @since 0.1.147
|
|
62
|
+
*/
|
|
63
|
+
function computeOf(fieldType) {
|
|
64
|
+
return fieldType?.metadata.get("db.compute");
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The transitive non-computed operands of the `@db.compute` field `field` of
|
|
68
|
+
* `viewType`: its leaves, a computed leaf replaced by its own operands, and
|
|
69
|
+
* (`via`) the computed fields passed through. `undefined` when `field` is not
|
|
70
|
+
* computed. Cycles are not followed (they are rejected where the view's
|
|
71
|
+
* columns are built).
|
|
72
|
+
* @since 0.1.147
|
|
73
|
+
*/
|
|
74
|
+
function computedOperands(viewType, field) {
|
|
75
|
+
const props = viewType.type.kind === "object" ? viewType.type.props : void 0;
|
|
76
|
+
const exprOf = (name) => computeOf(props?.get(name));
|
|
77
|
+
const root = exprOf(field);
|
|
78
|
+
if (root === void 0) return void 0;
|
|
79
|
+
const out = /* @__PURE__ */ new Set();
|
|
80
|
+
const via = [];
|
|
81
|
+
const seen = new Set([field]);
|
|
82
|
+
const visit = (expr) => walkViewExpr(expr, (path) => {
|
|
83
|
+
const nested = exprOf(path);
|
|
84
|
+
if (nested === void 0) out.add(path);
|
|
85
|
+
else if (!seen.has(path)) {
|
|
86
|
+
seen.add(path);
|
|
87
|
+
via.push(path);
|
|
88
|
+
visit(nested);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
visit(root);
|
|
92
|
+
return {
|
|
93
|
+
operands: [...out],
|
|
94
|
+
via
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
//#endregion
|
|
4
98
|
//#region src/rel/relation-helpers.ts
|
|
5
99
|
/**
|
|
6
100
|
* Finds the FK entry that connects a `@db.rel.to` relation to its target.
|
|
@@ -52,6 +146,452 @@ function tableNameOf(type) {
|
|
|
52
146
|
function resolveRelationTargetTable(relation) {
|
|
53
147
|
return tableNameOf(relation.targetType());
|
|
54
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* A string key of `fields`' values on `obj` — equal for rows that agree on
|
|
151
|
+
* every field (`null` / `undefined` key distinctly from any string). `read`
|
|
152
|
+
* reads one field (default: a top-level property).
|
|
153
|
+
*/
|
|
154
|
+
function compositeKey(fields, obj, read = (o, f) => o[f]) {
|
|
155
|
+
let key = "";
|
|
156
|
+
for (let i = 0; i < fields.length; i++) {
|
|
157
|
+
if (i > 0) key += "\0\0";
|
|
158
|
+
const v = read(obj, fields[i]);
|
|
159
|
+
key += v === null || v === void 0 ? "\0" : String(v);
|
|
160
|
+
}
|
|
161
|
+
return key;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Keeps, of each group of `rows` sharing `keyOf(row)`, the rows `page`
|
|
165
|
+
* selects — `$skip` / `$limit` applied per group, in the order of `rows`.
|
|
166
|
+
* The order of the kept rows is preserved.
|
|
167
|
+
*/
|
|
168
|
+
function slicePerGroup(rows, keyOf, page) {
|
|
169
|
+
const skip = page.skip ?? 0;
|
|
170
|
+
const end = page.limit === void 0 ? Infinity : skip + page.limit;
|
|
171
|
+
const seen = /* @__PURE__ */ new Map();
|
|
172
|
+
const kept = [];
|
|
173
|
+
for (const row of rows) {
|
|
174
|
+
const key = keyOf(row);
|
|
175
|
+
const index = seen.get(key) ?? 0;
|
|
176
|
+
seen.set(key, index + 1);
|
|
177
|
+
if (index >= skip && index < end) kept.push(row);
|
|
178
|
+
}
|
|
179
|
+
return kept;
|
|
180
|
+
}
|
|
181
|
+
//#endregion
|
|
182
|
+
//#region src/query/relation-filter.ts
|
|
183
|
+
/**
|
|
184
|
+
* Relational filter predicates (`{ nav: { $some | $none: <filter on the
|
|
185
|
+
* related table> } }`) — resolution against the related tables.
|
|
186
|
+
*
|
|
187
|
+
* Semantics: `rel(r, nav)` is exactly the set of related rows `$with=nav`
|
|
188
|
+
* loads for row `r` (same foreign-key pairing, the relation's
|
|
189
|
+
* `@db.rel.filter` included). `$some: F` holds when one of them matches `F`,
|
|
190
|
+
* `$none: F` when none does; a NULL foreign-key component means "no related
|
|
191
|
+
* row". The core resolves each predicate into a {@link ResolvedRelationFilter}
|
|
192
|
+
* (physical names on every side, the inner filter translated by the related
|
|
193
|
+
* table's own field mapper) before the adapter sees the filter — adapters
|
|
194
|
+
* only render it.
|
|
195
|
+
*
|
|
196
|
+
* @since 0.1.147
|
|
197
|
+
*/
|
|
198
|
+
/**
|
|
199
|
+
* Maximum nesting of relational predicates in one filter, server-added ones
|
|
200
|
+
* included (a predicate inside a predicate's operand counts one level). The
|
|
201
|
+
* core backstop for every caller; higher than moost-db's per-client limit
|
|
202
|
+
* (`REL_FILTER_CLIENT_MAX_DEPTH`) so server overlays (row scopes,
|
|
203
|
+
* `transformRelationFilter`) have headroom above what a client may send.
|
|
204
|
+
*/
|
|
205
|
+
const REL_FILTER_MAX_DEPTH = 4;
|
|
206
|
+
/** Maximum number of relational predicates in one filter (nested and server-added ones included). See {@link REL_FILTER_MAX_DEPTH}. */
|
|
207
|
+
const REL_FILTER_MAX_NODES = 16;
|
|
208
|
+
/**
|
|
209
|
+
* Cross-realm brand of {@link ResolvedRelationFilter}: `instanceof` fails when
|
|
210
|
+
* two copies of `@atscript/db` are loaded (ESM + CJS, or nested installs) and
|
|
211
|
+
* an adapter from one sees nodes built by the other.
|
|
212
|
+
*/
|
|
213
|
+
const RESOLVED_BRAND = Symbol.for("@atscript/db:ResolvedRelationFilter");
|
|
214
|
+
/**
|
|
215
|
+
* A relational predicate as adapters receive it — the operand of
|
|
216
|
+
* `FilterVisitor.relation(field, op, operand)` once the core translated the
|
|
217
|
+
* filter. Every name is physical:
|
|
218
|
+
*
|
|
219
|
+
* - `to` / `from`: `pairs` correlate a SOURCE column with a TARGET column
|
|
220
|
+
* (`target.<pair.target> = source.<pair.source>`, one pair per composite
|
|
221
|
+
* key part);
|
|
222
|
+
* - `via`: `pairs` is empty — `junction.toSource` correlates the junction
|
|
223
|
+
* with the source row, `junction.toTarget` with the target row.
|
|
224
|
+
*
|
|
225
|
+
* `filter` is the inner filter on the TARGET (already translated by the
|
|
226
|
+
* target's field mapper: renames, flattening, value formatters; nested
|
|
227
|
+
* predicates resolved the same way), conjoined with the target part of the
|
|
228
|
+
* relation's `@db.rel.filter`. `{}` matches every related row.
|
|
229
|
+
*
|
|
230
|
+
* @since 0.1.147
|
|
231
|
+
*/
|
|
232
|
+
var ResolvedRelationFilter = class {
|
|
233
|
+
/** @internal cross-realm brand (see {@link isResolvedRelationFilter}). */
|
|
234
|
+
[RESOLVED_BRAND] = true;
|
|
235
|
+
kind;
|
|
236
|
+
/** Logical navigation field name on the source table. */
|
|
237
|
+
nav;
|
|
238
|
+
source;
|
|
239
|
+
target;
|
|
240
|
+
pairs;
|
|
241
|
+
junction;
|
|
242
|
+
filter;
|
|
243
|
+
constructor(init) {
|
|
244
|
+
this.kind = init.kind;
|
|
245
|
+
this.nav = init.nav;
|
|
246
|
+
this.source = init.source;
|
|
247
|
+
this.target = init.target;
|
|
248
|
+
this.pairs = init.pairs;
|
|
249
|
+
this.junction = init.junction;
|
|
250
|
+
this.filter = init.filter;
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
/** `true` for a {@link ResolvedRelationFilter} (the resolved operand of a predicate). */
|
|
254
|
+
function isResolvedRelationFilter(value) {
|
|
255
|
+
return typeof value === "object" && value !== null && value[RESOLVED_BRAND] === true;
|
|
256
|
+
}
|
|
257
|
+
/** `true` when `value` (a filter entry's value) is an operator map with a `$some` / `$none` key. */
|
|
258
|
+
function hasRelationOp(value) {
|
|
259
|
+
if (!require_object.isPlainObject(value)) return false;
|
|
260
|
+
for (const key in value) if ((0, _uniqu_core.isRelationOp)(key)) return true;
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Pre-scan results of filters the core BUILT (translated / resolved
|
|
265
|
+
* filters, never mutated after) — adapters scan the same translated filter
|
|
266
|
+
* several times per operation. Caller-owned filters are never cached: they
|
|
267
|
+
* may be mutated between two queries.
|
|
268
|
+
*/
|
|
269
|
+
const relationFilterCache = /* @__PURE__ */ new WeakMap();
|
|
270
|
+
/** @internal Records the pre-scan result of a filter the core built. */
|
|
271
|
+
function noteRelationFilter(filter, has) {
|
|
272
|
+
if (filter && typeof filter === "object") relationFilterCache.set(filter, has);
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* `true` when `filter` holds a relational predicate anywhere outside
|
|
276
|
+
* predicate operands (through `$and` / `$or` / `$not`) — the cheap pre-scan
|
|
277
|
+
* the field mappers and renderers use to keep predicate-free filters on
|
|
278
|
+
* their fast paths. Results for filters the core translated are cached.
|
|
279
|
+
*/
|
|
280
|
+
function containsRelationFilter(filter) {
|
|
281
|
+
if (!filter || typeof filter !== "object") return false;
|
|
282
|
+
const cached = relationFilterCache.get(filter);
|
|
283
|
+
if (cached !== void 0) return cached;
|
|
284
|
+
for (const [key, value] of Object.entries(filter)) if (key === "$and" || key === "$or") {
|
|
285
|
+
if (Array.isArray(value) && value.some((child) => containsRelationFilter(child))) return true;
|
|
286
|
+
} else if (key === "$not") {
|
|
287
|
+
if (containsRelationFilter(value)) return true;
|
|
288
|
+
} else if (!key.startsWith("$") && hasRelationOp(value)) return true;
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Calls `visit` for every resolved predicate of a TRANSLATED filter,
|
|
293
|
+
* depth-first: the top-level ones, then (with `nested`) those inside each
|
|
294
|
+
* operand — target filters and junction filters alike. Adapters use it to
|
|
295
|
+
* prepare per-predicate data (memory snapshots, self-referencing checks).
|
|
296
|
+
*/
|
|
297
|
+
function forEachResolvedRelation(filter, visit, nested = true) {
|
|
298
|
+
if (!filter || typeof filter !== "object") return;
|
|
299
|
+
for (const [key, value] of Object.entries(filter)) if (key === "$and" || key === "$or") {
|
|
300
|
+
if (Array.isArray(value)) for (const child of value) forEachResolvedRelation(child, visit, nested);
|
|
301
|
+
} else if (key === "$not") forEachResolvedRelation(value, visit, nested);
|
|
302
|
+
else if (!key.startsWith("$") && require_object.isPlainObject(value)) for (const [op, operand] of Object.entries(value)) {
|
|
303
|
+
if (!(0, _uniqu_core.isRelationOp)(op) || !isResolvedRelationFilter(operand)) continue;
|
|
304
|
+
visit(operand, op);
|
|
305
|
+
if (nested) {
|
|
306
|
+
forEachResolvedRelation(operand.filter, visit, nested);
|
|
307
|
+
forEachResolvedRelation(operand.junction?.filter, visit, nested);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/** A fresh {@link TRelGuardState} for one query. */
|
|
312
|
+
function relGuardState(write = false) {
|
|
313
|
+
return {
|
|
314
|
+
depth: 0,
|
|
315
|
+
counter: { nodes: 0 },
|
|
316
|
+
write,
|
|
317
|
+
path: ""
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function invalid(path, message) {
|
|
321
|
+
return new require_db_error.DbError("INVALID_QUERY", [{
|
|
322
|
+
path,
|
|
323
|
+
message
|
|
324
|
+
}]);
|
|
325
|
+
}
|
|
326
|
+
function notSupported(path, message) {
|
|
327
|
+
return new require_db_error.DbError("REL_FILTER_NOT_SUPPORTED", [{
|
|
328
|
+
path,
|
|
329
|
+
message
|
|
330
|
+
}]);
|
|
331
|
+
}
|
|
332
|
+
/** The present, non-empty `parts` ANDed (`{}` when none, a single one as is). */
|
|
333
|
+
function andFilters(...parts) {
|
|
334
|
+
const present = parts.filter((p) => p != null && Object.keys(p).length > 0);
|
|
335
|
+
if (present.length === 0) return {};
|
|
336
|
+
if (present.length === 1) return present[0];
|
|
337
|
+
return { $and: present };
|
|
338
|
+
}
|
|
339
|
+
const NO_STATIC_FILTER = {};
|
|
340
|
+
const staticFilterCache = /* @__PURE__ */ new WeakMap();
|
|
341
|
+
/**
|
|
342
|
+
* A relation's `@db.rel.filter` as logical filters per side — what `$with`
|
|
343
|
+
* loading and relational predicates both AND into the related rows (the
|
|
344
|
+
* filter is part of the relation's meaning). Top-level `and` conditions are
|
|
345
|
+
* split by side: an unqualified field and the related type's fields go to
|
|
346
|
+
* `target`, the `@db.rel.via` junction's to `junction`. A single condition
|
|
347
|
+
* that reads both sides (e.g. an `or` across them) or compares two fields is
|
|
348
|
+
* rejected with `INVALID_QUERY` — `name` is the navigation field (error path).
|
|
349
|
+
*
|
|
350
|
+
* @since 0.1.147
|
|
351
|
+
*/
|
|
352
|
+
function relationStaticFilter(relation, name = "") {
|
|
353
|
+
if (!relation.filter) return NO_STATIC_FILTER;
|
|
354
|
+
const cached = staticFilterCache.get(relation);
|
|
355
|
+
if (cached) return cached;
|
|
356
|
+
const targetType = relation.targetType();
|
|
357
|
+
const junctionType = relation.viaType?.();
|
|
358
|
+
const targetName = tableNameOf(targetType);
|
|
359
|
+
const junctionName = junctionType ? tableNameOf(junctionType) : void 0;
|
|
360
|
+
const sideOf = (ref) => {
|
|
361
|
+
const type = ref.type?.();
|
|
362
|
+
if (!type || type === targetType || tableNameOf(type) === targetName) return "target";
|
|
363
|
+
if (junctionType && (type === junctionType || tableNameOf(type) === junctionName)) return "junction";
|
|
364
|
+
throw invalid(name, `@db.rel.filter on "${name}" references a type other than the related type` + (junctionType ? " and the junction" : "") + " — only those can be referenced");
|
|
365
|
+
};
|
|
366
|
+
const target = [];
|
|
367
|
+
const junction = [];
|
|
368
|
+
const conjuncts = "$and" in relation.filter ? relation.filter.$and : [relation.filter];
|
|
369
|
+
for (const conjunct of conjuncts) {
|
|
370
|
+
const sides = /* @__PURE__ */ new Set();
|
|
371
|
+
collectSides(conjunct, (ref) => sides.add(sideOf(ref)), name);
|
|
372
|
+
if (sides.size > 1) throw invalid(name, `@db.rel.filter on "${name}" has a condition reading both the junction and the related type — split it into separate "and" conditions`);
|
|
373
|
+
const filter = translateQueryTree(conjunct, (ref) => ref.field);
|
|
374
|
+
(sides.has("junction") ? junction : target).push(filter);
|
|
375
|
+
}
|
|
376
|
+
const result = {
|
|
377
|
+
target: target.length > 0 ? andFilters(...target) : void 0,
|
|
378
|
+
junction: junction.length > 0 ? andFilters(...junction) : void 0
|
|
379
|
+
};
|
|
380
|
+
staticFilterCache.set(relation, result);
|
|
381
|
+
return result;
|
|
382
|
+
}
|
|
383
|
+
function collectSides(node, visit, name) {
|
|
384
|
+
if ("$and" in node) {
|
|
385
|
+
for (const child of node.$and) collectSides(child, visit, name);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if ("$or" in node) {
|
|
389
|
+
for (const child of node.$or) collectSides(child, visit, name);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if ("$not" in node) {
|
|
393
|
+
collectSides(node.$not, visit, name);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
visit(node.left);
|
|
397
|
+
if (isFieldRef(node.right)) throw invalid(name, `@db.rel.filter on "${name}" compares two fields — only field-to-value conditions are supported`);
|
|
398
|
+
}
|
|
399
|
+
function zip(left, right, a, b) {
|
|
400
|
+
return left.map((value, i) => ({
|
|
401
|
+
[a]: value,
|
|
402
|
+
[b]: right[i]
|
|
403
|
+
}));
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* The {@link TRelationFilterHost} of a readable: resolves a navigation
|
|
407
|
+
* field's related table (and junction) through `resolve` (the readable's
|
|
408
|
+
* table resolver), pairing foreign keys exactly like `$with` loading does
|
|
409
|
+
* (`findFKEntryForRelation` / `findRemoteFK`).
|
|
410
|
+
*/
|
|
411
|
+
function createRelationFilterHost(owner, resolve) {
|
|
412
|
+
const plans = /* @__PURE__ */ new Map();
|
|
413
|
+
const sameAdapter = (other, path, what) => {
|
|
414
|
+
if (!owner.getAdapter().sharesStoreWith(other.getAdapter())) throw notSupported(path, `Relational predicate on "${path}": the ${what} table lives in a different database or adapter`);
|
|
415
|
+
};
|
|
416
|
+
const planOf = (nav, path) => {
|
|
417
|
+
const cached = plans.get(nav);
|
|
418
|
+
if (cached) return cached;
|
|
419
|
+
const meta = owner.getMetadata();
|
|
420
|
+
const relation = meta.relations.get(nav);
|
|
421
|
+
if (!relation) throw invalid(path, `"$some" / "$none" are only valid on a navigation relation — "${nav}" is not one`);
|
|
422
|
+
const target = resolve(relation.targetType());
|
|
423
|
+
if (!target) throw notSupported(path, `Relational predicate on "${path}": the related table is not available`);
|
|
424
|
+
sameAdapter(target, path, "related");
|
|
425
|
+
let plan;
|
|
426
|
+
if (relation.direction === "to") {
|
|
427
|
+
const fk = findFKEntryForRelation(relation, meta.foreignKeys);
|
|
428
|
+
if (!fk) throw invalid(path, `Relation "${nav}" has no foreign key to filter by`);
|
|
429
|
+
plan = {
|
|
430
|
+
kind: "to",
|
|
431
|
+
relation,
|
|
432
|
+
target,
|
|
433
|
+
pairs: zip(fk.fields, fk.targetFields, "source", "target")
|
|
434
|
+
};
|
|
435
|
+
} else if (relation.direction === "from") {
|
|
436
|
+
const fk = findRemoteFK(target.getMetadata(), owner.tableName, relation.alias);
|
|
437
|
+
if (!fk) throw invalid(path, `Relation "${nav}" has no foreign key to filter by`);
|
|
438
|
+
plan = {
|
|
439
|
+
kind: "from",
|
|
440
|
+
relation,
|
|
441
|
+
target,
|
|
442
|
+
pairs: zip(fk.targetFields, fk.fields, "source", "target")
|
|
443
|
+
};
|
|
444
|
+
} else {
|
|
445
|
+
const junctionType = relation.viaType?.();
|
|
446
|
+
const junction = junctionType ? resolve(junctionType) : void 0;
|
|
447
|
+
if (!junction) throw notSupported(path, `Relational predicate on "${path}": the junction table is not available`);
|
|
448
|
+
sameAdapter(junction, path, "junction");
|
|
449
|
+
const junctionMeta = junction.getMetadata();
|
|
450
|
+
const fkToThis = findRemoteFK(junctionMeta, owner.tableName);
|
|
451
|
+
const fkToTarget = findRemoteFK(junctionMeta, tableNameOf(relation.targetType()));
|
|
452
|
+
if (!fkToThis || !fkToTarget) throw invalid(path, `Relation "${nav}" has no junction foreign keys to filter by`);
|
|
453
|
+
if (fkToThis === fkToTarget) throw invalid(path, `Relation "${nav}" is a self-referencing many-to-many — relational predicates cannot tell its two junction keys apart`);
|
|
454
|
+
plan = {
|
|
455
|
+
kind: "via",
|
|
456
|
+
relation,
|
|
457
|
+
target,
|
|
458
|
+
pairs: [],
|
|
459
|
+
junction: {
|
|
460
|
+
owner: junction,
|
|
461
|
+
toSource: zip(fkToThis.fields, fkToThis.targetFields, "junction", "source"),
|
|
462
|
+
toTarget: zip(fkToTarget.fields, fkToTarget.targetFields, "junction", "target")
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
plans.set(nav, plan);
|
|
467
|
+
return plan;
|
|
468
|
+
};
|
|
469
|
+
const tableOf = (o) => {
|
|
470
|
+
const adapter = o.getAdapter();
|
|
471
|
+
return {
|
|
472
|
+
table: adapter.resolveTableName(),
|
|
473
|
+
name: adapter.resolveTableName(false),
|
|
474
|
+
adapter
|
|
475
|
+
};
|
|
476
|
+
};
|
|
477
|
+
return {
|
|
478
|
+
guard(nav, op, inner, state) {
|
|
479
|
+
const path = state.path ? `${state.path}.${nav}` : nav;
|
|
480
|
+
const depth = state.depth + 1;
|
|
481
|
+
if (depth > 4) throw invalid("", `Relational predicates nest at most 4 levels deep`);
|
|
482
|
+
if (++state.counter.nodes > 16) throw invalid("", `At most 16 relational predicates per query`);
|
|
483
|
+
if (!require_object.isPlainObject(inner)) throw invalid(path, `"${op}" on "${path}" expects a filter object`);
|
|
484
|
+
const plan = planOf(nav, path);
|
|
485
|
+
relationStaticFilter(plan.relation, path);
|
|
486
|
+
try {
|
|
487
|
+
plan.target._guardRelationOperand(inner, {
|
|
488
|
+
...state,
|
|
489
|
+
depth,
|
|
490
|
+
path
|
|
491
|
+
});
|
|
492
|
+
} catch (error) {
|
|
493
|
+
throw prefixError(error, path);
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
resolve(nav, _op, inner, depth) {
|
|
497
|
+
if (depth > 4) throw invalid("", `Relational predicates nest at most 4 levels deep`);
|
|
498
|
+
const plan = planOf(nav, nav);
|
|
499
|
+
const ownerMeta = owner.getMetadata();
|
|
500
|
+
const targetMeta = plan.target.getMetadata();
|
|
501
|
+
const statics = relationStaticFilter(plan.relation, nav);
|
|
502
|
+
const filter = plan.target._resolveRelationOperand(andFilters(statics.target, inner), depth);
|
|
503
|
+
let junction;
|
|
504
|
+
if (plan.junction) {
|
|
505
|
+
const junctionMeta = plan.junction.owner.getMetadata();
|
|
506
|
+
junction = {
|
|
507
|
+
...tableOf(plan.junction.owner),
|
|
508
|
+
toSource: plan.junction.toSource.map((p) => ({
|
|
509
|
+
junction: junctionMeta.physicalPath(p.junction),
|
|
510
|
+
source: ownerMeta.physicalPath(p.source)
|
|
511
|
+
})),
|
|
512
|
+
toTarget: plan.junction.toTarget.map((p) => ({
|
|
513
|
+
junction: junctionMeta.physicalPath(p.junction),
|
|
514
|
+
target: targetMeta.physicalPath(p.target)
|
|
515
|
+
})),
|
|
516
|
+
...statics.junction ? { filter: plan.junction.owner._resolveRelationOperand(statics.junction, depth) } : {}
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
return new ResolvedRelationFilter({
|
|
520
|
+
kind: plan.kind,
|
|
521
|
+
nav,
|
|
522
|
+
source: tableOf(owner),
|
|
523
|
+
target: tableOf(plan.target),
|
|
524
|
+
pairs: plan.pairs.map((p) => ({
|
|
525
|
+
source: ownerMeta.physicalPath(p.source),
|
|
526
|
+
target: targetMeta.physicalPath(p.target)
|
|
527
|
+
})),
|
|
528
|
+
junction,
|
|
529
|
+
filter
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
/** Re-throws a related table's guard error with its paths under the navigation chain. */
|
|
535
|
+
function prefixError(error, path) {
|
|
536
|
+
if (!(error instanceof require_db_error.DbError)) return error;
|
|
537
|
+
if (error.errors.every((e) => !e.path)) return error;
|
|
538
|
+
if (error.errors.every((e) => e.path === path || e.path.startsWith(`${path}.`))) return error;
|
|
539
|
+
const errors = error.errors.map((e) => ({
|
|
540
|
+
path: e.path ? `${path}.${e.path}` : path,
|
|
541
|
+
message: e.message
|
|
542
|
+
}));
|
|
543
|
+
return new require_db_error.DbError(error.code, errors, `${errors[0]?.message ?? error.message} (in "${path}")`);
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Resolves every relational predicate of a (logical) filter through
|
|
547
|
+
* `meta.relationFilters` — the step every field-mapper entry point runs
|
|
548
|
+
* first. Predicates at the top of `filter` are at level `depth + 1`.
|
|
549
|
+
* Already-resolved operands pass through, so translating twice is safe.
|
|
550
|
+
* One pass: unchanged subtrees (and `filter` itself, when nothing needed
|
|
551
|
+
* resolving) are returned as they are.
|
|
552
|
+
*/
|
|
553
|
+
function resolveRelationFilterTree(filter, meta, depth) {
|
|
554
|
+
let out;
|
|
555
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
556
|
+
let next = value;
|
|
557
|
+
if (key === "$and" || key === "$or") {
|
|
558
|
+
if (Array.isArray(value)) {
|
|
559
|
+
let children;
|
|
560
|
+
for (let i = 0; i < value.length; i++) {
|
|
561
|
+
const child = value[i];
|
|
562
|
+
if (!child || typeof child !== "object") continue;
|
|
563
|
+
const resolved = resolveRelationFilterTree(child, meta, depth);
|
|
564
|
+
if (resolved === child) continue;
|
|
565
|
+
children ??= [...value];
|
|
566
|
+
children[i] = resolved;
|
|
567
|
+
}
|
|
568
|
+
if (children) next = children;
|
|
569
|
+
}
|
|
570
|
+
} else if (key === "$not") {
|
|
571
|
+
if (value && typeof value === "object") next = resolveRelationFilterTree(value, meta, depth);
|
|
572
|
+
} else if (!key.startsWith("$") && hasRelationOp(value)) next = resolvePredicateMap(key, value, meta, depth);
|
|
573
|
+
if (next === value) continue;
|
|
574
|
+
out ??= { ...filter };
|
|
575
|
+
out[key] = next;
|
|
576
|
+
}
|
|
577
|
+
if (!out) return filter;
|
|
578
|
+
noteRelationFilter(out, true);
|
|
579
|
+
return out;
|
|
580
|
+
}
|
|
581
|
+
/** One `{ $some | $none: … }` map of `key` resolved (operands already resolved pass through). */
|
|
582
|
+
function resolvePredicateMap(key, value, meta, depth) {
|
|
583
|
+
const host = meta.relationFilters;
|
|
584
|
+
if (!host) {
|
|
585
|
+
if (!meta.navFields.has(key)) throw invalid(key, `"$some" / "$none" are only valid on a navigation relation — "${key}" is not one`);
|
|
586
|
+
throw notSupported(key, `Relational predicate on "${key}" needs the table to come from a DbSpace (no table resolver)`);
|
|
587
|
+
}
|
|
588
|
+
const ops = {};
|
|
589
|
+
for (const [op, operand] of Object.entries(value)) {
|
|
590
|
+
if (!(0, _uniqu_core.isRelationOp)(op)) throw invalid(key, `Cannot mix "$some" / "$none" with "${op}" on "${key}"`);
|
|
591
|
+
ops[op] = isResolvedRelationFilter(operand) ? operand : host.resolve(key, op, operand, depth + 1);
|
|
592
|
+
}
|
|
593
|
+
return ops;
|
|
594
|
+
}
|
|
55
595
|
//#endregion
|
|
56
596
|
//#region src/shared/keys.ts
|
|
57
597
|
/** String form of a key value (an ObjectId stringifies to its hex, a bigint to its digits). */
|
|
@@ -877,6 +1417,30 @@ async function viaReplace(via, targets, parentPK, maxDepth, depth) {
|
|
|
877
1417
|
await viaLinkTargets(via, targets, parentPK, maxDepth, depth);
|
|
878
1418
|
}
|
|
879
1419
|
//#endregion
|
|
1420
|
+
Object.defineProperty(exports, "REL_FILTER_MAX_DEPTH", {
|
|
1421
|
+
enumerable: true,
|
|
1422
|
+
get: function() {
|
|
1423
|
+
return REL_FILTER_MAX_DEPTH;
|
|
1424
|
+
}
|
|
1425
|
+
});
|
|
1426
|
+
Object.defineProperty(exports, "REL_FILTER_MAX_NODES", {
|
|
1427
|
+
enumerable: true,
|
|
1428
|
+
get: function() {
|
|
1429
|
+
return REL_FILTER_MAX_NODES;
|
|
1430
|
+
}
|
|
1431
|
+
});
|
|
1432
|
+
Object.defineProperty(exports, "ResolvedRelationFilter", {
|
|
1433
|
+
enumerable: true,
|
|
1434
|
+
get: function() {
|
|
1435
|
+
return ResolvedRelationFilter;
|
|
1436
|
+
}
|
|
1437
|
+
});
|
|
1438
|
+
Object.defineProperty(exports, "andFilters", {
|
|
1439
|
+
enumerable: true,
|
|
1440
|
+
get: function() {
|
|
1441
|
+
return andFilters;
|
|
1442
|
+
}
|
|
1443
|
+
});
|
|
880
1444
|
Object.defineProperty(exports, "applyPatchNestedTo", {
|
|
881
1445
|
enumerable: true,
|
|
882
1446
|
get: function() {
|
|
@@ -943,6 +1507,36 @@ Object.defineProperty(exports, "checkDepthOverflow", {
|
|
|
943
1507
|
return checkDepthOverflow;
|
|
944
1508
|
}
|
|
945
1509
|
});
|
|
1510
|
+
Object.defineProperty(exports, "compositeKey", {
|
|
1511
|
+
enumerable: true,
|
|
1512
|
+
get: function() {
|
|
1513
|
+
return compositeKey;
|
|
1514
|
+
}
|
|
1515
|
+
});
|
|
1516
|
+
Object.defineProperty(exports, "computeOf", {
|
|
1517
|
+
enumerable: true,
|
|
1518
|
+
get: function() {
|
|
1519
|
+
return computeOf;
|
|
1520
|
+
}
|
|
1521
|
+
});
|
|
1522
|
+
Object.defineProperty(exports, "computedOperands", {
|
|
1523
|
+
enumerable: true,
|
|
1524
|
+
get: function() {
|
|
1525
|
+
return computedOperands;
|
|
1526
|
+
}
|
|
1527
|
+
});
|
|
1528
|
+
Object.defineProperty(exports, "containsRelationFilter", {
|
|
1529
|
+
enumerable: true,
|
|
1530
|
+
get: function() {
|
|
1531
|
+
return containsRelationFilter;
|
|
1532
|
+
}
|
|
1533
|
+
});
|
|
1534
|
+
Object.defineProperty(exports, "createRelationFilterHost", {
|
|
1535
|
+
enumerable: true,
|
|
1536
|
+
get: function() {
|
|
1537
|
+
return createRelationFilterHost;
|
|
1538
|
+
}
|
|
1539
|
+
});
|
|
946
1540
|
Object.defineProperty(exports, "enrichFkViolation", {
|
|
947
1541
|
enumerable: true,
|
|
948
1542
|
get: function() {
|
|
@@ -967,6 +1561,36 @@ Object.defineProperty(exports, "findRemoteFK", {
|
|
|
967
1561
|
return findRemoteFK;
|
|
968
1562
|
}
|
|
969
1563
|
});
|
|
1564
|
+
Object.defineProperty(exports, "forEachResolvedRelation", {
|
|
1565
|
+
enumerable: true,
|
|
1566
|
+
get: function() {
|
|
1567
|
+
return forEachResolvedRelation;
|
|
1568
|
+
}
|
|
1569
|
+
});
|
|
1570
|
+
Object.defineProperty(exports, "hasRelationOp", {
|
|
1571
|
+
enumerable: true,
|
|
1572
|
+
get: function() {
|
|
1573
|
+
return hasRelationOp;
|
|
1574
|
+
}
|
|
1575
|
+
});
|
|
1576
|
+
Object.defineProperty(exports, "isFieldRef", {
|
|
1577
|
+
enumerable: true,
|
|
1578
|
+
get: function() {
|
|
1579
|
+
return isFieldRef;
|
|
1580
|
+
}
|
|
1581
|
+
});
|
|
1582
|
+
Object.defineProperty(exports, "isResolvedRelationFilter", {
|
|
1583
|
+
enumerable: true,
|
|
1584
|
+
get: function() {
|
|
1585
|
+
return isResolvedRelationFilter;
|
|
1586
|
+
}
|
|
1587
|
+
});
|
|
1588
|
+
Object.defineProperty(exports, "noteRelationFilter", {
|
|
1589
|
+
enumerable: true,
|
|
1590
|
+
get: function() {
|
|
1591
|
+
return noteRelationFilter;
|
|
1592
|
+
}
|
|
1593
|
+
});
|
|
970
1594
|
Object.defineProperty(exports, "pkTupleKey", {
|
|
971
1595
|
enumerable: true,
|
|
972
1596
|
get: function() {
|
|
@@ -991,12 +1615,30 @@ Object.defineProperty(exports, "preValidateNestedFrom", {
|
|
|
991
1615
|
return preValidateNestedFrom;
|
|
992
1616
|
}
|
|
993
1617
|
});
|
|
1618
|
+
Object.defineProperty(exports, "relGuardState", {
|
|
1619
|
+
enumerable: true,
|
|
1620
|
+
get: function() {
|
|
1621
|
+
return relGuardState;
|
|
1622
|
+
}
|
|
1623
|
+
});
|
|
1624
|
+
Object.defineProperty(exports, "relationStaticFilter", {
|
|
1625
|
+
enumerable: true,
|
|
1626
|
+
get: function() {
|
|
1627
|
+
return relationStaticFilter;
|
|
1628
|
+
}
|
|
1629
|
+
});
|
|
994
1630
|
Object.defineProperty(exports, "remapDeleteFkViolation", {
|
|
995
1631
|
enumerable: true,
|
|
996
1632
|
get: function() {
|
|
997
1633
|
return remapDeleteFkViolation;
|
|
998
1634
|
}
|
|
999
1635
|
});
|
|
1636
|
+
Object.defineProperty(exports, "resolveRelationFilterTree", {
|
|
1637
|
+
enumerable: true,
|
|
1638
|
+
get: function() {
|
|
1639
|
+
return resolveRelationFilterTree;
|
|
1640
|
+
}
|
|
1641
|
+
});
|
|
1000
1642
|
Object.defineProperty(exports, "resolveRelationTargetTable", {
|
|
1001
1643
|
enumerable: true,
|
|
1002
1644
|
get: function() {
|
|
@@ -1015,15 +1657,39 @@ Object.defineProperty(exports, "sameKey", {
|
|
|
1015
1657
|
return sameKey;
|
|
1016
1658
|
}
|
|
1017
1659
|
});
|
|
1660
|
+
Object.defineProperty(exports, "slicePerGroup", {
|
|
1661
|
+
enumerable: true,
|
|
1662
|
+
get: function() {
|
|
1663
|
+
return slicePerGroup;
|
|
1664
|
+
}
|
|
1665
|
+
});
|
|
1018
1666
|
Object.defineProperty(exports, "tableNameOf", {
|
|
1019
1667
|
enumerable: true,
|
|
1020
1668
|
get: function() {
|
|
1021
1669
|
return tableNameOf;
|
|
1022
1670
|
}
|
|
1023
1671
|
});
|
|
1672
|
+
Object.defineProperty(exports, "translateQueryTree", {
|
|
1673
|
+
enumerable: true,
|
|
1674
|
+
get: function() {
|
|
1675
|
+
return translateQueryTree;
|
|
1676
|
+
}
|
|
1677
|
+
});
|
|
1024
1678
|
Object.defineProperty(exports, "validateBatch", {
|
|
1025
1679
|
enumerable: true,
|
|
1026
1680
|
get: function() {
|
|
1027
1681
|
return validateBatch;
|
|
1028
1682
|
}
|
|
1029
1683
|
});
|
|
1684
|
+
Object.defineProperty(exports, "viewExprNullable", {
|
|
1685
|
+
enumerable: true,
|
|
1686
|
+
get: function() {
|
|
1687
|
+
return viewExprNullable;
|
|
1688
|
+
}
|
|
1689
|
+
});
|
|
1690
|
+
Object.defineProperty(exports, "walkViewExpr", {
|
|
1691
|
+
enumerable: true,
|
|
1692
|
+
get: function() {
|
|
1693
|
+
return walkViewExpr;
|
|
1694
|
+
}
|
|
1695
|
+
});
|