@atscript/db-mongo 0.1.134 → 0.1.136
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 +7 -26
- package/dist/agg.mjs +5 -24
- package/dist/index.cjs +194 -143
- package/dist/index.d.cts +11 -2
- package/dist/index.d.mts +11 -2
- package/dist/index.mjs +181 -130
- package/dist/mongo-accumulator-C8J36t07.cjs +256 -0
- package/dist/mongo-accumulator-Dh6eEkyR.mjs +227 -0
- package/package.json +4 -4
- package/dist/mongo-filter-Bf5rWV4P.cjs +0 -73
- package/dist/mongo-filter-CtyITmZM.mjs +0 -68
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { isFieldRef, walkFilter } from "@atscript/db";
|
|
2
|
+
import { assertAggregateFn } from "@atscript/db/agg";
|
|
3
|
+
//#region src/lib/mongo-filter.ts
|
|
4
|
+
const EMPTY = {};
|
|
5
|
+
function parseRegexString(value) {
|
|
6
|
+
if (value instanceof RegExp) return {
|
|
7
|
+
pattern: value.source,
|
|
8
|
+
flags: value.flags
|
|
9
|
+
};
|
|
10
|
+
const str = String(value);
|
|
11
|
+
const match = str.match(/^\/(.+)\/([gimsuy]*)$/);
|
|
12
|
+
if (match) return {
|
|
13
|
+
pattern: match[1],
|
|
14
|
+
flags: match[2]
|
|
15
|
+
};
|
|
16
|
+
return {
|
|
17
|
+
pattern: str,
|
|
18
|
+
flags: ""
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Earth radius in meters used by MongoDB's `$centerSphere` radians conversion
|
|
23
|
+
* (Mongo documents dividing by 6378.1 km).
|
|
24
|
+
*/
|
|
25
|
+
const EARTH_RADIUS_M = 6378100;
|
|
26
|
+
const mongoVisitor = {
|
|
27
|
+
comparison(field, op, value) {
|
|
28
|
+
if (op === "$eq") return { [field]: value };
|
|
29
|
+
if (op === "$exists") return value ? { [field]: { $ne: null } } : { [field]: null };
|
|
30
|
+
if (op === "$regex") {
|
|
31
|
+
const { pattern, flags } = parseRegexString(value);
|
|
32
|
+
return flags ? { [field]: {
|
|
33
|
+
$regex: pattern,
|
|
34
|
+
$options: flags
|
|
35
|
+
} } : { [field]: { $regex: pattern } };
|
|
36
|
+
}
|
|
37
|
+
if (op === "$geoWithin") {
|
|
38
|
+
const { center, radius } = value;
|
|
39
|
+
return { [field]: { $geoWithin: { $centerSphere: [center, radius / EARTH_RADIUS_M] } } };
|
|
40
|
+
}
|
|
41
|
+
return { [field]: { [op]: value } };
|
|
42
|
+
},
|
|
43
|
+
and(children) {
|
|
44
|
+
if (children.length === 0) return EMPTY;
|
|
45
|
+
if (children.length === 1) return children[0];
|
|
46
|
+
return { $and: children };
|
|
47
|
+
},
|
|
48
|
+
or(children) {
|
|
49
|
+
if (children.length === 0) return { _impossible: true };
|
|
50
|
+
if (children.length === 1) return children[0];
|
|
51
|
+
return { $or: children };
|
|
52
|
+
},
|
|
53
|
+
not(child) {
|
|
54
|
+
return { $nor: [child] };
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
/** A `{ $field: path }` comparison operand (a field-to-field comparison). */
|
|
58
|
+
function isFieldOperand(value) {
|
|
59
|
+
return value !== null && typeof value === "object" && typeof value.$field === "string";
|
|
60
|
+
}
|
|
61
|
+
/** {@link mongoVisitor} plus field-to-field comparisons (`{ $field }` operands → `$expr`). */
|
|
62
|
+
const fieldOperandVisitor = {
|
|
63
|
+
...mongoVisitor,
|
|
64
|
+
comparison(field, op, value) {
|
|
65
|
+
return isFieldOperand(value) ? { $expr: { [op]: [`$${field}`, `$${value.$field}`] } } : mongoVisitor.comparison(field, op, value);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Translates a generic {@link FilterExpr} into a MongoDB-compatible
|
|
70
|
+
* {@link Filter} document.
|
|
71
|
+
*
|
|
72
|
+
* MongoDB's query language is nearly identical to the `FilterExpr` structure,
|
|
73
|
+
* so this is largely a structural pass-through via the `walkFilter` visitor.
|
|
74
|
+
* `fieldOperands` (view predicates only — `translateQueryTree` output) turns
|
|
75
|
+
* `{ $field: path }` operands into field-to-field `$expr` comparisons; a
|
|
76
|
+
* request filter never gets that reading.
|
|
77
|
+
*/
|
|
78
|
+
function buildMongoFilter(filter, { fieldOperands = false } = {}) {
|
|
79
|
+
if (!filter || Object.keys(filter).length === 0) return EMPTY;
|
|
80
|
+
return walkFilter(filter, fieldOperands ? fieldOperandVisitor : mongoVisitor) ?? EMPTY;
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/lib/mongo-view-expr.ts
|
|
84
|
+
/**
|
|
85
|
+
* `x IS NOT NULL` in SQL terms — true when `x` is neither null nor missing.
|
|
86
|
+
* Aggregation order puts a missing value below null and every other value
|
|
87
|
+
* above it, so one comparison covers both (`{ $ne: [x, null] }` alone would
|
|
88
|
+
* be true for a MISSING field).
|
|
89
|
+
* @since 0.1.136
|
|
90
|
+
*/
|
|
91
|
+
function notNullExpr(x) {
|
|
92
|
+
return { $gt: [x, null] };
|
|
93
|
+
}
|
|
94
|
+
/** `x IS NULL` in SQL terms — true when `x` is null or missing (see {@link notNullExpr}). @since 0.1.136 */
|
|
95
|
+
function isNullExpr(x) {
|
|
96
|
+
return { $lte: [x, null] };
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* `x`, with a missing value read as null — for a projected column (a missing
|
|
100
|
+
* key would drop it from the row) and a `$group` key (missing and null would
|
|
101
|
+
* form two groups). Only where a value may be missing: the wrapper hides the
|
|
102
|
+
* path from `$match` / `$sort` pushdown.
|
|
103
|
+
* @since 0.1.136
|
|
104
|
+
*/
|
|
105
|
+
function orNull(x) {
|
|
106
|
+
return { $ifNull: [x, null] };
|
|
107
|
+
}
|
|
108
|
+
/** A literal inside `$expr` — `$`-prefixed strings would otherwise read as field paths. */
|
|
109
|
+
function literal(value) {
|
|
110
|
+
return typeof value === "string" && value.startsWith("$") ? { $literal: value } : value;
|
|
111
|
+
}
|
|
112
|
+
const COMPARISONS = {
|
|
113
|
+
$eq: "$eq",
|
|
114
|
+
$ne: "$ne",
|
|
115
|
+
$gt: "$gt",
|
|
116
|
+
$gte: "$gte",
|
|
117
|
+
$lt: "$lt",
|
|
118
|
+
$lte: "$lte"
|
|
119
|
+
};
|
|
120
|
+
/** `cond` AND-guarded so that every field operand is non-null (SQL: NULL operand → not true). */
|
|
121
|
+
function guarded(operands, cond) {
|
|
122
|
+
return { $and: [...operands.map((o) => notNullExpr(o)), cond] };
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Translates a view predicate (join condition, conditional-aggregate filter)
|
|
126
|
+
* to an aggregation expression for `$match: { $expr }` / `$cond`, matching
|
|
127
|
+
* SQL's three-valued logic wherever a NULL operand makes SQL's comparison
|
|
128
|
+
* UNKNOWN (treated as false):
|
|
129
|
+
*
|
|
130
|
+
* - `<`, `<=`, `>`, `>=`, `!= <literal>`, field `=` field, field `!=` field
|
|
131
|
+
* and `not in` are guarded with "every field operand is not null";
|
|
132
|
+
* - `= null` / `not exists` → null-or-missing; `!= null` / `exists` → neither;
|
|
133
|
+
* - `= <literal>` and `in (…)` compare directly (a null operand never equals
|
|
134
|
+
* a non-null literal); an empty `not in` is true;
|
|
135
|
+
* - `and` / `or` / `not` map directly — so `not (x > 1)` is TRUE for a null
|
|
136
|
+
* `x` here while SQL yields UNKNOWN (documented divergence);
|
|
137
|
+
* - `matches` is rejected (`$regexMatch` needs MongoDB 4.2).
|
|
138
|
+
* @since 0.1.136
|
|
139
|
+
*/
|
|
140
|
+
function queryNodeToExpr(node, pathOf) {
|
|
141
|
+
if ("$and" in node) return { $and: node.$and.map((n) => queryNodeToExpr(n, pathOf)) };
|
|
142
|
+
if ("$or" in node) return { $or: node.$or.map((n) => queryNodeToExpr(n, pathOf)) };
|
|
143
|
+
if ("$not" in node) return { $not: [queryNodeToExpr(node.$not, pathOf)] };
|
|
144
|
+
const comp = node;
|
|
145
|
+
const x = pathOf(comp.left);
|
|
146
|
+
switch (comp.op) {
|
|
147
|
+
case "$exists": return comp.right === false ? isNullExpr(x) : notNullExpr(x);
|
|
148
|
+
case "$in": return { $in: [x, (Array.isArray(comp.right) ? comp.right : [comp.right]).map((v) => literal(v))] };
|
|
149
|
+
case "$nin": {
|
|
150
|
+
const values = Array.isArray(comp.right) ? comp.right : [comp.right];
|
|
151
|
+
if (values.length === 0) return { $literal: true };
|
|
152
|
+
return guarded([x], { $not: [{ $in: [x, values.map((v) => literal(v))] }] });
|
|
153
|
+
}
|
|
154
|
+
case "$regex": throw new Error("matches is not supported in view predicates");
|
|
155
|
+
default:
|
|
156
|
+
}
|
|
157
|
+
const op = COMPARISONS[comp.op];
|
|
158
|
+
if (!op) throw new Error(`Operator "${comp.op}" is not supported in view predicates`);
|
|
159
|
+
if (isFieldRef(comp.right)) {
|
|
160
|
+
const y = pathOf(comp.right);
|
|
161
|
+
return guarded([x, y], { [op]: [x, y] });
|
|
162
|
+
}
|
|
163
|
+
if (comp.right === null || comp.right === void 0) {
|
|
164
|
+
if (op === "$eq") return isNullExpr(x);
|
|
165
|
+
if (op === "$ne") return notNullExpr(x);
|
|
166
|
+
return { $literal: false };
|
|
167
|
+
}
|
|
168
|
+
const value = literal(comp.right);
|
|
169
|
+
if (op === "$eq") return { $eq: [x, value] };
|
|
170
|
+
return guarded([x], { [op]: [x, value] });
|
|
171
|
+
}
|
|
172
|
+
//#endregion
|
|
173
|
+
//#region src/lib/mongo-accumulator.ts
|
|
174
|
+
/**
|
|
175
|
+
* The `$group` accumulator of one aggregate — shared by grouped queries
|
|
176
|
+
* (`agg.ts`) and managed views (`mongo-view-pipeline.ts`), so both count and
|
|
177
|
+
* sum alike.
|
|
178
|
+
*
|
|
179
|
+
* - `count(*)` → `{ $sum: 1 }`; `count(f)` counts values that are neither
|
|
180
|
+
* null nor missing (SQL `COUNT(f)`);
|
|
181
|
+
* - `sum` / `avg` / `min` / `max` → `$sum` / `$avg` / `$min` / `$max` (all
|
|
182
|
+
* skip null / missing values);
|
|
183
|
+
* - `countDistinct(f)` → `{ $addToSet: { $ifNull: [f, "$$REMOVE"] } }`, a
|
|
184
|
+
* SET of the non-null values (`$$REMOVE` adds nothing) the caller turns
|
|
185
|
+
* into its size with {@link distinctCountExpr}.
|
|
186
|
+
*
|
|
187
|
+
* `where` (a conditional aggregate's row predicate, an aggregation
|
|
188
|
+
* expression) swaps the source for `{ $cond: [where, src, null] }` — the
|
|
189
|
+
* rejected rows contribute a null, which every accumulator skips — and makes
|
|
190
|
+
* the counts `{ $sum: { $cond: [where (and not null), 1, 0] } }`.
|
|
191
|
+
*
|
|
192
|
+
* @param fn - The aggregate function (re-asserted: `INVALID_QUERY` when unknown).
|
|
193
|
+
* @param src - The source operand (`"$path"`), or `"*"` for `count(*)`.
|
|
194
|
+
* @param where - Row predicate of a conditional aggregate.
|
|
195
|
+
* @param path - Error path of the re-assertion.
|
|
196
|
+
*/
|
|
197
|
+
function buildAccumulator(fn, src, where, path) {
|
|
198
|
+
assertAggregateFn(fn, path);
|
|
199
|
+
if (fn === "count") {
|
|
200
|
+
if (src === "*") return { $sum: where ? { $cond: [
|
|
201
|
+
where,
|
|
202
|
+
1,
|
|
203
|
+
0
|
|
204
|
+
] } : 1 };
|
|
205
|
+
return { $sum: { $cond: [
|
|
206
|
+
where ? { $and: [where, notNullExpr(src)] } : notNullExpr(src),
|
|
207
|
+
1,
|
|
208
|
+
0
|
|
209
|
+
] } };
|
|
210
|
+
}
|
|
211
|
+
const value = where ? { $cond: [
|
|
212
|
+
where,
|
|
213
|
+
src,
|
|
214
|
+
null
|
|
215
|
+
] } : src;
|
|
216
|
+
return fn === "countDistinct" ? { $addToSet: { $ifNull: [value, "$$REMOVE"] } } : { [`$${fn}`]: value };
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* The size of a `countDistinct` set (`"$alias"`) — the value the accumulator
|
|
220
|
+
* stands for, projected right after `$group` so later stages (`$having`,
|
|
221
|
+
* `$sort`) see a number.
|
|
222
|
+
*/
|
|
223
|
+
function distinctCountExpr(set) {
|
|
224
|
+
return { $size: set };
|
|
225
|
+
}
|
|
226
|
+
//#endregion
|
|
227
|
+
export { buildMongoFilter as a, queryNodeToExpr as i, distinctCountExpr as n, orNull as r, buildAccumulator as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/db-mongo",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.136",
|
|
4
4
|
"description": "Mongodb plugin for atscript.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"atscript",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@atscript/core": "^0.1.92",
|
|
50
50
|
"@atscript/typescript": "^0.1.92",
|
|
51
|
-
"@uniqu/core": "^0.1.
|
|
51
|
+
"@uniqu/core": "^0.1.11",
|
|
52
52
|
"mongodb": "^6.17.0",
|
|
53
53
|
"mongodb-memory-server-core": "^10.0.0",
|
|
54
54
|
"unplugin-atscript": "^0.1.92"
|
|
@@ -56,9 +56,9 @@
|
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"@atscript/core": "^0.1.92",
|
|
58
58
|
"@atscript/typescript": "^0.1.92",
|
|
59
|
-
"@uniqu/core": "^0.1.
|
|
59
|
+
"@uniqu/core": "^0.1.11",
|
|
60
60
|
"mongodb": "^6.17.0",
|
|
61
|
-
"@atscript/db": "^0.1.
|
|
61
|
+
"@atscript/db": "^0.1.136"
|
|
62
62
|
},
|
|
63
63
|
"scripts": {
|
|
64
64
|
"postinstall": "asc -f dts",
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
let _atscript_db = require("@atscript/db");
|
|
2
|
-
//#region src/lib/mongo-filter.ts
|
|
3
|
-
const EMPTY = {};
|
|
4
|
-
function parseRegexString(value) {
|
|
5
|
-
if (value instanceof RegExp) return {
|
|
6
|
-
pattern: value.source,
|
|
7
|
-
flags: value.flags
|
|
8
|
-
};
|
|
9
|
-
const str = String(value);
|
|
10
|
-
const match = str.match(/^\/(.+)\/([gimsuy]*)$/);
|
|
11
|
-
if (match) return {
|
|
12
|
-
pattern: match[1],
|
|
13
|
-
flags: match[2]
|
|
14
|
-
};
|
|
15
|
-
return {
|
|
16
|
-
pattern: str,
|
|
17
|
-
flags: ""
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Earth radius in meters used by MongoDB's `$centerSphere` radians conversion
|
|
22
|
-
* (Mongo documents dividing by 6378.1 km).
|
|
23
|
-
*/
|
|
24
|
-
const EARTH_RADIUS_M = 6378100;
|
|
25
|
-
const mongoVisitor = {
|
|
26
|
-
comparison(field, op, value) {
|
|
27
|
-
if (op === "$eq") return { [field]: value };
|
|
28
|
-
if (op === "$exists") return value ? { [field]: { $ne: null } } : { [field]: null };
|
|
29
|
-
if (op === "$regex") {
|
|
30
|
-
const { pattern, flags } = parseRegexString(value);
|
|
31
|
-
return flags ? { [field]: {
|
|
32
|
-
$regex: pattern,
|
|
33
|
-
$options: flags
|
|
34
|
-
} } : { [field]: { $regex: pattern } };
|
|
35
|
-
}
|
|
36
|
-
if (op === "$geoWithin") {
|
|
37
|
-
const { center, radius } = value;
|
|
38
|
-
return { [field]: { $geoWithin: { $centerSphere: [center, radius / EARTH_RADIUS_M] } } };
|
|
39
|
-
}
|
|
40
|
-
return { [field]: { [op]: value } };
|
|
41
|
-
},
|
|
42
|
-
and(children) {
|
|
43
|
-
if (children.length === 0) return EMPTY;
|
|
44
|
-
if (children.length === 1) return children[0];
|
|
45
|
-
return { $and: children };
|
|
46
|
-
},
|
|
47
|
-
or(children) {
|
|
48
|
-
if (children.length === 0) return { _impossible: true };
|
|
49
|
-
if (children.length === 1) return children[0];
|
|
50
|
-
return { $or: children };
|
|
51
|
-
},
|
|
52
|
-
not(child) {
|
|
53
|
-
return { $nor: [child] };
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
/**
|
|
57
|
-
* Translates a generic {@link FilterExpr} into a MongoDB-compatible
|
|
58
|
-
* {@link Filter} document.
|
|
59
|
-
*
|
|
60
|
-
* MongoDB's query language is nearly identical to the `FilterExpr` structure,
|
|
61
|
-
* so this is largely a structural pass-through via the `walkFilter` visitor.
|
|
62
|
-
*/
|
|
63
|
-
function buildMongoFilter(filter) {
|
|
64
|
-
if (!filter || Object.keys(filter).length === 0) return EMPTY;
|
|
65
|
-
return (0, _atscript_db.walkFilter)(filter, mongoVisitor) ?? EMPTY;
|
|
66
|
-
}
|
|
67
|
-
//#endregion
|
|
68
|
-
Object.defineProperty(exports, "buildMongoFilter", {
|
|
69
|
-
enumerable: true,
|
|
70
|
-
get: function() {
|
|
71
|
-
return buildMongoFilter;
|
|
72
|
-
}
|
|
73
|
-
});
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { walkFilter } from "@atscript/db";
|
|
2
|
-
//#region src/lib/mongo-filter.ts
|
|
3
|
-
const EMPTY = {};
|
|
4
|
-
function parseRegexString(value) {
|
|
5
|
-
if (value instanceof RegExp) return {
|
|
6
|
-
pattern: value.source,
|
|
7
|
-
flags: value.flags
|
|
8
|
-
};
|
|
9
|
-
const str = String(value);
|
|
10
|
-
const match = str.match(/^\/(.+)\/([gimsuy]*)$/);
|
|
11
|
-
if (match) return {
|
|
12
|
-
pattern: match[1],
|
|
13
|
-
flags: match[2]
|
|
14
|
-
};
|
|
15
|
-
return {
|
|
16
|
-
pattern: str,
|
|
17
|
-
flags: ""
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Earth radius in meters used by MongoDB's `$centerSphere` radians conversion
|
|
22
|
-
* (Mongo documents dividing by 6378.1 km).
|
|
23
|
-
*/
|
|
24
|
-
const EARTH_RADIUS_M = 6378100;
|
|
25
|
-
const mongoVisitor = {
|
|
26
|
-
comparison(field, op, value) {
|
|
27
|
-
if (op === "$eq") return { [field]: value };
|
|
28
|
-
if (op === "$exists") return value ? { [field]: { $ne: null } } : { [field]: null };
|
|
29
|
-
if (op === "$regex") {
|
|
30
|
-
const { pattern, flags } = parseRegexString(value);
|
|
31
|
-
return flags ? { [field]: {
|
|
32
|
-
$regex: pattern,
|
|
33
|
-
$options: flags
|
|
34
|
-
} } : { [field]: { $regex: pattern } };
|
|
35
|
-
}
|
|
36
|
-
if (op === "$geoWithin") {
|
|
37
|
-
const { center, radius } = value;
|
|
38
|
-
return { [field]: { $geoWithin: { $centerSphere: [center, radius / EARTH_RADIUS_M] } } };
|
|
39
|
-
}
|
|
40
|
-
return { [field]: { [op]: value } };
|
|
41
|
-
},
|
|
42
|
-
and(children) {
|
|
43
|
-
if (children.length === 0) return EMPTY;
|
|
44
|
-
if (children.length === 1) return children[0];
|
|
45
|
-
return { $and: children };
|
|
46
|
-
},
|
|
47
|
-
or(children) {
|
|
48
|
-
if (children.length === 0) return { _impossible: true };
|
|
49
|
-
if (children.length === 1) return children[0];
|
|
50
|
-
return { $or: children };
|
|
51
|
-
},
|
|
52
|
-
not(child) {
|
|
53
|
-
return { $nor: [child] };
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
/**
|
|
57
|
-
* Translates a generic {@link FilterExpr} into a MongoDB-compatible
|
|
58
|
-
* {@link Filter} document.
|
|
59
|
-
*
|
|
60
|
-
* MongoDB's query language is nearly identical to the `FilterExpr` structure,
|
|
61
|
-
* so this is largely a structural pass-through via the `walkFilter` visitor.
|
|
62
|
-
*/
|
|
63
|
-
function buildMongoFilter(filter) {
|
|
64
|
-
if (!filter || Object.keys(filter).length === 0) return EMPTY;
|
|
65
|
-
return walkFilter(filter, mongoVisitor) ?? EMPTY;
|
|
66
|
-
}
|
|
67
|
-
//#endregion
|
|
68
|
-
export { buildMongoFilter as t };
|