@atscript/db-mongo 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.
@@ -0,0 +1,123 @@
1
+ import { FilterExpr, TDbCollation } from "@atscript/db";
2
+ import { Document, Filter } from "mongodb";
3
+
4
+ //#region src/lib/mongo-filter.d.ts
5
+ /**
6
+ * Translates a generic {@link FilterExpr} into a MongoDB-compatible
7
+ * {@link Filter} document.
8
+ *
9
+ * MongoDB's query language is nearly identical to the `FilterExpr` structure,
10
+ * so this is largely a structural pass-through via the `walkFilter` visitor.
11
+ * `fieldOperands` (view predicates only — `translateQueryTree` output) turns
12
+ * `{ $field: path }` operands into field-to-field `$expr` comparisons; a
13
+ * request filter never gets that reading.
14
+ *
15
+ * Predicate-free filters only: a relational predicate (`$some` / `$none`)
16
+ * throws `REL_FILTER_NOT_SUPPORTED` — render those with
17
+ * {@link buildMongoQuery} / {@link mongoFilterStages}.
18
+ */
19
+ declare function buildMongoFilter(filter: FilterExpr, {
20
+ fieldOperands,
21
+ collation
22
+ }?: {
23
+ fieldOperands?: boolean;
24
+ collation?: TMongoFieldCollation;
25
+ }): Filter<any>;
26
+ /**
27
+ * A table's per-field collation (`@db.column.collate`), keyed by PHYSICAL
28
+ * field path — `undefined` / `'binary'` compare byte-wise. See
29
+ * {@link TMongoFilterOptions.collation}.
30
+ *
31
+ * @since 0.1.147
32
+ */
33
+ type TMongoFieldCollation = (field: string) => TDbCollation | undefined;
34
+ /** Options of {@link buildMongoQuery} / {@link mongoFilterStages}. @since 0.1.147 */
35
+ interface TMongoFilterOptions {
36
+ /**
37
+ * The source table's per-field collation. A pipeline with relational
38
+ * predicates runs WITHOUT an operation-wide `collation` (it would govern
39
+ * every `$lookup` too — join keys and the related table's fields), so a
40
+ * `'nocase'` field's comparisons are rendered explicitly instead: `$eq` /
41
+ * `$ne` / `$in` / `$nin` on string values become anchored, escaped
42
+ * case-insensitive regular expressions. Range operators on a string value
43
+ * and every string comparison on a `'unicode'` field cannot be rendered
44
+ * faithfully and throw `REL_FILTER_NOT_SUPPORTED`.
45
+ *
46
+ * Omitted: the source's own fields compare byte-wise. Predicate operands
47
+ * always use the RELATED table's collation (read from its adapter).
48
+ */
49
+ collation?: TMongoFieldCollation;
50
+ }
51
+ /**
52
+ * The per-field collation of a table, read from its (Mongo) adapter's
53
+ * `fieldCollation` — what a relational-predicate pipeline renders `'nocase'`
54
+ * fields with ({@link TMongoFilterOptions.collation}).
55
+ */
56
+ declare function collationOfAdapter(adapter: unknown): TMongoFieldCollation | undefined;
57
+ /**
58
+ * Prefix of the temporary fields predicate lookups write (`<prefix><n>`) —
59
+ * long and specific so it cannot plausibly collide with a stored field; the
60
+ * fields are dropped with `$unset` before any row leaves the pipeline.
61
+ *
62
+ * @since 0.1.147
63
+ */
64
+ declare const REL_FILTER_TEMP_PREFIX = "__atscript_rf_";
65
+ /**
66
+ * A filter rendered for an aggregation pipeline — what
67
+ * {@link buildMongoQuery} returns. Stage order: `$match: pre` (when set),
68
+ * the `lookups`, `$match: match` (when non-empty), `$unset: temp`
69
+ * ({@link mongoFilterStages} assembles them).
70
+ *
71
+ * @since 0.1.147
72
+ */
73
+ interface TMongoFilterPlan {
74
+ /** The predicate-free top-level conjuncts — matched BEFORE the lookups, so they run only for surviving documents. */
75
+ pre?: Filter<any>;
76
+ /** One `$lookup` per relational predicate, each writing a temporary array field (at most one element). */
77
+ lookups: Document[];
78
+ /** The rest of the filter, predicates read as `{ <temp>: { $ne: [] } }` (`$some`) / `{ <temp>: { $size: 0 } }` (`$none`). */
79
+ match: Filter<any>;
80
+ /** The temporary fields the lookups add ({@link REL_FILTER_TEMP_PREFIX}`<n>`, dropped with `$unset`). */
81
+ temp: string[];
82
+ }
83
+ /**
84
+ * Renders a translated filter that may hold relational predicates
85
+ * (`{ nav: { $some | $none: ResolvedRelationFilter } }`) for an aggregation
86
+ * pipeline. Each predicate becomes a correlated `$lookup` into the related
87
+ * collection (`let` = the source key fields; a bare `$expr` `$eq` per key
88
+ * part — index-friendly — then a `{ <key>: { $ne: null } }` guard; the
89
+ * inner filter — nested predicates as lookups inside it — then
90
+ * `$limit: 1`); `via` looks up the junction, and inside it the target. The
91
+ * predicate itself reads the lookup's temporary array field
92
+ * ({@link REL_FILTER_TEMP_PREFIX}`<n>`). Top-level conjuncts without
93
+ * predicates go to `pre` so the lookups run only for documents that survive
94
+ * them.
95
+ *
96
+ * Run the pipeline WITHOUT an operation-wide `collation`: it would apply to
97
+ * the join keys and the related tables' fields too. Each related table's
98
+ * `'nocase'` fields are compared case-insensitively explicitly (see
99
+ * {@link TMongoFilterOptions.collation}, which does the same for the source).
100
+ *
101
+ * A predicate-free filter yields `{ pre, lookups: [], match: {}, temp: [] }`.
102
+ *
103
+ * @since 0.1.147
104
+ */
105
+ declare function buildMongoQuery(filter: FilterExpr | undefined, options?: TMongoFilterOptions): TMongoFilterPlan;
106
+ /**
107
+ * The pipeline stages that apply `filter`: `[{ $match }]` for a
108
+ * predicate-free filter (exactly what the reads always emitted — `options`
109
+ * is ignored then), else `$match(pre)` → predicate `$lookup`s → `$match` →
110
+ * `$unset` of the temporary fields ({@link buildMongoQuery}).
111
+ *
112
+ * @since 0.1.147
113
+ */
114
+ declare function mongoFilterStages(filter: FilterExpr | undefined, options?: TMongoFilterOptions): Document[];
115
+ /**
116
+ * The stages of a {@link TMongoFilterPlan}, in order: `$match(pre)`, the
117
+ * lookups, `$match(match)`, `$unset` of the temporary fields.
118
+ *
119
+ * @since 0.1.147
120
+ */
121
+ declare function planStages(plan: TMongoFilterPlan): Document[];
122
+ //#endregion
123
+ export { buildMongoFilter as a, mongoFilterStages as c, TMongoFilterPlan as i, planStages as l, TMongoFieldCollation as n, buildMongoQuery as o, TMongoFilterOptions as r, collationOfAdapter as s, REL_FILTER_TEMP_PREFIX as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/db-mongo",
3
- "version": "0.1.146",
3
+ "version": "0.1.147",
4
4
  "description": "Mongodb plugin for atscript.",
5
5
  "keywords": [
6
6
  "atscript",
@@ -46,19 +46,19 @@
46
46
  "access": "public"
47
47
  },
48
48
  "devDependencies": {
49
- "@atscript/core": "^0.1.98",
50
- "@atscript/typescript": "^0.1.98",
51
- "@uniqu/core": "^0.1.11",
49
+ "@atscript/core": "^0.1.99",
50
+ "@atscript/typescript": "^0.1.99",
51
+ "@uniqu/core": "^0.1.12",
52
52
  "mongodb": "^6.17.0",
53
53
  "mongodb-memory-server-core": "^10.0.0",
54
- "unplugin-atscript": "^0.1.98"
54
+ "unplugin-atscript": "^0.1.99"
55
55
  },
56
56
  "peerDependencies": {
57
- "@atscript/core": "^0.1.98",
58
- "@atscript/typescript": "^0.1.98",
59
- "@uniqu/core": "^0.1.11",
57
+ "@atscript/core": "^0.1.99",
58
+ "@atscript/typescript": "^0.1.99",
59
+ "@uniqu/core": "^0.1.12",
60
60
  "mongodb": "^6.17.0",
61
- "@atscript/db": "^0.1.146"
61
+ "@atscript/db": "^0.1.147"
62
62
  },
63
63
  "scripts": {
64
64
  "postinstall": "asc -f dts",
@@ -1,265 +0,0 @@
1
- let _atscript_db = require("@atscript/db");
2
- let _atscript_db_agg = require("@atscript/db/agg");
3
- //#region src/lib/mongo-view-expr.ts
4
- /**
5
- * `x IS NOT NULL` in SQL terms — true when `x` is neither null nor missing.
6
- * Aggregation order puts a missing value below null and every other value
7
- * above it, so one comparison covers both (`{ $ne: [x, null] }` alone would
8
- * be true for a MISSING field).
9
- * @since 0.1.136
10
- */
11
- function notNullExpr(x) {
12
- return { $gt: [x, null] };
13
- }
14
- /** `x IS NULL` in SQL terms — true when `x` is null or missing (see {@link notNullExpr}). @since 0.1.136 */
15
- function isNullExpr(x) {
16
- return { $lte: [x, null] };
17
- }
18
- /**
19
- * `x`, with a missing value read as null — for a projected column (a missing
20
- * key would drop it from the row) and a `$group` key (missing and null would
21
- * form two groups). Only where a value may be missing: the wrapper hides the
22
- * path from `$match` / `$sort` pushdown.
23
- * @since 0.1.136
24
- */
25
- function orNull(x) {
26
- return { $ifNull: [x, null] };
27
- }
28
- /** A literal inside `$expr` — `$`-prefixed strings would otherwise read as field paths. */
29
- function literal(value) {
30
- return typeof value === "string" && value.startsWith("$") ? { $literal: value } : value;
31
- }
32
- const COMPARISONS = {
33
- $eq: "$eq",
34
- $ne: "$ne",
35
- $gt: "$gt",
36
- $gte: "$gte",
37
- $lt: "$lt",
38
- $lte: "$lte"
39
- };
40
- /** `cond` AND-guarded so that every field operand is non-null (SQL: NULL operand → not true). */
41
- function guarded(operands, cond) {
42
- return { $and: [...operands.map((o) => notNullExpr(o)), cond] };
43
- }
44
- /**
45
- * Field-to-field comparison `x <op> y` (`$eq` … `$lte`): both operands must
46
- * be non-null (SQL `NULL = NULL` is UNKNOWN) — view predicates and
47
- * `buildMongoFilter`'s field operands.
48
- * @since 0.1.137
49
- */
50
- function fieldCompareExpr(op, x, y) {
51
- return guarded([x, y], { [op]: [x, y] });
52
- }
53
- /**
54
- * Translates a view predicate (join condition, conditional-aggregate filter)
55
- * to an aggregation expression for `$match: { $expr }` / `$cond`, matching
56
- * SQL's three-valued logic wherever a NULL operand makes SQL's comparison
57
- * UNKNOWN (treated as false):
58
- *
59
- * - `<`, `<=`, `>`, `>=`, `!= <literal>`, field `=` field, field `!=` field
60
- * and `not in` are guarded with "every field operand is not null";
61
- * - `= null` / `not exists` → null-or-missing; `!= null` / `exists` → neither;
62
- * - `= <literal>` and `in (…)` compare directly (a null operand never equals
63
- * a non-null literal); an empty `not in` is true;
64
- * - `and` / `or` / `not` map directly — so `not (x > 1)` is TRUE for a null
65
- * `x` here while SQL yields UNKNOWN (documented divergence);
66
- * - `matches` is rejected (`$regexMatch` needs MongoDB 4.2).
67
- * @since 0.1.136
68
- */
69
- function queryNodeToExpr(node, pathOf) {
70
- if ("$and" in node) return { $and: node.$and.map((n) => queryNodeToExpr(n, pathOf)) };
71
- if ("$or" in node) return { $or: node.$or.map((n) => queryNodeToExpr(n, pathOf)) };
72
- if ("$not" in node) return { $not: [queryNodeToExpr(node.$not, pathOf)] };
73
- const comp = node;
74
- const x = pathOf(comp.left);
75
- switch (comp.op) {
76
- case "$exists": return comp.right === false ? isNullExpr(x) : notNullExpr(x);
77
- case "$in": return { $in: [x, (Array.isArray(comp.right) ? comp.right : [comp.right]).map((v) => literal(v))] };
78
- case "$nin": {
79
- const values = Array.isArray(comp.right) ? comp.right : [comp.right];
80
- if (values.length === 0) return { $literal: true };
81
- return guarded([x], { $not: [{ $in: [x, values.map((v) => literal(v))] }] });
82
- }
83
- case "$regex": throw new Error("matches is not supported in view predicates");
84
- default:
85
- }
86
- const op = COMPARISONS[comp.op];
87
- if (!op) throw new Error(`Operator "${comp.op}" is not supported in view predicates`);
88
- if ((0, _atscript_db.isFieldRef)(comp.right)) return fieldCompareExpr(op, x, pathOf(comp.right));
89
- if (comp.right === null || comp.right === void 0) {
90
- if (op === "$eq") return isNullExpr(x);
91
- if (op === "$ne") return notNullExpr(x);
92
- return { $literal: false };
93
- }
94
- const value = literal(comp.right);
95
- if (op === "$eq") return { $eq: [x, value] };
96
- return guarded([x], { [op]: [x, value] });
97
- }
98
- //#endregion
99
- //#region src/lib/mongo-filter.ts
100
- const EMPTY = {};
101
- function parseRegexString(value) {
102
- if (value instanceof RegExp) return {
103
- pattern: value.source,
104
- flags: value.flags
105
- };
106
- const str = String(value);
107
- const match = str.match(/^\/(.+)\/([gimsuy]*)$/);
108
- if (match) return {
109
- pattern: match[1],
110
- flags: match[2]
111
- };
112
- return {
113
- pattern: str,
114
- flags: ""
115
- };
116
- }
117
- /**
118
- * Earth radius in meters used by MongoDB's `$centerSphere` radians conversion
119
- * (Mongo documents dividing by 6378.1 km).
120
- */
121
- const EARTH_RADIUS_M = 6378100;
122
- const mongoVisitor = {
123
- comparison(field, op, value) {
124
- if (op === "$eq") return { [field]: value };
125
- if (op === "$exists") return value ? { [field]: { $ne: null } } : { [field]: null };
126
- if (op === "$regex") {
127
- const { pattern, flags } = parseRegexString(value);
128
- return flags ? { [field]: {
129
- $regex: pattern,
130
- $options: flags
131
- } } : { [field]: { $regex: pattern } };
132
- }
133
- if (op === "$geoWithin") {
134
- const { center, radius } = value;
135
- return { [field]: { $geoWithin: { $centerSphere: [center, radius / EARTH_RADIUS_M] } } };
136
- }
137
- return { [field]: { [op]: value } };
138
- },
139
- and(children) {
140
- if (children.length === 0) return EMPTY;
141
- if (children.length === 1) return children[0];
142
- return { $and: children };
143
- },
144
- or(children) {
145
- if (children.length === 0) return { _impossible: true };
146
- if (children.length === 1) return children[0];
147
- return { $or: children };
148
- },
149
- not(child) {
150
- return { $nor: [child] };
151
- }
152
- };
153
- /** A `{ $field: path }` comparison operand (a field-to-field comparison). */
154
- function isFieldOperand(value) {
155
- return value !== null && typeof value === "object" && typeof value.$field === "string";
156
- }
157
- /**
158
- * {@link mongoVisitor} plus field-to-field comparisons (`{ $field }` operands →
159
- * `$expr`), null-guarded like SQL ({@link fieldCompareExpr}).
160
- */
161
- const fieldOperandVisitor = {
162
- ...mongoVisitor,
163
- comparison(field, op, value) {
164
- return isFieldOperand(value) ? { $expr: fieldCompareExpr(op, `$${field}`, `$${value.$field}`) } : mongoVisitor.comparison(field, op, value);
165
- }
166
- };
167
- /**
168
- * Translates a generic {@link FilterExpr} into a MongoDB-compatible
169
- * {@link Filter} document.
170
- *
171
- * MongoDB's query language is nearly identical to the `FilterExpr` structure,
172
- * so this is largely a structural pass-through via the `walkFilter` visitor.
173
- * `fieldOperands` (view predicates only — `translateQueryTree` output) turns
174
- * `{ $field: path }` operands into field-to-field `$expr` comparisons; a
175
- * request filter never gets that reading.
176
- */
177
- function buildMongoFilter(filter, { fieldOperands = false } = {}) {
178
- if (!filter || Object.keys(filter).length === 0) return EMPTY;
179
- return (0, _atscript_db.walkFilter)(filter, fieldOperands ? fieldOperandVisitor : mongoVisitor) ?? EMPTY;
180
- }
181
- //#endregion
182
- //#region src/lib/mongo-accumulator.ts
183
- /**
184
- * The `$group` accumulator of one aggregate — shared by grouped queries
185
- * (`agg.ts`) and managed views (`mongo-view-pipeline.ts`), so both count and
186
- * sum alike.
187
- *
188
- * - `count(*)` → `{ $sum: 1 }`; `count(f)` counts values that are neither
189
- * null nor missing (SQL `COUNT(f)`);
190
- * - `sum` / `avg` / `min` / `max` → `$sum` / `$avg` / `$min` / `$max` (all
191
- * skip null / missing values);
192
- * - `countDistinct(f)` → `{ $addToSet: { $ifNull: [f, "$$REMOVE"] } }`, a
193
- * SET of the non-null values (`$$REMOVE` adds nothing) the caller turns
194
- * into its size with {@link distinctCountExpr}.
195
- *
196
- * `where` (a conditional aggregate's row predicate, an aggregation
197
- * expression) swaps the source for `{ $cond: [where, src, null] }` — the
198
- * rejected rows contribute a null, which every accumulator skips — and makes
199
- * the counts `{ $sum: { $cond: [where (and not null), 1, 0] } }`.
200
- *
201
- * @param fn - The aggregate function (re-asserted: `INVALID_QUERY` when unknown).
202
- * @param src - The source operand (`"$path"`), or `"*"` for `count(*)`.
203
- * @param where - Row predicate of a conditional aggregate.
204
- * @param path - Error path of the re-assertion.
205
- */
206
- function buildAccumulator(fn, src, where, path) {
207
- (0, _atscript_db_agg.assertAggregateFn)(fn, path);
208
- if (fn === "count") {
209
- if (src === "*") return { $sum: where ? { $cond: [
210
- where,
211
- 1,
212
- 0
213
- ] } : 1 };
214
- return { $sum: { $cond: [
215
- where ? { $and: [where, notNullExpr(src)] } : notNullExpr(src),
216
- 1,
217
- 0
218
- ] } };
219
- }
220
- const value = where ? { $cond: [
221
- where,
222
- src,
223
- null
224
- ] } : src;
225
- return fn === "countDistinct" ? { $addToSet: { $ifNull: [value, "$$REMOVE"] } } : { [`$${fn}`]: value };
226
- }
227
- /**
228
- * The size of a `countDistinct` set (`"$alias"`) — the value the accumulator
229
- * stands for, projected right after `$group` so later stages (`$having`,
230
- * `$sort`) see a number.
231
- */
232
- function distinctCountExpr(set) {
233
- return { $size: set };
234
- }
235
- //#endregion
236
- Object.defineProperty(exports, "buildAccumulator", {
237
- enumerable: true,
238
- get: function() {
239
- return buildAccumulator;
240
- }
241
- });
242
- Object.defineProperty(exports, "buildMongoFilter", {
243
- enumerable: true,
244
- get: function() {
245
- return buildMongoFilter;
246
- }
247
- });
248
- Object.defineProperty(exports, "distinctCountExpr", {
249
- enumerable: true,
250
- get: function() {
251
- return distinctCountExpr;
252
- }
253
- });
254
- Object.defineProperty(exports, "orNull", {
255
- enumerable: true,
256
- get: function() {
257
- return orNull;
258
- }
259
- });
260
- Object.defineProperty(exports, "queryNodeToExpr", {
261
- enumerable: true,
262
- get: function() {
263
- return queryNodeToExpr;
264
- }
265
- });
@@ -1,236 +0,0 @@
1
- import { isFieldRef, walkFilter } from "@atscript/db";
2
- import { assertAggregateFn } from "@atscript/db/agg";
3
- //#region src/lib/mongo-view-expr.ts
4
- /**
5
- * `x IS NOT NULL` in SQL terms — true when `x` is neither null nor missing.
6
- * Aggregation order puts a missing value below null and every other value
7
- * above it, so one comparison covers both (`{ $ne: [x, null] }` alone would
8
- * be true for a MISSING field).
9
- * @since 0.1.136
10
- */
11
- function notNullExpr(x) {
12
- return { $gt: [x, null] };
13
- }
14
- /** `x IS NULL` in SQL terms — true when `x` is null or missing (see {@link notNullExpr}). @since 0.1.136 */
15
- function isNullExpr(x) {
16
- return { $lte: [x, null] };
17
- }
18
- /**
19
- * `x`, with a missing value read as null — for a projected column (a missing
20
- * key would drop it from the row) and a `$group` key (missing and null would
21
- * form two groups). Only where a value may be missing: the wrapper hides the
22
- * path from `$match` / `$sort` pushdown.
23
- * @since 0.1.136
24
- */
25
- function orNull(x) {
26
- return { $ifNull: [x, null] };
27
- }
28
- /** A literal inside `$expr` — `$`-prefixed strings would otherwise read as field paths. */
29
- function literal(value) {
30
- return typeof value === "string" && value.startsWith("$") ? { $literal: value } : value;
31
- }
32
- const COMPARISONS = {
33
- $eq: "$eq",
34
- $ne: "$ne",
35
- $gt: "$gt",
36
- $gte: "$gte",
37
- $lt: "$lt",
38
- $lte: "$lte"
39
- };
40
- /** `cond` AND-guarded so that every field operand is non-null (SQL: NULL operand → not true). */
41
- function guarded(operands, cond) {
42
- return { $and: [...operands.map((o) => notNullExpr(o)), cond] };
43
- }
44
- /**
45
- * Field-to-field comparison `x <op> y` (`$eq` … `$lte`): both operands must
46
- * be non-null (SQL `NULL = NULL` is UNKNOWN) — view predicates and
47
- * `buildMongoFilter`'s field operands.
48
- * @since 0.1.137
49
- */
50
- function fieldCompareExpr(op, x, y) {
51
- return guarded([x, y], { [op]: [x, y] });
52
- }
53
- /**
54
- * Translates a view predicate (join condition, conditional-aggregate filter)
55
- * to an aggregation expression for `$match: { $expr }` / `$cond`, matching
56
- * SQL's three-valued logic wherever a NULL operand makes SQL's comparison
57
- * UNKNOWN (treated as false):
58
- *
59
- * - `<`, `<=`, `>`, `>=`, `!= <literal>`, field `=` field, field `!=` field
60
- * and `not in` are guarded with "every field operand is not null";
61
- * - `= null` / `not exists` → null-or-missing; `!= null` / `exists` → neither;
62
- * - `= <literal>` and `in (…)` compare directly (a null operand never equals
63
- * a non-null literal); an empty `not in` is true;
64
- * - `and` / `or` / `not` map directly — so `not (x > 1)` is TRUE for a null
65
- * `x` here while SQL yields UNKNOWN (documented divergence);
66
- * - `matches` is rejected (`$regexMatch` needs MongoDB 4.2).
67
- * @since 0.1.136
68
- */
69
- function queryNodeToExpr(node, pathOf) {
70
- if ("$and" in node) return { $and: node.$and.map((n) => queryNodeToExpr(n, pathOf)) };
71
- if ("$or" in node) return { $or: node.$or.map((n) => queryNodeToExpr(n, pathOf)) };
72
- if ("$not" in node) return { $not: [queryNodeToExpr(node.$not, pathOf)] };
73
- const comp = node;
74
- const x = pathOf(comp.left);
75
- switch (comp.op) {
76
- case "$exists": return comp.right === false ? isNullExpr(x) : notNullExpr(x);
77
- case "$in": return { $in: [x, (Array.isArray(comp.right) ? comp.right : [comp.right]).map((v) => literal(v))] };
78
- case "$nin": {
79
- const values = Array.isArray(comp.right) ? comp.right : [comp.right];
80
- if (values.length === 0) return { $literal: true };
81
- return guarded([x], { $not: [{ $in: [x, values.map((v) => literal(v))] }] });
82
- }
83
- case "$regex": throw new Error("matches is not supported in view predicates");
84
- default:
85
- }
86
- const op = COMPARISONS[comp.op];
87
- if (!op) throw new Error(`Operator "${comp.op}" is not supported in view predicates`);
88
- if (isFieldRef(comp.right)) return fieldCompareExpr(op, x, pathOf(comp.right));
89
- if (comp.right === null || comp.right === void 0) {
90
- if (op === "$eq") return isNullExpr(x);
91
- if (op === "$ne") return notNullExpr(x);
92
- return { $literal: false };
93
- }
94
- const value = literal(comp.right);
95
- if (op === "$eq") return { $eq: [x, value] };
96
- return guarded([x], { [op]: [x, value] });
97
- }
98
- //#endregion
99
- //#region src/lib/mongo-filter.ts
100
- const EMPTY = {};
101
- function parseRegexString(value) {
102
- if (value instanceof RegExp) return {
103
- pattern: value.source,
104
- flags: value.flags
105
- };
106
- const str = String(value);
107
- const match = str.match(/^\/(.+)\/([gimsuy]*)$/);
108
- if (match) return {
109
- pattern: match[1],
110
- flags: match[2]
111
- };
112
- return {
113
- pattern: str,
114
- flags: ""
115
- };
116
- }
117
- /**
118
- * Earth radius in meters used by MongoDB's `$centerSphere` radians conversion
119
- * (Mongo documents dividing by 6378.1 km).
120
- */
121
- const EARTH_RADIUS_M = 6378100;
122
- const mongoVisitor = {
123
- comparison(field, op, value) {
124
- if (op === "$eq") return { [field]: value };
125
- if (op === "$exists") return value ? { [field]: { $ne: null } } : { [field]: null };
126
- if (op === "$regex") {
127
- const { pattern, flags } = parseRegexString(value);
128
- return flags ? { [field]: {
129
- $regex: pattern,
130
- $options: flags
131
- } } : { [field]: { $regex: pattern } };
132
- }
133
- if (op === "$geoWithin") {
134
- const { center, radius } = value;
135
- return { [field]: { $geoWithin: { $centerSphere: [center, radius / EARTH_RADIUS_M] } } };
136
- }
137
- return { [field]: { [op]: value } };
138
- },
139
- and(children) {
140
- if (children.length === 0) return EMPTY;
141
- if (children.length === 1) return children[0];
142
- return { $and: children };
143
- },
144
- or(children) {
145
- if (children.length === 0) return { _impossible: true };
146
- if (children.length === 1) return children[0];
147
- return { $or: children };
148
- },
149
- not(child) {
150
- return { $nor: [child] };
151
- }
152
- };
153
- /** A `{ $field: path }` comparison operand (a field-to-field comparison). */
154
- function isFieldOperand(value) {
155
- return value !== null && typeof value === "object" && typeof value.$field === "string";
156
- }
157
- /**
158
- * {@link mongoVisitor} plus field-to-field comparisons (`{ $field }` operands →
159
- * `$expr`), null-guarded like SQL ({@link fieldCompareExpr}).
160
- */
161
- const fieldOperandVisitor = {
162
- ...mongoVisitor,
163
- comparison(field, op, value) {
164
- return isFieldOperand(value) ? { $expr: fieldCompareExpr(op, `$${field}`, `$${value.$field}`) } : mongoVisitor.comparison(field, op, value);
165
- }
166
- };
167
- /**
168
- * Translates a generic {@link FilterExpr} into a MongoDB-compatible
169
- * {@link Filter} document.
170
- *
171
- * MongoDB's query language is nearly identical to the `FilterExpr` structure,
172
- * so this is largely a structural pass-through via the `walkFilter` visitor.
173
- * `fieldOperands` (view predicates only — `translateQueryTree` output) turns
174
- * `{ $field: path }` operands into field-to-field `$expr` comparisons; a
175
- * request filter never gets that reading.
176
- */
177
- function buildMongoFilter(filter, { fieldOperands = false } = {}) {
178
- if (!filter || Object.keys(filter).length === 0) return EMPTY;
179
- return walkFilter(filter, fieldOperands ? fieldOperandVisitor : mongoVisitor) ?? EMPTY;
180
- }
181
- //#endregion
182
- //#region src/lib/mongo-accumulator.ts
183
- /**
184
- * The `$group` accumulator of one aggregate — shared by grouped queries
185
- * (`agg.ts`) and managed views (`mongo-view-pipeline.ts`), so both count and
186
- * sum alike.
187
- *
188
- * - `count(*)` → `{ $sum: 1 }`; `count(f)` counts values that are neither
189
- * null nor missing (SQL `COUNT(f)`);
190
- * - `sum` / `avg` / `min` / `max` → `$sum` / `$avg` / `$min` / `$max` (all
191
- * skip null / missing values);
192
- * - `countDistinct(f)` → `{ $addToSet: { $ifNull: [f, "$$REMOVE"] } }`, a
193
- * SET of the non-null values (`$$REMOVE` adds nothing) the caller turns
194
- * into its size with {@link distinctCountExpr}.
195
- *
196
- * `where` (a conditional aggregate's row predicate, an aggregation
197
- * expression) swaps the source for `{ $cond: [where, src, null] }` — the
198
- * rejected rows contribute a null, which every accumulator skips — and makes
199
- * the counts `{ $sum: { $cond: [where (and not null), 1, 0] } }`.
200
- *
201
- * @param fn - The aggregate function (re-asserted: `INVALID_QUERY` when unknown).
202
- * @param src - The source operand (`"$path"`), or `"*"` for `count(*)`.
203
- * @param where - Row predicate of a conditional aggregate.
204
- * @param path - Error path of the re-assertion.
205
- */
206
- function buildAccumulator(fn, src, where, path) {
207
- assertAggregateFn(fn, path);
208
- if (fn === "count") {
209
- if (src === "*") return { $sum: where ? { $cond: [
210
- where,
211
- 1,
212
- 0
213
- ] } : 1 };
214
- return { $sum: { $cond: [
215
- where ? { $and: [where, notNullExpr(src)] } : notNullExpr(src),
216
- 1,
217
- 0
218
- ] } };
219
- }
220
- const value = where ? { $cond: [
221
- where,
222
- src,
223
- null
224
- ] } : src;
225
- return fn === "countDistinct" ? { $addToSet: { $ifNull: [value, "$$REMOVE"] } } : { [`$${fn}`]: value };
226
- }
227
- /**
228
- * The size of a `countDistinct` set (`"$alias"`) — the value the accumulator
229
- * stands for, projected right after `$group` so later stages (`$having`,
230
- * `$sort`) see a number.
231
- */
232
- function distinctCountExpr(set) {
233
- return { $size: set };
234
- }
235
- //#endregion
236
- export { queryNodeToExpr as a, orNull as i, distinctCountExpr as n, buildMongoFilter as r, buildAccumulator as t };