@atscript/db 0.1.145 → 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.
Files changed (45) hide show
  1. package/dist/agg.cjs +1 -1
  2. package/dist/agg.d.cts +1 -1
  3. package/dist/agg.d.mts +1 -1
  4. package/dist/agg.mjs +1 -1
  5. package/dist/{aggregate-fns-CfsveE1w.mjs → aggregate-fns-C0BymEqa.mjs} +3 -1
  6. package/dist/{aggregate-fns-CGBv3E8S.cjs → aggregate-fns-CtsRbZn9.cjs} +8 -0
  7. package/dist/{buckets-DYFu0eZ8.d.cts → buckets-CjdPipCC.d.cts} +522 -17
  8. package/dist/{buckets-DRycmhOW.d.mts → buckets-DYopfd2Y.d.mts} +522 -17
  9. package/dist/{column-diff-e2oHc71_.mjs → column-diff-CO06iQvI.mjs} +862 -115
  10. package/dist/{column-diff-D_Kyuh0S.cjs → column-diff-Dt7EDJsA.cjs} +872 -113
  11. package/dist/{column-diff-Q9UmWn5x.d.mts → fk-diff-BtOmL4OW.d.mts} +48 -2
  12. package/dist/{column-diff-w-Mym_3w.d.cts → fk-diff-DLqwVkKG.d.cts} +48 -2
  13. package/dist/index.cjs +28 -33
  14. package/dist/index.d.cts +40 -15
  15. package/dist/index.d.mts +40 -15
  16. package/dist/index.mjs +5 -33
  17. package/dist/{nested-writer-CnOOAehr.mjs → nested-writer-D35UsDBT.mjs} +542 -2
  18. package/dist/{nested-writer-xfQwxplL.cjs → nested-writer-SqtYJHF0.cjs} +666 -0
  19. package/dist/plugin.cjs +266 -5
  20. package/dist/plugin.mjs +267 -6
  21. package/dist/rel.cjs +2 -2
  22. package/dist/rel.d.cts +2 -2
  23. package/dist/rel.d.mts +2 -2
  24. package/dist/rel.mjs +2 -2
  25. package/dist/{relation-helpers-B-0NRKat.d.mts → relation-helpers-C2bLtE-t.d.mts} +1 -1
  26. package/dist/{relation-helpers-BOMm_HUI.d.cts → relation-helpers-DH3LvwLV.d.cts} +1 -1
  27. package/dist/relation-loader-BFhVuJ-B.cjs +369 -0
  28. package/dist/relation-loader-C0qE8xgR.mjs +370 -0
  29. package/dist/shared.cjs +1 -1
  30. package/dist/shared.d.cts +4 -3
  31. package/dist/shared.d.mts +4 -3
  32. package/dist/shared.mjs +1 -1
  33. package/dist/sync.cjs +10 -6
  34. package/dist/sync.d.cts +2 -25
  35. package/dist/sync.d.mts +2 -25
  36. package/dist/sync.mjs +10 -6
  37. package/dist/{validation-utils-DOsB4e6G.cjs → validation-utils-Da2GjobR.cjs} +35 -24
  38. package/dist/{validation-utils-CMR4fe2M.mjs → validation-utils-Dq0uZ7ef.mjs} +35 -24
  39. package/dist/{validator-Bw6ks9Hy.d.mts → validator-CMnTI6r7.d.cts} +9 -6
  40. package/dist/{validator-Bw6ks9Hy.d.cts → validator-CMnTI6r7.d.mts} +9 -6
  41. package/dist/validator.d.cts +1 -1
  42. package/dist/validator.d.mts +1 -1
  43. package/package.json +8 -8
  44. package/dist/relation-loader-CBPY6kM7.cjs +0 -461
  45. 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
+ });