@prisma-next/mongo-query-builder 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +144 -0
- package/dist/index.d.mts +1198 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1591 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +46 -0
- package/src/accumulator-helpers.ts +172 -0
- package/src/builder.ts +958 -0
- package/src/exports/index.ts +51 -0
- package/src/expression-helpers.ts +519 -0
- package/src/field-accessor.ts +280 -0
- package/src/markers.ts +25 -0
- package/src/query.ts +55 -0
- package/src/resolve-path.ts +199 -0
- package/src/state-classes.ts +651 -0
- package/src/types.ts +113 -0
- package/src/update-ops.ts +229 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1591 @@
|
|
|
1
|
+
import { AggregateCommand, DeleteManyCommand, DeleteOneCommand, FindOneAndDeleteCommand, FindOneAndUpdateCommand, InsertManyCommand, InsertOneCommand, MongoAddFieldsStage, MongoAggAccumulator, MongoAggCond, MongoAggFieldRef, MongoAggLiteral, MongoAggOperator, MongoAndExpr, MongoBucketAutoStage, MongoBucketStage, MongoCountStage, MongoDensifyStage, MongoExistsExpr, MongoFacetStage, MongoFieldFilter, MongoFillStage, MongoGeoNearStage, MongoGraphLookupStage, MongoGroupStage, MongoLimitStage, MongoLookupStage, MongoMatchStage, MongoMergeStage, MongoOutStage, MongoProjectStage, MongoRedactStage, MongoReplaceRootStage, MongoSampleStage, MongoSearchMetaStage, MongoSearchStage, MongoSetWindowFieldsStage, MongoSkipStage, MongoSortByCountStage, MongoSortStage, MongoUnionWithStage, MongoUnwindStage, MongoVectorSearchStage, UpdateManyCommand, UpdateOneCommand } from "@prisma-next/mongo-query-ast/execution";
|
|
2
|
+
|
|
3
|
+
//#region src/accumulator-helpers.ts
|
|
4
|
+
function namedAccumulatorArgs(args) {
|
|
5
|
+
const result = {};
|
|
6
|
+
for (const [key, val] of Object.entries(args)) if (val !== void 0) result[key] = val.node;
|
|
7
|
+
return result;
|
|
8
|
+
}
|
|
9
|
+
const acc = {
|
|
10
|
+
sum(expr) {
|
|
11
|
+
return {
|
|
12
|
+
_field: void 0,
|
|
13
|
+
node: MongoAggAccumulator.sum(expr.node)
|
|
14
|
+
};
|
|
15
|
+
},
|
|
16
|
+
avg(expr) {
|
|
17
|
+
return {
|
|
18
|
+
_field: void 0,
|
|
19
|
+
node: MongoAggAccumulator.avg(expr.node)
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
min(expr) {
|
|
23
|
+
return {
|
|
24
|
+
_field: void 0,
|
|
25
|
+
node: MongoAggAccumulator.min(expr.node)
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
max(expr) {
|
|
29
|
+
return {
|
|
30
|
+
_field: void 0,
|
|
31
|
+
node: MongoAggAccumulator.max(expr.node)
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
first(expr) {
|
|
35
|
+
return {
|
|
36
|
+
_field: void 0,
|
|
37
|
+
node: MongoAggAccumulator.first(expr.node)
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
last(expr) {
|
|
41
|
+
return {
|
|
42
|
+
_field: void 0,
|
|
43
|
+
node: MongoAggAccumulator.last(expr.node)
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
push(expr) {
|
|
47
|
+
return {
|
|
48
|
+
_field: void 0,
|
|
49
|
+
node: MongoAggAccumulator.push(expr.node)
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
addToSet(expr) {
|
|
53
|
+
return {
|
|
54
|
+
_field: void 0,
|
|
55
|
+
node: MongoAggAccumulator.addToSet(expr.node)
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
count() {
|
|
59
|
+
return {
|
|
60
|
+
_field: void 0,
|
|
61
|
+
node: MongoAggAccumulator.count()
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
stdDevPop(expr) {
|
|
65
|
+
return {
|
|
66
|
+
_field: void 0,
|
|
67
|
+
node: MongoAggAccumulator.stdDevPop(expr.node)
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
stdDevSamp(expr) {
|
|
71
|
+
return {
|
|
72
|
+
_field: void 0,
|
|
73
|
+
node: MongoAggAccumulator.stdDevSamp(expr.node)
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
firstN(args) {
|
|
77
|
+
return {
|
|
78
|
+
_field: void 0,
|
|
79
|
+
node: MongoAggAccumulator.of("$firstN", namedAccumulatorArgs(args))
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
lastN(args) {
|
|
83
|
+
return {
|
|
84
|
+
_field: void 0,
|
|
85
|
+
node: MongoAggAccumulator.of("$lastN", namedAccumulatorArgs(args))
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
maxN(args) {
|
|
89
|
+
return {
|
|
90
|
+
_field: void 0,
|
|
91
|
+
node: MongoAggAccumulator.of("$maxN", namedAccumulatorArgs(args))
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
minN(args) {
|
|
95
|
+
return {
|
|
96
|
+
_field: void 0,
|
|
97
|
+
node: MongoAggAccumulator.of("$minN", namedAccumulatorArgs(args))
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
top(args) {
|
|
101
|
+
return {
|
|
102
|
+
_field: void 0,
|
|
103
|
+
node: MongoAggAccumulator.of("$top", {
|
|
104
|
+
output: args.output.node,
|
|
105
|
+
sortBy: MongoAggLiteral.of(args.sortBy)
|
|
106
|
+
})
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
bottom(args) {
|
|
110
|
+
return {
|
|
111
|
+
_field: void 0,
|
|
112
|
+
node: MongoAggAccumulator.of("$bottom", {
|
|
113
|
+
output: args.output.node,
|
|
114
|
+
sortBy: MongoAggLiteral.of(args.sortBy)
|
|
115
|
+
})
|
|
116
|
+
};
|
|
117
|
+
},
|
|
118
|
+
topN(args) {
|
|
119
|
+
return {
|
|
120
|
+
_field: void 0,
|
|
121
|
+
node: MongoAggAccumulator.of("$topN", {
|
|
122
|
+
output: args.output.node,
|
|
123
|
+
sortBy: MongoAggLiteral.of(args.sortBy),
|
|
124
|
+
n: args.n.node
|
|
125
|
+
})
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
bottomN(args) {
|
|
129
|
+
return {
|
|
130
|
+
_field: void 0,
|
|
131
|
+
node: MongoAggAccumulator.of("$bottomN", {
|
|
132
|
+
output: args.output.node,
|
|
133
|
+
sortBy: MongoAggLiteral.of(args.sortBy),
|
|
134
|
+
n: args.n.node
|
|
135
|
+
})
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
//#endregion
|
|
141
|
+
//#region src/update-ops.ts
|
|
142
|
+
const setOp = (path, value) => ({
|
|
143
|
+
op: "$set",
|
|
144
|
+
path,
|
|
145
|
+
value
|
|
146
|
+
});
|
|
147
|
+
const unsetOp = (path) => ({
|
|
148
|
+
op: "$unset",
|
|
149
|
+
path
|
|
150
|
+
});
|
|
151
|
+
const renameOp = (path, newName) => ({
|
|
152
|
+
op: "$rename",
|
|
153
|
+
path,
|
|
154
|
+
newName
|
|
155
|
+
});
|
|
156
|
+
const incOp = (path, amount) => ({
|
|
157
|
+
op: "$inc",
|
|
158
|
+
path,
|
|
159
|
+
amount
|
|
160
|
+
});
|
|
161
|
+
const mulOp = (path, factor) => ({
|
|
162
|
+
op: "$mul",
|
|
163
|
+
path,
|
|
164
|
+
factor
|
|
165
|
+
});
|
|
166
|
+
const minOp = (path, value) => ({
|
|
167
|
+
op: "$min",
|
|
168
|
+
path,
|
|
169
|
+
value
|
|
170
|
+
});
|
|
171
|
+
const maxOp = (path, value) => ({
|
|
172
|
+
op: "$max",
|
|
173
|
+
path,
|
|
174
|
+
value
|
|
175
|
+
});
|
|
176
|
+
const pushOp = (path, value) => ({
|
|
177
|
+
op: "$push",
|
|
178
|
+
path,
|
|
179
|
+
value
|
|
180
|
+
});
|
|
181
|
+
const addToSetOp = (path, value) => ({
|
|
182
|
+
op: "$addToSet",
|
|
183
|
+
path,
|
|
184
|
+
value
|
|
185
|
+
});
|
|
186
|
+
const popOp = (path, direction) => ({
|
|
187
|
+
op: "$pop",
|
|
188
|
+
path,
|
|
189
|
+
direction
|
|
190
|
+
});
|
|
191
|
+
const pullOp = (path, value) => ({
|
|
192
|
+
op: "$pull",
|
|
193
|
+
path,
|
|
194
|
+
value
|
|
195
|
+
});
|
|
196
|
+
const pullAllOp = (path, values) => ({
|
|
197
|
+
op: "$pullAll",
|
|
198
|
+
path,
|
|
199
|
+
values
|
|
200
|
+
});
|
|
201
|
+
const currentDateOp = (path) => ({
|
|
202
|
+
op: "$currentDate",
|
|
203
|
+
path
|
|
204
|
+
});
|
|
205
|
+
const setOnInsertOp = (path, value) => ({
|
|
206
|
+
op: "$setOnInsert",
|
|
207
|
+
path,
|
|
208
|
+
value
|
|
209
|
+
});
|
|
210
|
+
/**
|
|
211
|
+
* Fold an array of `TypedUpdateOp` into the non-pipeline variant of
|
|
212
|
+
* `MongoUpdateSpec` (`{ $set: { … }, $inc: { … }, … }`).
|
|
213
|
+
*
|
|
214
|
+
* Throws if the same operator targets the same path twice — a clear authoring
|
|
215
|
+
* error that Mongo would otherwise silently coalesce.
|
|
216
|
+
*/
|
|
217
|
+
function foldUpdateOps(ops) {
|
|
218
|
+
const buckets = {};
|
|
219
|
+
const seen = /* @__PURE__ */ new Set();
|
|
220
|
+
const ensure = (key) => {
|
|
221
|
+
let bucket = buckets[key];
|
|
222
|
+
if (!bucket) {
|
|
223
|
+
bucket = {};
|
|
224
|
+
buckets[key] = bucket;
|
|
225
|
+
}
|
|
226
|
+
return bucket;
|
|
227
|
+
};
|
|
228
|
+
const claim = (op, path) => {
|
|
229
|
+
const k = `${op}::${path}`;
|
|
230
|
+
if (seen.has(k)) throw new Error(`Update spec collision: ${op} on '${path}' was specified more than once. Combine the operations into a single call site.`);
|
|
231
|
+
seen.add(k);
|
|
232
|
+
};
|
|
233
|
+
for (const entry of ops) {
|
|
234
|
+
claim(entry.op, entry.path);
|
|
235
|
+
switch (entry.op) {
|
|
236
|
+
case "$set":
|
|
237
|
+
case "$min":
|
|
238
|
+
case "$max":
|
|
239
|
+
case "$push":
|
|
240
|
+
case "$addToSet":
|
|
241
|
+
case "$pull":
|
|
242
|
+
case "$setOnInsert":
|
|
243
|
+
ensure(entry.op)[entry.path] = entry.value;
|
|
244
|
+
break;
|
|
245
|
+
case "$unset":
|
|
246
|
+
ensure("$unset")[entry.path] = "";
|
|
247
|
+
break;
|
|
248
|
+
case "$rename":
|
|
249
|
+
ensure("$rename")[entry.path] = entry.newName;
|
|
250
|
+
break;
|
|
251
|
+
case "$inc":
|
|
252
|
+
ensure("$inc")[entry.path] = entry.amount;
|
|
253
|
+
break;
|
|
254
|
+
case "$mul":
|
|
255
|
+
ensure("$mul")[entry.path] = entry.factor;
|
|
256
|
+
break;
|
|
257
|
+
case "$pop":
|
|
258
|
+
ensure("$pop")[entry.path] = entry.direction;
|
|
259
|
+
break;
|
|
260
|
+
case "$pullAll":
|
|
261
|
+
ensure("$pullAll")[entry.path] = entry.values;
|
|
262
|
+
break;
|
|
263
|
+
case "$currentDate":
|
|
264
|
+
ensure("$currentDate")[entry.path] = true;
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return buckets;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Classify an array of updater items and produce a `MongoUpdateSpec`.
|
|
272
|
+
*
|
|
273
|
+
* - All `TypedUpdateOp` → fold via `foldUpdateOps` (classic `{ $set, $inc, … }`)
|
|
274
|
+
* - All `MongoUpdatePipelineStage` → return as-is (pipeline-style update)
|
|
275
|
+
* - Mixed → throw (also a type error at the call site via the union shape)
|
|
276
|
+
*/
|
|
277
|
+
function resolveUpdaterResult(items) {
|
|
278
|
+
if (items.length === 0) throw new Error("Updater returned no operations. Return at least one update from the callback (e.g. `[f.amount.set(0)]`).");
|
|
279
|
+
const isOp = (item) => "op" in item && typeof item.op === "string";
|
|
280
|
+
const first = items[0];
|
|
281
|
+
if (first === void 0) throw new Error("Unreachable: items.length > 0 but first is undefined");
|
|
282
|
+
const firstIsOp = isOp(first);
|
|
283
|
+
for (let i = 1; i < items.length; i++) {
|
|
284
|
+
const item = items[i];
|
|
285
|
+
if (item === void 0) continue;
|
|
286
|
+
if (isOp(item) !== firstIsOp) throw new Error("Cannot mix TypedUpdateOp values and pipeline stages in a single updater. Use either `[f.amount.set(0)]` (operator form) or `[f.stage.set({...})]` (pipeline form), not both.");
|
|
287
|
+
}
|
|
288
|
+
if (firstIsOp) return foldUpdateOps(items);
|
|
289
|
+
return items;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
//#endregion
|
|
293
|
+
//#region src/field-accessor.ts
|
|
294
|
+
function buildStageEmitters() {
|
|
295
|
+
return {
|
|
296
|
+
set: (fields) => new MongoAddFieldsStage(fields),
|
|
297
|
+
unset: (...paths) => {
|
|
298
|
+
const spec = {};
|
|
299
|
+
for (const p of paths) spec[p] = 0;
|
|
300
|
+
return new MongoProjectStage(spec);
|
|
301
|
+
},
|
|
302
|
+
replaceRoot: (newRoot) => new MongoReplaceRootStage(newRoot),
|
|
303
|
+
replaceWith: (newRoot) => new MongoReplaceRootStage(newRoot)
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
function buildExpression(path) {
|
|
307
|
+
return {
|
|
308
|
+
_field: void 0,
|
|
309
|
+
_path: path,
|
|
310
|
+
node: MongoAggFieldRef.of(path),
|
|
311
|
+
eq: (value) => MongoFieldFilter.eq(path, value),
|
|
312
|
+
ne: (value) => MongoFieldFilter.neq(path, value),
|
|
313
|
+
gt: (value) => MongoFieldFilter.gt(path, value),
|
|
314
|
+
gte: (value) => MongoFieldFilter.gte(path, value),
|
|
315
|
+
lt: (value) => MongoFieldFilter.lt(path, value),
|
|
316
|
+
lte: (value) => MongoFieldFilter.lte(path, value),
|
|
317
|
+
in: (values) => MongoFieldFilter.in(path, values),
|
|
318
|
+
nin: (values) => MongoFieldFilter.nin(path, values),
|
|
319
|
+
exists: (flag) => flag === false ? MongoExistsExpr.notExists(path) : MongoExistsExpr.exists(path),
|
|
320
|
+
set: (value) => setOp(path, value),
|
|
321
|
+
unset: () => unsetOp(path),
|
|
322
|
+
rename: (newName) => renameOp(path, newName),
|
|
323
|
+
inc: (amount) => incOp(path, amount),
|
|
324
|
+
mul: (factor) => mulOp(path, factor),
|
|
325
|
+
min: (value) => minOp(path, value),
|
|
326
|
+
max: (value) => maxOp(path, value),
|
|
327
|
+
push: (value) => pushOp(path, value),
|
|
328
|
+
addToSet: (value) => addToSetOp(path, value),
|
|
329
|
+
pop: (direction = 1) => popOp(path, direction),
|
|
330
|
+
pull: (value) => pullOp(path, value),
|
|
331
|
+
pullAll: (values) => pullAllOp(path, values),
|
|
332
|
+
currentDate: () => currentDateOp(path),
|
|
333
|
+
setOnInsert: (value) => setOnInsertOp(path, value)
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Construct a unified `FieldAccessor<S, N>` proxy. Property access creates
|
|
338
|
+
* an `Expression` using the property name as the field path; callable
|
|
339
|
+
* form accepts a dot-path string validated against `N` at compile time.
|
|
340
|
+
*
|
|
341
|
+
* The proxy target is a function so the resulting object is both callable
|
|
342
|
+
* and indexable. Symbol-keyed accesses (e.g. `Symbol.toPrimitive`) return
|
|
343
|
+
* `undefined` to keep accidental coercion behaviour unsurprising —
|
|
344
|
+
* matching the previous `FieldProxy` / `FilterProxy` semantics.
|
|
345
|
+
*/
|
|
346
|
+
function createFieldAccessor() {
|
|
347
|
+
const stageInstance = buildStageEmitters();
|
|
348
|
+
const callable = ((path) => buildExpression(path));
|
|
349
|
+
return new Proxy(callable, { get(target, prop, receiver) {
|
|
350
|
+
if (typeof prop === "symbol") return Reflect.get(target, prop, receiver);
|
|
351
|
+
if (prop === "stage") return stageInstance;
|
|
352
|
+
if (prop === "rawPath") return (path) => buildExpression(path);
|
|
353
|
+
return buildExpression(prop);
|
|
354
|
+
} });
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
//#endregion
|
|
358
|
+
//#region src/builder.ts
|
|
359
|
+
/**
|
|
360
|
+
* The pipeline state in the query-builder state machine.
|
|
361
|
+
*
|
|
362
|
+
* Reached from `CollectionHandle` or `FilteredCollection` after the first
|
|
363
|
+
* pipeline-stage method call (or directly via `aggregate()` shortcuts). Holds
|
|
364
|
+
* the accumulated `MongoPipelineStage[]` and exposes pipeline-stage methods,
|
|
365
|
+
* the `merge`/`out` write terminals, and the `build`/`aggregate` read
|
|
366
|
+
* terminals.
|
|
367
|
+
*
|
|
368
|
+
* Two phantom type parameters gate the conditional terminals:
|
|
369
|
+
*
|
|
370
|
+
* - `U extends UpdateEnabled` — when `'update-ok'`, the no-arg `updateMany()` /
|
|
371
|
+
* `updateOne()` form is available (consume the chain as an
|
|
372
|
+
* update-with-pipeline spec). Cleared by stages that produce content the
|
|
373
|
+
* `update` AST cannot represent (e.g. `$group`, `$lookup`, `$limit`).
|
|
374
|
+
* - `F extends FindAndModifyEnabled` — when `'fam-ok'`, the
|
|
375
|
+
* `findOneAndUpdate(...)` / `findOneAndDelete(...)` terminals are
|
|
376
|
+
* available. Cleared by stages incompatible with their wire-command slots
|
|
377
|
+
* (`$limit`, `$group`, mutating stages, …).
|
|
378
|
+
*
|
|
379
|
+
* The marker semantics are encoded in the per-method return types — see the
|
|
380
|
+
* marker table (and rationale per row) in
|
|
381
|
+
* `docs/architecture docs/adrs/ADR 201 - State-machine pattern for typed DSL builders.md`.
|
|
382
|
+
*/
|
|
383
|
+
var PipelineChain = class PipelineChain {
|
|
384
|
+
#contract;
|
|
385
|
+
#state;
|
|
386
|
+
constructor(contract, state) {
|
|
387
|
+
this.#contract = contract;
|
|
388
|
+
this.#state = state;
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Internal helper that appends a pipeline stage and branches into a new
|
|
392
|
+
* state-type. The fifth type parameter `NewN` carries the nested-path
|
|
393
|
+
* shape forward. It defaults to `Record<string, never>` so stages that
|
|
394
|
+
* fundamentally rewrite the document (`$group`, `$project`,
|
|
395
|
+
* `$replaceRoot`, …) automatically disable the callable form of
|
|
396
|
+
* `FieldAccessor` downstream. Additive stages (`match`, `addFields`,
|
|
397
|
+
* `sort`, `lookup`, …) explicitly re-thread the current `N`.
|
|
398
|
+
*/
|
|
399
|
+
#withStage(stage) {
|
|
400
|
+
return new PipelineChain(this.#contract, {
|
|
401
|
+
...this.#state,
|
|
402
|
+
stages: [...this.#state.stages, stage]
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
#writeMeta() {
|
|
406
|
+
return {
|
|
407
|
+
target: "mongo",
|
|
408
|
+
storageHash: this.#state.storageHash,
|
|
409
|
+
lane: "mongo-query",
|
|
410
|
+
paramDescriptors: []
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
match(filterOrFn) {
|
|
414
|
+
const filter = typeof filterOrFn === "function" ? filterOrFn(createFieldAccessor()) : filterOrFn;
|
|
415
|
+
return this.#withStage(new MongoMatchStage(filter));
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* `$sort`. Clears `UpdateEnabled` (`update` has no per-document sort) but
|
|
419
|
+
* preserves `FindAndModifyEnabled` (`findAndModify` has a `sort` slot).
|
|
420
|
+
*/
|
|
421
|
+
sort(spec) {
|
|
422
|
+
return this.#withStage(new MongoSortStage(spec));
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* `$limit`. Clears both markers — `limit` is incompatible with the `update`
|
|
426
|
+
* wire command, and `findAndModify` already implies single-document
|
|
427
|
+
* semantics (so `.limit(...)` adds no meaning, only ambiguity).
|
|
428
|
+
*/
|
|
429
|
+
limit(n) {
|
|
430
|
+
return this.#withStage(new MongoLimitStage(n));
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* `$skip`. Clears both markers — MongoDB's `findAndModify` wire command
|
|
434
|
+
* has no `skip` slot, so `deconstructFindAndModifyChain` rejects any
|
|
435
|
+
* `$skip` at runtime; keeping the marker `fam-cleared` makes the type
|
|
436
|
+
* system reflect the same constraint (see ADR 201 marker table).
|
|
437
|
+
*/
|
|
438
|
+
skip(n) {
|
|
439
|
+
return this.#withStage(new MongoSkipStage(n));
|
|
440
|
+
}
|
|
441
|
+
sample(n) {
|
|
442
|
+
return this.#withStage(new MongoSampleStage(n));
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* `$addFields`. Preserves `UpdateEnabled` (representable as
|
|
446
|
+
* update-with-pipeline `$set`); clears `FindAndModifyEnabled` (no analogue
|
|
447
|
+
* in the find-and-modify wire commands). The nested-path shape `N` is
|
|
448
|
+
* preserved — newly added flat fields are reachable via property access
|
|
449
|
+
* (`f.newField`) but do not themselves carry nested structure.
|
|
450
|
+
*/
|
|
451
|
+
addFields(fn$1) {
|
|
452
|
+
const newFields = fn$1(createFieldAccessor());
|
|
453
|
+
const exprRecord = {};
|
|
454
|
+
for (const [key, typed] of Object.entries(newFields)) exprRecord[key] = typed.node;
|
|
455
|
+
return this.#withStage(new MongoAddFieldsStage(exprRecord));
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* `$lookup`. Clears both markers — joins are not representable in either
|
|
459
|
+
* the `update` or `findAndModify` wire commands. The original document's
|
|
460
|
+
* nested-path shape `N` is preserved (the lookup adds a sidecar array
|
|
461
|
+
* field; existing keys are untouched).
|
|
462
|
+
*/
|
|
463
|
+
lookup(options) {
|
|
464
|
+
const contract = this.#contract;
|
|
465
|
+
const modelName = contract.roots[options.from];
|
|
466
|
+
if (!modelName) {
|
|
467
|
+
const validRoots = Object.keys(contract.roots).join(", ");
|
|
468
|
+
throw new Error(`lookup() unknown root: "${options.from}". Valid roots: ${validRoots}`);
|
|
469
|
+
}
|
|
470
|
+
const collectionName = contract.models[modelName]?.storage?.collection ?? options.from;
|
|
471
|
+
return this.#withStage(new MongoLookupStage({
|
|
472
|
+
from: collectionName,
|
|
473
|
+
localField: options.localField,
|
|
474
|
+
foreignField: options.foreignField,
|
|
475
|
+
as: options.as
|
|
476
|
+
}));
|
|
477
|
+
}
|
|
478
|
+
project(...args) {
|
|
479
|
+
if (args.length === 1 && typeof args[0] === "function") {
|
|
480
|
+
const fn$1 = args[0];
|
|
481
|
+
const spec = fn$1(createFieldAccessor());
|
|
482
|
+
const projection$1 = {};
|
|
483
|
+
for (const [key, val] of Object.entries(spec)) projection$1[key] = val === 1 ? 1 : val.node;
|
|
484
|
+
return this.#withStage(new MongoProjectStage(projection$1));
|
|
485
|
+
}
|
|
486
|
+
const keys = args;
|
|
487
|
+
const projection = {};
|
|
488
|
+
for (const key of keys) projection[key] = 1;
|
|
489
|
+
return this.#withStage(new MongoProjectStage(projection));
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* `$unwind`. Clears both markers — array unrolling produces multiple output
|
|
493
|
+
* documents per input, incompatible with both single-document update and
|
|
494
|
+
* find-and-modify wire commands. The original `N` is preserved: unwind
|
|
495
|
+
* replaces the unwound array slot with its element but leaves the rest
|
|
496
|
+
* of the document structurally intact.
|
|
497
|
+
*/
|
|
498
|
+
unwind(field, options) {
|
|
499
|
+
return this.#withStage(new MongoUnwindStage(`$${field}`, options?.preserveNullAndEmptyArrays ?? false));
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* `$group`. Clears both markers — group output bears no relation to source
|
|
503
|
+
* documents; neither `update` nor `findAndModify` can consume it. Nested
|
|
504
|
+
* path shape is reset (the source document's path tree is gone).
|
|
505
|
+
*/
|
|
506
|
+
group(fn$1) {
|
|
507
|
+
const { _id: groupIdExpr, ...rest } = fn$1(createFieldAccessor());
|
|
508
|
+
const groupId = groupIdExpr === null ? null : groupIdExpr.node;
|
|
509
|
+
const accumulators = {};
|
|
510
|
+
for (const [key, typed] of Object.entries(rest)) {
|
|
511
|
+
if (typed === null) throw new Error(`group() field "${key}" must not be null. Only _id can be null.`);
|
|
512
|
+
if (typed.node.kind !== "accumulator") throw new Error(`group() field "${key}" must use an accumulator (e.g. acc.sum(), acc.count()). Got "${typed.node.kind}" expression.`);
|
|
513
|
+
accumulators[key] = typed.node;
|
|
514
|
+
}
|
|
515
|
+
return this.#withStage(new MongoGroupStage(groupId, accumulators));
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* `$replaceRoot`. Preserves `UpdateEnabled` (representable as
|
|
519
|
+
* update-with-pipeline `$replaceRoot`); clears `FindAndModifyEnabled`.
|
|
520
|
+
* Nested path shape is reset — the replaced root has no relation to
|
|
521
|
+
* the original document structure.
|
|
522
|
+
*/
|
|
523
|
+
replaceRoot(fn$1) {
|
|
524
|
+
const expr = fn$1(createFieldAccessor());
|
|
525
|
+
return this.#withStage(new MongoReplaceRootStage(expr.node));
|
|
526
|
+
}
|
|
527
|
+
count(field) {
|
|
528
|
+
return this.#withStage(new MongoCountStage(field));
|
|
529
|
+
}
|
|
530
|
+
sortByCount(fn$1) {
|
|
531
|
+
const expr = fn$1(createFieldAccessor());
|
|
532
|
+
return this.#withStage(new MongoSortByCountStage(expr.node));
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* `$redact`. Preserves `UpdateEnabled`; clears `FindAndModifyEnabled`.
|
|
536
|
+
* Shape- and nested-path-preserving (the document tree is unchanged).
|
|
537
|
+
*/
|
|
538
|
+
redact(fn$1) {
|
|
539
|
+
const expr = fn$1(createFieldAccessor());
|
|
540
|
+
return this.#withStage(new MongoRedactStage(expr.node));
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* `$out` write terminal. Materialises the pipeline output into
|
|
544
|
+
* `collection` (optionally in `db`), replacing any prior contents. Unlike
|
|
545
|
+
* the other pipeline-stage methods, this **terminates** the chain — it
|
|
546
|
+
* returns a `MongoQueryPlan` rather than another `PipelineChain`, since
|
|
547
|
+
* `$out` must be the final stage and there is nothing further to chain.
|
|
548
|
+
*
|
|
549
|
+
* Lane is `mongo-query` (matching all other terminals in this package) so
|
|
550
|
+
* middleware can dispatch on intent without inspecting the command.
|
|
551
|
+
*
|
|
552
|
+
* The result row stream is empty (`unknown` row type) — the data lives
|
|
553
|
+
* in the destination collection, not the response.
|
|
554
|
+
*/
|
|
555
|
+
out(collection, db) {
|
|
556
|
+
return this.#writeTerminal(new MongoOutStage(collection, db));
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* `$merge` write terminal. Streams the pipeline output into the target
|
|
560
|
+
* collection per the supplied merge semantics (`whenMatched` /
|
|
561
|
+
* `whenNotMatched`). Like `out()`, terminates the chain — `$merge` must
|
|
562
|
+
* be the final stage.
|
|
563
|
+
*/
|
|
564
|
+
merge(options) {
|
|
565
|
+
return this.#writeTerminal(new MongoMergeStage(options));
|
|
566
|
+
}
|
|
567
|
+
#writeTerminal(stage) {
|
|
568
|
+
const pipeline = [...this.#state.stages, stage];
|
|
569
|
+
const command = new AggregateCommand(this.#state.collection, pipeline);
|
|
570
|
+
const meta = {
|
|
571
|
+
target: "mongo",
|
|
572
|
+
storageHash: this.#state.storageHash,
|
|
573
|
+
lane: "mongo-query",
|
|
574
|
+
paramDescriptors: []
|
|
575
|
+
};
|
|
576
|
+
return {
|
|
577
|
+
collection: this.#state.collection,
|
|
578
|
+
command,
|
|
579
|
+
meta
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
unionWith(collection, pipeline) {
|
|
583
|
+
return this.#withStage(new MongoUnionWithStage(collection, pipeline));
|
|
584
|
+
}
|
|
585
|
+
bucket(options) {
|
|
586
|
+
return this.#withStage(new MongoBucketStage(options));
|
|
587
|
+
}
|
|
588
|
+
bucketAuto(options) {
|
|
589
|
+
return this.#withStage(new MongoBucketAutoStage(options));
|
|
590
|
+
}
|
|
591
|
+
geoNear(options) {
|
|
592
|
+
return this.#withStage(new MongoGeoNearStage(options));
|
|
593
|
+
}
|
|
594
|
+
facet(facets) {
|
|
595
|
+
return this.#withStage(new MongoFacetStage(facets));
|
|
596
|
+
}
|
|
597
|
+
graphLookup(options) {
|
|
598
|
+
return this.#withStage(new MongoGraphLookupStage(options));
|
|
599
|
+
}
|
|
600
|
+
setWindowFields(options) {
|
|
601
|
+
return this.#withStage(new MongoSetWindowFieldsStage(options));
|
|
602
|
+
}
|
|
603
|
+
densify(options) {
|
|
604
|
+
return this.#withStage(new MongoDensifyStage(options));
|
|
605
|
+
}
|
|
606
|
+
fill(options) {
|
|
607
|
+
return this.#withStage(new MongoFillStage(options));
|
|
608
|
+
}
|
|
609
|
+
search(config, index) {
|
|
610
|
+
return this.#withStage(new MongoSearchStage(config, index));
|
|
611
|
+
}
|
|
612
|
+
searchMeta(config, index) {
|
|
613
|
+
return this.#withStage(new MongoSearchMetaStage(config, index));
|
|
614
|
+
}
|
|
615
|
+
vectorSearch(options) {
|
|
616
|
+
return this.#withStage(new MongoVectorSearchStage(options));
|
|
617
|
+
}
|
|
618
|
+
pipe(stage) {
|
|
619
|
+
return this.#withStage(stage);
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* No-arg `updateMany()`: deconstruct the chain into leading `$match`
|
|
623
|
+
* stages (folded into the filter) and remaining stages (which must all
|
|
624
|
+
* be valid pipeline-update stages). Available only when `U = 'update-ok'`.
|
|
625
|
+
*
|
|
626
|
+
* The optional callback parameter exists for subclass-override
|
|
627
|
+
* compatibility with `FilteredCollection.updateMany(updaterFn)` — TS's
|
|
628
|
+
* strict override check requires the parent's parameter to accept at
|
|
629
|
+
* least what the child's signature does. A runtime guard throws if a
|
|
630
|
+
* callback is actually passed on a bare `PipelineChain`. Note that
|
|
631
|
+
* because nothing in the public surface transitions `U` from
|
|
632
|
+
* `'update-cleared'` (the initial state on `CollectionHandle` /
|
|
633
|
+
* `FilteredCollection`) back to `'update-ok'`, the no-arg form is
|
|
634
|
+
* reachable only via explicit type casts in internal tests — the
|
|
635
|
+
* callback-form "type hole" is therefore not reachable from user
|
|
636
|
+
* code. See `docs/architecture docs/adrs/ADR 201 - State-machine
|
|
637
|
+
* pattern for typed DSL builders.md` for the marker-transition table.
|
|
638
|
+
*/
|
|
639
|
+
updateMany(updaterFn) {
|
|
640
|
+
if (updaterFn !== void 0) throw new Error("updateMany() on a PipelineChain expects no arguments — the chain itself is the update pipeline. To update with an operator callback, call .updateMany(fn) on a FilteredCollection (i.e. after .match()).");
|
|
641
|
+
const { filter, updatePipeline } = deconstructUpdateChain(this.#state.stages);
|
|
642
|
+
const command = new UpdateManyCommand(this.#state.collection, filter, updatePipeline);
|
|
643
|
+
return {
|
|
644
|
+
collection: this.#state.collection,
|
|
645
|
+
command,
|
|
646
|
+
meta: this.#writeMeta()
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* No-arg `updateOne()`: same as `updateMany()` but maps to a single-doc
|
|
651
|
+
* update. Carries the same optional-callback/subclass-compat caveat
|
|
652
|
+
* documented above — the callback form is reachable only via forced
|
|
653
|
+
* casts in internal tests.
|
|
654
|
+
*/
|
|
655
|
+
updateOne(updaterFn) {
|
|
656
|
+
if (updaterFn !== void 0) throw new Error("updateOne() on a PipelineChain expects no arguments — the chain itself is the update pipeline. To update with an operator callback, call .updateOne(fn) on a FilteredCollection (i.e. after .match()).");
|
|
657
|
+
const { filter, updatePipeline } = deconstructUpdateChain(this.#state.stages);
|
|
658
|
+
const command = new UpdateOneCommand(this.#state.collection, filter, updatePipeline);
|
|
659
|
+
return {
|
|
660
|
+
collection: this.#state.collection,
|
|
661
|
+
command,
|
|
662
|
+
meta: this.#writeMeta()
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Find a single document matching the accumulated pipeline (which must
|
|
667
|
+
* consist solely of leading `$match` stages followed by at most one
|
|
668
|
+
* `$sort`) and apply `updaterFn`. Available only when
|
|
669
|
+
* `FindAndModifyEnabled` is `'fam-ok'` — stages that clear the marker
|
|
670
|
+
* (including `$skip`, which MongoDB's `findAndModify` has no slot for)
|
|
671
|
+
* make this method invisible at the type level.
|
|
672
|
+
*
|
|
673
|
+
* The pipeline stages are deconstructed into the wire command's `filter`
|
|
674
|
+
* and `sort` slots. If any non-deconstructable stage is present, a
|
|
675
|
+
* runtime error is thrown as a defensive check (the type system should
|
|
676
|
+
* prevent this).
|
|
677
|
+
*/
|
|
678
|
+
findOneAndUpdate(updaterFn, opts = {}) {
|
|
679
|
+
const { filter, sort } = deconstructFindAndModifyChain(this.#state.stages);
|
|
680
|
+
const update = resolveUpdaterResult(updaterFn(createFieldAccessor()));
|
|
681
|
+
const command = new FindOneAndUpdateCommand(this.#state.collection, filter, update, opts.upsert ?? false, sort, opts.returnDocument ?? "after");
|
|
682
|
+
const meta = {
|
|
683
|
+
target: "mongo",
|
|
684
|
+
storageHash: this.#state.storageHash,
|
|
685
|
+
lane: "mongo-query",
|
|
686
|
+
paramDescriptors: []
|
|
687
|
+
};
|
|
688
|
+
return {
|
|
689
|
+
collection: this.#state.collection,
|
|
690
|
+
command,
|
|
691
|
+
meta
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Find a single document matching the accumulated pipeline and delete it.
|
|
696
|
+
* Same marker gating and deconstruction as `findOneAndUpdate`.
|
|
697
|
+
*/
|
|
698
|
+
findOneAndDelete() {
|
|
699
|
+
const { filter, sort } = deconstructFindAndModifyChain(this.#state.stages);
|
|
700
|
+
const command = new FindOneAndDeleteCommand(this.#state.collection, filter, sort);
|
|
701
|
+
const meta = {
|
|
702
|
+
target: "mongo",
|
|
703
|
+
storageHash: this.#state.storageHash,
|
|
704
|
+
lane: "mongo-query",
|
|
705
|
+
paramDescriptors: []
|
|
706
|
+
};
|
|
707
|
+
return {
|
|
708
|
+
collection: this.#state.collection,
|
|
709
|
+
command,
|
|
710
|
+
meta
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Materialise the chain as a `MongoQueryPlan` wrapping an `AggregateCommand`.
|
|
715
|
+
*/
|
|
716
|
+
build() {
|
|
717
|
+
const command = new AggregateCommand(this.#state.collection, this.#state.stages);
|
|
718
|
+
const meta = {
|
|
719
|
+
target: "mongo",
|
|
720
|
+
storageHash: this.#state.storageHash,
|
|
721
|
+
lane: "mongo-query",
|
|
722
|
+
paramDescriptors: []
|
|
723
|
+
};
|
|
724
|
+
return {
|
|
725
|
+
collection: this.#state.collection,
|
|
726
|
+
command,
|
|
727
|
+
meta
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Alias for `build()` — surfaces the read intent at the call site.
|
|
732
|
+
*/
|
|
733
|
+
aggregate() {
|
|
734
|
+
return this.build();
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
/**
|
|
738
|
+
* Walk the accumulated pipeline stages and extract the `filter` and `sort`
|
|
739
|
+
* slots for a `findOneAndUpdate` / `findOneAndDelete` wire command.
|
|
740
|
+
*
|
|
741
|
+
* The helper accepts exactly the canonical shape `match+ -> sort?` and
|
|
742
|
+
* nothing else:
|
|
743
|
+
*
|
|
744
|
+
* - one or more `$match` stages (AND-folded into a single filter),
|
|
745
|
+
* - optionally followed by a single `$sort` stage.
|
|
746
|
+
*
|
|
747
|
+
* Anything else — a `$sort` before `$match`, multiple `$sort` stages, a
|
|
748
|
+
* `$skip` stage, or any non-`$match`/`$sort` stage — is rejected with a
|
|
749
|
+
* clear error. The type system already prevents most of these at compile
|
|
750
|
+
* time via the `FindAndModifyEnabled` marker, but the runtime check
|
|
751
|
+
* guards the escape hatches (e.g. `.pipe(...)`) and future marker gaps.
|
|
752
|
+
*
|
|
753
|
+
* `$skip` is rejected outright because MongoDB's `findAndModify` command
|
|
754
|
+
* has no skip slot; a silently-dropped skip is a latent correctness bug
|
|
755
|
+
* waiting to happen. (A02 removed skip from the typed AST for the same
|
|
756
|
+
* reason.)
|
|
757
|
+
*/
|
|
758
|
+
function deconstructFindAndModifyChain(stages) {
|
|
759
|
+
const matchFilters = [];
|
|
760
|
+
let sort;
|
|
761
|
+
let seenNonMatch = false;
|
|
762
|
+
for (const stage of stages) if (stage instanceof MongoMatchStage) {
|
|
763
|
+
if (seenNonMatch) throw new Error("findOneAndUpdate/findOneAndDelete requires the canonical $match+ -> $sort? shape, but a $match stage was found after a $sort. Re-order the chain so every .match() call precedes the .sort() call.");
|
|
764
|
+
matchFilters.push(stage.filter);
|
|
765
|
+
} else if (stage instanceof MongoSortStage) {
|
|
766
|
+
if (sort !== void 0) throw new Error("findOneAndUpdate/findOneAndDelete accepts at most one $sort stage; drop the extra .sort() call or combine the keys into a single sort spec.");
|
|
767
|
+
sort = { ...stage.sort };
|
|
768
|
+
seenNonMatch = true;
|
|
769
|
+
} else if (stage instanceof MongoSkipStage) throw new Error("findOneAndUpdate/findOneAndDelete does not support .skip() — MongoDB findAndModify has no skip slot. Remove the .skip() call, or use .aggregate()/.build() if the chain needs skip semantics.");
|
|
770
|
+
else throw new Error(`findOneAndUpdate/findOneAndDelete requires the canonical \$match+ -> \$sort? shape, but encountered a '${stage.constructor.name}' stage. This is likely a bug — the type system should have prevented this chain.`);
|
|
771
|
+
if (matchFilters.length === 0) throw new Error("findOneAndUpdate/findOneAndDelete requires at least one .match() call.");
|
|
772
|
+
const first = matchFilters[0];
|
|
773
|
+
if (first === void 0) throw new Error("Unreachable: matchFilters.length > 0 but first is undefined");
|
|
774
|
+
return {
|
|
775
|
+
filter: matchFilters.length === 1 ? first : MongoAndExpr.of(matchFilters),
|
|
776
|
+
sort
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Walk the accumulated pipeline stages: leading `$match` stages become the
|
|
781
|
+
* filter, remaining stages must all be valid `MongoUpdatePipelineStage`
|
|
782
|
+
* members (currently `$addFields`, `$project`, `$replaceRoot`).
|
|
783
|
+
*/
|
|
784
|
+
function deconstructUpdateChain(stages) {
|
|
785
|
+
const matchFilters = [];
|
|
786
|
+
let boundary = 0;
|
|
787
|
+
for (const stage of stages) {
|
|
788
|
+
if (!(stage instanceof MongoMatchStage)) break;
|
|
789
|
+
matchFilters.push(stage.filter);
|
|
790
|
+
boundary++;
|
|
791
|
+
}
|
|
792
|
+
if (matchFilters.length === 0) throw new Error("No-arg updateMany/updateOne requires at least one .match() call.");
|
|
793
|
+
const remaining = stages.slice(boundary);
|
|
794
|
+
if (remaining.length === 0) throw new Error("No-arg updateMany/updateOne requires at least one pipeline-update stage (e.g. .addFields(), .project(), .replaceRoot()) after the .match() stages.");
|
|
795
|
+
const updatePipeline = [];
|
|
796
|
+
for (const stage of remaining) if (stage instanceof MongoAddFieldsStage || stage instanceof MongoProjectStage || stage instanceof MongoReplaceRootStage) updatePipeline.push(stage);
|
|
797
|
+
else throw new Error(`No-arg updateMany/updateOne: encountered non-update stage '${stage.constructor.name}' after the leading \$match stages. Only \$addFields/\$set, \$project/\$unset, and \$replaceRoot/\$replaceWith stages are valid in an update pipeline.`);
|
|
798
|
+
const first = matchFilters[0];
|
|
799
|
+
if (first === void 0) throw new Error("Unreachable: matchFilters.length > 0 but first is undefined");
|
|
800
|
+
return {
|
|
801
|
+
filter: matchFilters.length === 1 ? first : MongoAndExpr.of(matchFilters),
|
|
802
|
+
updatePipeline
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
//#endregion
|
|
807
|
+
//#region src/expression-helpers.ts
|
|
808
|
+
function numericExpr(op, args) {
|
|
809
|
+
return {
|
|
810
|
+
_field: {
|
|
811
|
+
codecId: "mongo/double@1",
|
|
812
|
+
nullable: false
|
|
813
|
+
},
|
|
814
|
+
node: MongoAggOperator.of(op, args.map((a) => a.node))
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
function numericUnaryExpr(op, arg) {
|
|
818
|
+
return {
|
|
819
|
+
_field: {
|
|
820
|
+
codecId: "mongo/double@1",
|
|
821
|
+
nullable: false
|
|
822
|
+
},
|
|
823
|
+
node: MongoAggOperator.of(op, arg.node)
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
function stringExpr(op, args) {
|
|
827
|
+
return {
|
|
828
|
+
_field: {
|
|
829
|
+
codecId: "mongo/string@1",
|
|
830
|
+
nullable: false
|
|
831
|
+
},
|
|
832
|
+
node: MongoAggOperator.of(op, args.map((a) => a.node))
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
function stringUnaryExpr(op, arg) {
|
|
836
|
+
return {
|
|
837
|
+
_field: {
|
|
838
|
+
codecId: "mongo/string@1",
|
|
839
|
+
nullable: false
|
|
840
|
+
},
|
|
841
|
+
node: MongoAggOperator.of(op, arg.node)
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
function booleanExpr(op, args) {
|
|
845
|
+
return {
|
|
846
|
+
_field: {
|
|
847
|
+
codecId: "mongo/bool@1",
|
|
848
|
+
nullable: false
|
|
849
|
+
},
|
|
850
|
+
node: MongoAggOperator.of(op, args.map((a) => a.node))
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
function booleanUnaryExpr(op, arg) {
|
|
854
|
+
return {
|
|
855
|
+
_field: {
|
|
856
|
+
codecId: "mongo/bool@1",
|
|
857
|
+
nullable: false
|
|
858
|
+
},
|
|
859
|
+
node: MongoAggOperator.of(op, arg.node)
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
function dateUnaryExpr(op, arg) {
|
|
863
|
+
return {
|
|
864
|
+
_field: {
|
|
865
|
+
codecId: "mongo/date@1",
|
|
866
|
+
nullable: false
|
|
867
|
+
},
|
|
868
|
+
node: MongoAggOperator.of(op, arg.node)
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
function arrayExpr(op, args) {
|
|
872
|
+
return {
|
|
873
|
+
_field: {
|
|
874
|
+
codecId: "mongo/array@1",
|
|
875
|
+
nullable: false
|
|
876
|
+
},
|
|
877
|
+
node: MongoAggOperator.of(op, args.map((a) => a.node))
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
function arrayUnaryExpr(op, arg) {
|
|
881
|
+
return {
|
|
882
|
+
_field: {
|
|
883
|
+
codecId: "mongo/array@1",
|
|
884
|
+
nullable: false
|
|
885
|
+
},
|
|
886
|
+
node: MongoAggOperator.of(op, arg.node)
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
function docUnaryExpr(op, arg) {
|
|
890
|
+
return {
|
|
891
|
+
_field: {
|
|
892
|
+
codecId: arg._field.codecId,
|
|
893
|
+
nullable: false
|
|
894
|
+
},
|
|
895
|
+
node: MongoAggOperator.of(op, arg.node)
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
function namedArgsExpr(op, args, _field) {
|
|
899
|
+
const nodeArgs = {};
|
|
900
|
+
for (const [key, val] of Object.entries(args)) if (val !== void 0) nodeArgs[key] = val.node;
|
|
901
|
+
return {
|
|
902
|
+
_field,
|
|
903
|
+
node: MongoAggOperator.of(op, nodeArgs)
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
const NUMERIC = {
|
|
907
|
+
codecId: "mongo/double@1",
|
|
908
|
+
nullable: false
|
|
909
|
+
};
|
|
910
|
+
const STRING = {
|
|
911
|
+
codecId: "mongo/string@1",
|
|
912
|
+
nullable: false
|
|
913
|
+
};
|
|
914
|
+
const BOOLEAN = {
|
|
915
|
+
codecId: "mongo/bool@1",
|
|
916
|
+
nullable: false
|
|
917
|
+
};
|
|
918
|
+
const DATE = {
|
|
919
|
+
codecId: "mongo/date@1",
|
|
920
|
+
nullable: false
|
|
921
|
+
};
|
|
922
|
+
const ARRAY = {
|
|
923
|
+
codecId: "mongo/array@1",
|
|
924
|
+
nullable: false
|
|
925
|
+
};
|
|
926
|
+
const DOC = {
|
|
927
|
+
codecId: "mongo/document@1",
|
|
928
|
+
nullable: false
|
|
929
|
+
};
|
|
930
|
+
function literal(value) {
|
|
931
|
+
return {
|
|
932
|
+
_field: void 0,
|
|
933
|
+
node: MongoAggLiteral.of(value)
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
const fn = {
|
|
937
|
+
add(...args) {
|
|
938
|
+
return numericExpr("$add", args);
|
|
939
|
+
},
|
|
940
|
+
subtract(a, b) {
|
|
941
|
+
return numericExpr("$subtract", [a, b]);
|
|
942
|
+
},
|
|
943
|
+
multiply(...args) {
|
|
944
|
+
return numericExpr("$multiply", args);
|
|
945
|
+
},
|
|
946
|
+
divide(a, b) {
|
|
947
|
+
return numericExpr("$divide", [a, b]);
|
|
948
|
+
},
|
|
949
|
+
concat(...args) {
|
|
950
|
+
return stringExpr("$concat", args);
|
|
951
|
+
},
|
|
952
|
+
toLower(a) {
|
|
953
|
+
return stringUnaryExpr("$toLower", a);
|
|
954
|
+
},
|
|
955
|
+
toUpper(a) {
|
|
956
|
+
return stringUnaryExpr("$toUpper", a);
|
|
957
|
+
},
|
|
958
|
+
size(a) {
|
|
959
|
+
return numericUnaryExpr("$size", a);
|
|
960
|
+
},
|
|
961
|
+
cond(condition, thenExpr, elseExpr) {
|
|
962
|
+
return {
|
|
963
|
+
_field: thenExpr._field,
|
|
964
|
+
node: new MongoAggCond(condition, thenExpr.node, elseExpr.node)
|
|
965
|
+
};
|
|
966
|
+
},
|
|
967
|
+
literal,
|
|
968
|
+
year(a) {
|
|
969
|
+
return numericUnaryExpr("$year", a);
|
|
970
|
+
},
|
|
971
|
+
month(a) {
|
|
972
|
+
return numericUnaryExpr("$month", a);
|
|
973
|
+
},
|
|
974
|
+
dayOfMonth(a) {
|
|
975
|
+
return numericUnaryExpr("$dayOfMonth", a);
|
|
976
|
+
},
|
|
977
|
+
hour(a) {
|
|
978
|
+
return numericUnaryExpr("$hour", a);
|
|
979
|
+
},
|
|
980
|
+
minute(a) {
|
|
981
|
+
return numericUnaryExpr("$minute", a);
|
|
982
|
+
},
|
|
983
|
+
second(a) {
|
|
984
|
+
return numericUnaryExpr("$second", a);
|
|
985
|
+
},
|
|
986
|
+
millisecond(a) {
|
|
987
|
+
return numericUnaryExpr("$millisecond", a);
|
|
988
|
+
},
|
|
989
|
+
dateToString(args) {
|
|
990
|
+
return namedArgsExpr("$dateToString", args, STRING);
|
|
991
|
+
},
|
|
992
|
+
dateFromString(args) {
|
|
993
|
+
return namedArgsExpr("$dateFromString", args, DATE);
|
|
994
|
+
},
|
|
995
|
+
dateDiff(args) {
|
|
996
|
+
return namedArgsExpr("$dateDiff", args, NUMERIC);
|
|
997
|
+
},
|
|
998
|
+
dateAdd(args) {
|
|
999
|
+
return namedArgsExpr("$dateAdd", args, DATE);
|
|
1000
|
+
},
|
|
1001
|
+
dateSubtract(args) {
|
|
1002
|
+
return namedArgsExpr("$dateSubtract", args, DATE);
|
|
1003
|
+
},
|
|
1004
|
+
dateTrunc(args) {
|
|
1005
|
+
return namedArgsExpr("$dateTrunc", args, DATE);
|
|
1006
|
+
},
|
|
1007
|
+
substr(str, start, length) {
|
|
1008
|
+
return stringExpr("$substr", [
|
|
1009
|
+
str,
|
|
1010
|
+
start,
|
|
1011
|
+
length
|
|
1012
|
+
]);
|
|
1013
|
+
},
|
|
1014
|
+
substrBytes(str, start, count) {
|
|
1015
|
+
return stringExpr("$substrBytes", [
|
|
1016
|
+
str,
|
|
1017
|
+
start,
|
|
1018
|
+
count
|
|
1019
|
+
]);
|
|
1020
|
+
},
|
|
1021
|
+
trim(args) {
|
|
1022
|
+
return namedArgsExpr("$trim", args, STRING);
|
|
1023
|
+
},
|
|
1024
|
+
ltrim(args) {
|
|
1025
|
+
return namedArgsExpr("$ltrim", args, STRING);
|
|
1026
|
+
},
|
|
1027
|
+
rtrim(args) {
|
|
1028
|
+
return namedArgsExpr("$rtrim", args, STRING);
|
|
1029
|
+
},
|
|
1030
|
+
split(str, delimiter) {
|
|
1031
|
+
return arrayExpr("$split", [str, delimiter]);
|
|
1032
|
+
},
|
|
1033
|
+
strLenCP(a) {
|
|
1034
|
+
return numericUnaryExpr("$strLenCP", a);
|
|
1035
|
+
},
|
|
1036
|
+
strLenBytes(a) {
|
|
1037
|
+
return numericUnaryExpr("$strLenBytes", a);
|
|
1038
|
+
},
|
|
1039
|
+
regexMatch(args) {
|
|
1040
|
+
return namedArgsExpr("$regexMatch", args, BOOLEAN);
|
|
1041
|
+
},
|
|
1042
|
+
regexFind(args) {
|
|
1043
|
+
return namedArgsExpr("$regexFind", args, DOC);
|
|
1044
|
+
},
|
|
1045
|
+
regexFindAll(args) {
|
|
1046
|
+
return namedArgsExpr("$regexFindAll", args, ARRAY);
|
|
1047
|
+
},
|
|
1048
|
+
replaceOne(args) {
|
|
1049
|
+
return namedArgsExpr("$replaceOne", args, STRING);
|
|
1050
|
+
},
|
|
1051
|
+
replaceAll(args) {
|
|
1052
|
+
return namedArgsExpr("$replaceAll", args, STRING);
|
|
1053
|
+
},
|
|
1054
|
+
cmp(a, b) {
|
|
1055
|
+
return numericExpr("$cmp", [a, b]);
|
|
1056
|
+
},
|
|
1057
|
+
eq(a, b) {
|
|
1058
|
+
return booleanExpr("$eq", [a, b]);
|
|
1059
|
+
},
|
|
1060
|
+
ne(a, b) {
|
|
1061
|
+
return booleanExpr("$ne", [a, b]);
|
|
1062
|
+
},
|
|
1063
|
+
gt(a, b) {
|
|
1064
|
+
return booleanExpr("$gt", [a, b]);
|
|
1065
|
+
},
|
|
1066
|
+
gte(a, b) {
|
|
1067
|
+
return booleanExpr("$gte", [a, b]);
|
|
1068
|
+
},
|
|
1069
|
+
lt(a, b) {
|
|
1070
|
+
return booleanExpr("$lt", [a, b]);
|
|
1071
|
+
},
|
|
1072
|
+
lte(a, b) {
|
|
1073
|
+
return booleanExpr("$lte", [a, b]);
|
|
1074
|
+
},
|
|
1075
|
+
arrayElemAt(arr, idx) {
|
|
1076
|
+
return {
|
|
1077
|
+
_field: {
|
|
1078
|
+
codecId: DOC.codecId,
|
|
1079
|
+
nullable: true
|
|
1080
|
+
},
|
|
1081
|
+
node: MongoAggOperator.of("$arrayElemAt", [arr.node, idx.node])
|
|
1082
|
+
};
|
|
1083
|
+
},
|
|
1084
|
+
concatArrays(...args) {
|
|
1085
|
+
return arrayExpr("$concatArrays", args);
|
|
1086
|
+
},
|
|
1087
|
+
firstElem(a) {
|
|
1088
|
+
return {
|
|
1089
|
+
_field: {
|
|
1090
|
+
codecId: DOC.codecId,
|
|
1091
|
+
nullable: true
|
|
1092
|
+
},
|
|
1093
|
+
node: MongoAggOperator.of("$first", a.node)
|
|
1094
|
+
};
|
|
1095
|
+
},
|
|
1096
|
+
lastElem(a) {
|
|
1097
|
+
return {
|
|
1098
|
+
_field: {
|
|
1099
|
+
codecId: DOC.codecId,
|
|
1100
|
+
nullable: true
|
|
1101
|
+
},
|
|
1102
|
+
node: MongoAggOperator.of("$last", a.node)
|
|
1103
|
+
};
|
|
1104
|
+
},
|
|
1105
|
+
isIn(elem, arr) {
|
|
1106
|
+
return booleanExpr("$in", [elem, arr]);
|
|
1107
|
+
},
|
|
1108
|
+
indexOfArray(arr, value, ...rest) {
|
|
1109
|
+
return numericExpr("$indexOfArray", [
|
|
1110
|
+
arr,
|
|
1111
|
+
value,
|
|
1112
|
+
...rest
|
|
1113
|
+
]);
|
|
1114
|
+
},
|
|
1115
|
+
isArray(a) {
|
|
1116
|
+
return booleanUnaryExpr("$isArray", a);
|
|
1117
|
+
},
|
|
1118
|
+
reverseArray(a) {
|
|
1119
|
+
return arrayUnaryExpr("$reverseArray", a);
|
|
1120
|
+
},
|
|
1121
|
+
slice(arr, ...rest) {
|
|
1122
|
+
return arrayExpr("$slice", [arr, ...rest]);
|
|
1123
|
+
},
|
|
1124
|
+
zip(args) {
|
|
1125
|
+
const nodeArgs = { inputs: args.inputs.map((a) => a.node) };
|
|
1126
|
+
if (args.useLongestLength) nodeArgs["useLongestLength"] = args.useLongestLength.node;
|
|
1127
|
+
if (args.defaults) nodeArgs["defaults"] = args.defaults.node;
|
|
1128
|
+
return {
|
|
1129
|
+
_field: ARRAY,
|
|
1130
|
+
node: MongoAggOperator.of("$zip", nodeArgs)
|
|
1131
|
+
};
|
|
1132
|
+
},
|
|
1133
|
+
range(start, end, step) {
|
|
1134
|
+
return arrayExpr("$range", [
|
|
1135
|
+
start,
|
|
1136
|
+
end,
|
|
1137
|
+
step
|
|
1138
|
+
]);
|
|
1139
|
+
},
|
|
1140
|
+
setUnion(...args) {
|
|
1141
|
+
return arrayExpr("$setUnion", args);
|
|
1142
|
+
},
|
|
1143
|
+
setIntersection(...args) {
|
|
1144
|
+
return arrayExpr("$setIntersection", args);
|
|
1145
|
+
},
|
|
1146
|
+
setDifference(a, b) {
|
|
1147
|
+
return arrayExpr("$setDifference", [a, b]);
|
|
1148
|
+
},
|
|
1149
|
+
setEquals(...args) {
|
|
1150
|
+
return booleanExpr("$setEquals", args);
|
|
1151
|
+
},
|
|
1152
|
+
setIsSubset(a, b) {
|
|
1153
|
+
return booleanExpr("$setIsSubset", [a, b]);
|
|
1154
|
+
},
|
|
1155
|
+
anyElementTrue(a) {
|
|
1156
|
+
return booleanUnaryExpr("$anyElementTrue", a);
|
|
1157
|
+
},
|
|
1158
|
+
allElementsTrue(a) {
|
|
1159
|
+
return booleanUnaryExpr("$allElementsTrue", a);
|
|
1160
|
+
},
|
|
1161
|
+
typeOf(a) {
|
|
1162
|
+
return stringUnaryExpr("$type", a);
|
|
1163
|
+
},
|
|
1164
|
+
convert(args) {
|
|
1165
|
+
return namedArgsExpr("$convert", args, DOC);
|
|
1166
|
+
},
|
|
1167
|
+
toInt(a) {
|
|
1168
|
+
return numericUnaryExpr("$toInt", a);
|
|
1169
|
+
},
|
|
1170
|
+
toLong(a) {
|
|
1171
|
+
return numericUnaryExpr("$toLong", a);
|
|
1172
|
+
},
|
|
1173
|
+
toDouble(a) {
|
|
1174
|
+
return numericUnaryExpr("$toDouble", a);
|
|
1175
|
+
},
|
|
1176
|
+
toDecimal(a) {
|
|
1177
|
+
return numericUnaryExpr("$toDecimal", a);
|
|
1178
|
+
},
|
|
1179
|
+
toString_(a) {
|
|
1180
|
+
return stringUnaryExpr("$toString", a);
|
|
1181
|
+
},
|
|
1182
|
+
toObjectId(a) {
|
|
1183
|
+
return docUnaryExpr("$toObjectId", a);
|
|
1184
|
+
},
|
|
1185
|
+
toBool(a) {
|
|
1186
|
+
return booleanUnaryExpr("$toBool", a);
|
|
1187
|
+
},
|
|
1188
|
+
toDate(a) {
|
|
1189
|
+
return dateUnaryExpr("$toDate", a);
|
|
1190
|
+
},
|
|
1191
|
+
objectToArray(a) {
|
|
1192
|
+
return arrayUnaryExpr("$objectToArray", a);
|
|
1193
|
+
},
|
|
1194
|
+
arrayToObject(a) {
|
|
1195
|
+
return {
|
|
1196
|
+
_field: DOC,
|
|
1197
|
+
node: MongoAggOperator.of("$arrayToObject", a.node)
|
|
1198
|
+
};
|
|
1199
|
+
},
|
|
1200
|
+
getField(args) {
|
|
1201
|
+
return namedArgsExpr("$getField", args, DOC);
|
|
1202
|
+
},
|
|
1203
|
+
setField(args) {
|
|
1204
|
+
return namedArgsExpr("$setField", args, DOC);
|
|
1205
|
+
}
|
|
1206
|
+
};
|
|
1207
|
+
|
|
1208
|
+
//#endregion
|
|
1209
|
+
//#region src/state-classes.ts
|
|
1210
|
+
/**
|
|
1211
|
+
* "Match-all" filter used by the unqualified-write terminals
|
|
1212
|
+
* (`updateAll`/`deleteAll`). The canonical representation is still
|
|
1213
|
+
* undecided — `MongoAndExpr` with an empty conjunction and a dedicated
|
|
1214
|
+
* `MongoMatchAllExpr` node are both candidates. For now we use
|
|
1215
|
+
* `_id $exists: true`, which is trivially true on every document and
|
|
1216
|
+
* avoids introducing a new AST node before the wider question is
|
|
1217
|
+
* resolved. Centralised so the eventual switch is a one-line change.
|
|
1218
|
+
*/
|
|
1219
|
+
function matchAllFilter() {
|
|
1220
|
+
return MongoExistsExpr.exists("_id");
|
|
1221
|
+
}
|
|
1222
|
+
/**
|
|
1223
|
+
* Resolve an updater callback into a `MongoUpdateSpec` (either the folded
|
|
1224
|
+
* operator object or a pipeline-stage array). Centralised so all write
|
|
1225
|
+
* terminals share the same fold / dispatch semantics.
|
|
1226
|
+
*/
|
|
1227
|
+
function resolveUpdaterCallback(updaterFn) {
|
|
1228
|
+
return resolveUpdaterResult(updaterFn(createFieldAccessor()));
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Build the `PlanMeta` envelope shared by every write terminal in this
|
|
1232
|
+
* package. Lane is `mongo-query` (single lane for all query-builder terminals)
|
|
1233
|
+
* so middleware can dispatch on intent without inspecting the command.
|
|
1234
|
+
*/
|
|
1235
|
+
function writeMeta(storageHash) {
|
|
1236
|
+
return {
|
|
1237
|
+
target: "mongo",
|
|
1238
|
+
storageHash,
|
|
1239
|
+
lane: "mongo-query",
|
|
1240
|
+
paramDescriptors: []
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Root state of the query-builder state machine. Returned from
|
|
1245
|
+
* `mongoQuery(...).from(name)` and bound to a single collection.
|
|
1246
|
+
*
|
|
1247
|
+
* Inherits the entire pipeline-stage surface from `PipelineChain` (since an
|
|
1248
|
+
* empty `CollectionHandle` is observably an empty pipeline). Adds:
|
|
1249
|
+
*
|
|
1250
|
+
* - `match(...)` — overridden to transition to `FilteredCollection`, which
|
|
1251
|
+
* accumulates filters for eventual splatting into write/find-and-modify
|
|
1252
|
+
* wire commands.
|
|
1253
|
+
* - **Insert / unqualified-write methods** (M2): `insertOne`, `insertMany`,
|
|
1254
|
+
* `updateAll`, `deleteAll`. These live *only* here — the corresponding
|
|
1255
|
+
* methods are absent from `FilteredCollection`, so a caller cannot
|
|
1256
|
+
* accidentally produce an unqualified write by forgetting to `.match(...)`
|
|
1257
|
+
* later in the chain. Bodies land in M2.
|
|
1258
|
+
*/
|
|
1259
|
+
var CollectionHandle = class extends PipelineChain {
|
|
1260
|
+
#ctx;
|
|
1261
|
+
#modelName;
|
|
1262
|
+
constructor(ctx, modelName) {
|
|
1263
|
+
super(ctx.contract, {
|
|
1264
|
+
collection: ctx.collection,
|
|
1265
|
+
stages: [],
|
|
1266
|
+
storageHash: ctx.storageHash
|
|
1267
|
+
});
|
|
1268
|
+
this.#ctx = ctx;
|
|
1269
|
+
this.#modelName = modelName;
|
|
1270
|
+
}
|
|
1271
|
+
/**
|
|
1272
|
+
* Bound model name. Exposed so type tests can assert the binding without
|
|
1273
|
+
* flipping into a pipeline. Not part of the public-API contract.
|
|
1274
|
+
*/
|
|
1275
|
+
get _modelName() {
|
|
1276
|
+
return this.#modelName;
|
|
1277
|
+
}
|
|
1278
|
+
match(filterOrFn) {
|
|
1279
|
+
const resolved = typeof filterOrFn === "function" ? filterOrFn(createFieldAccessor()) : filterOrFn;
|
|
1280
|
+
return new FilteredCollection(this.#ctx, this.#modelName, [resolved]);
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Insert a single document. Document fields are passed straight through to
|
|
1284
|
+
* the wire `InsertOneCommand` — codec normalisation happens at the
|
|
1285
|
+
* adapter/driver boundary, identically to the SQL builder (see Open Item
|
|
1286
|
+
* #14 confirmation in the design conversation).
|
|
1287
|
+
*
|
|
1288
|
+
* Returns a `MongoQueryPlan<InsertOneResult>` whose row stream yields a
|
|
1289
|
+
* single result document with the server-assigned `insertedId`.
|
|
1290
|
+
*/
|
|
1291
|
+
insertOne(document) {
|
|
1292
|
+
const command = new InsertOneCommand(this.#ctx.collection, document);
|
|
1293
|
+
return {
|
|
1294
|
+
collection: this.#ctx.collection,
|
|
1295
|
+
command,
|
|
1296
|
+
meta: writeMeta(this.#ctx.storageHash)
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
/**
|
|
1300
|
+
* Insert a batch of documents. Order is preserved in the returned
|
|
1301
|
+
* `insertedIds` array.
|
|
1302
|
+
*/
|
|
1303
|
+
insertMany(documents) {
|
|
1304
|
+
if (documents.length === 0) throw new Error("insertMany() requires at least one document.");
|
|
1305
|
+
const command = new InsertManyCommand(this.#ctx.collection, documents);
|
|
1306
|
+
return {
|
|
1307
|
+
collection: this.#ctx.collection,
|
|
1308
|
+
command,
|
|
1309
|
+
meta: writeMeta(this.#ctx.storageHash)
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* Update *every* document in the collection. Lives only on
|
|
1314
|
+
* `CollectionHandle` — the corresponding method is intentionally absent
|
|
1315
|
+
* from `FilteredCollection` so a caller cannot accidentally produce an
|
|
1316
|
+
* unqualified write by forgetting to `.match(...)` first. Pair with
|
|
1317
|
+
* `.match(...).updateMany(...)` for the filtered case.
|
|
1318
|
+
*/
|
|
1319
|
+
updateAll(updaterFn) {
|
|
1320
|
+
const update = resolveUpdaterCallback(updaterFn);
|
|
1321
|
+
const command = new UpdateManyCommand(this.#ctx.collection, matchAllFilter(), update);
|
|
1322
|
+
return {
|
|
1323
|
+
collection: this.#ctx.collection,
|
|
1324
|
+
command,
|
|
1325
|
+
meta: writeMeta(this.#ctx.storageHash)
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
/**
|
|
1329
|
+
* Delete *every* document in the collection. See `updateAll` for the
|
|
1330
|
+
* rationale around the unqualified-write surface being limited to this
|
|
1331
|
+
* state class.
|
|
1332
|
+
*/
|
|
1333
|
+
deleteAll() {
|
|
1334
|
+
const command = new DeleteManyCommand(this.#ctx.collection, matchAllFilter());
|
|
1335
|
+
return {
|
|
1336
|
+
collection: this.#ctx.collection,
|
|
1337
|
+
command,
|
|
1338
|
+
meta: writeMeta(this.#ctx.storageHash)
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
/**
|
|
1342
|
+
* Insert-or-update the document matching `filterFn`. The filter is
|
|
1343
|
+
* mandatory (vs. `updateAll`'s tautological match) because an upsert
|
|
1344
|
+
* without a discriminating predicate would either match every existing
|
|
1345
|
+
* document or insert an indistinguishable new one.
|
|
1346
|
+
*
|
|
1347
|
+
* Maps to `UpdateOneCommand` with `upsert: true`. The driver inserts a
|
|
1348
|
+
* new document derived from the filter equality fields plus the update
|
|
1349
|
+
* spec when no match is found; otherwise updates the matched document.
|
|
1350
|
+
*/
|
|
1351
|
+
upsertOne(filterFn, updaterFn) {
|
|
1352
|
+
const filter = filterFn(createFieldAccessor());
|
|
1353
|
+
const update = resolveUpdaterCallback(updaterFn);
|
|
1354
|
+
const command = new UpdateOneCommand(this.#ctx.collection, filter, update, true);
|
|
1355
|
+
return {
|
|
1356
|
+
collection: this.#ctx.collection,
|
|
1357
|
+
command,
|
|
1358
|
+
meta: writeMeta(this.#ctx.storageHash)
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
};
|
|
1362
|
+
/**
|
|
1363
|
+
* State reached after one or more `.match(...)` calls on `CollectionHandle`.
|
|
1364
|
+
*
|
|
1365
|
+
* Inherits the pipeline-stage surface from `PipelineChain`, with the
|
|
1366
|
+
* accumulated filters baked in as a leading `$match` stage on the underlying
|
|
1367
|
+
* pipeline state. This means read-terminal output (`.aggregate()` /
|
|
1368
|
+
* `.build()`) and any subsequent pipeline-stage chain see the filtered
|
|
1369
|
+
* collection as input — the read story works through pure inheritance.
|
|
1370
|
+
*
|
|
1371
|
+
* Adds:
|
|
1372
|
+
*
|
|
1373
|
+
* - `match(...)` — pushes another `$match` stage *and* records the filter in
|
|
1374
|
+
* the accumulator, so the eventual write/find-and-modify terminal can
|
|
1375
|
+
* splat the AND-folded filter into the wire command's `filter` slot.
|
|
1376
|
+
* - **Filtered writes** (M2): `updateMany`, `updateOne`, `deleteMany`,
|
|
1377
|
+
* `deleteOne`, `upsertOne`. Stubbed in M1. (Upsert-many is an open
|
|
1378
|
+
* question in the spec — see TML-2267 — and is intentionally absent.)
|
|
1379
|
+
* - **Find-and-modify** (M3): `findOneAndUpdate`, `findOneAndDelete`.
|
|
1380
|
+
* Stubbed in M1.
|
|
1381
|
+
*
|
|
1382
|
+
* Notably *does not* expose `insertOne`/`insertMany`/`updateAll`/`deleteAll`
|
|
1383
|
+
* — those are insert or unqualified-write operations that are nonsense
|
|
1384
|
+
* after a filter has been applied.
|
|
1385
|
+
*/
|
|
1386
|
+
var FilteredCollection = class FilteredCollection extends PipelineChain {
|
|
1387
|
+
#ctx;
|
|
1388
|
+
#modelName;
|
|
1389
|
+
#filters;
|
|
1390
|
+
constructor(ctx, modelName, filters) {
|
|
1391
|
+
if (filters.length === 0) throw new Error("FilteredCollection requires at least one accumulated filter");
|
|
1392
|
+
const first = filters[0];
|
|
1393
|
+
if (first === void 0) throw new Error("FilteredCollection: unreachable empty-filters branch");
|
|
1394
|
+
const leading = filters.length === 1 ? first : foldAnd(filters);
|
|
1395
|
+
super(ctx.contract, {
|
|
1396
|
+
collection: ctx.collection,
|
|
1397
|
+
stages: [new MongoMatchStage(leading)],
|
|
1398
|
+
storageHash: ctx.storageHash
|
|
1399
|
+
});
|
|
1400
|
+
this.#ctx = ctx;
|
|
1401
|
+
this.#modelName = modelName;
|
|
1402
|
+
this.#filters = filters;
|
|
1403
|
+
}
|
|
1404
|
+
get _modelName() {
|
|
1405
|
+
return this.#modelName;
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* Accumulated filter list. Exposed for the M2/M3 write/find-and-modify
|
|
1409
|
+
* terminals to splat into wire-command `filter` slots; not part of the
|
|
1410
|
+
* public-API contract.
|
|
1411
|
+
*/
|
|
1412
|
+
get _filters() {
|
|
1413
|
+
return this.#filters;
|
|
1414
|
+
}
|
|
1415
|
+
match(filterOrFn) {
|
|
1416
|
+
const resolved = typeof filterOrFn === "function" ? filterOrFn(createFieldAccessor()) : filterOrFn;
|
|
1417
|
+
return new FilteredCollection(this.#ctx, this.#modelName, [...this.#filters, resolved]);
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* AND-fold the accumulated filters into a single `MongoFilterExpr` for
|
|
1421
|
+
* splatting into a write/find-and-modify wire command's `filter` slot.
|
|
1422
|
+
* Length-1 short-circuits to avoid a redundant `$and` wrapper.
|
|
1423
|
+
*/
|
|
1424
|
+
#foldedFilter() {
|
|
1425
|
+
const first = this.#filters[0];
|
|
1426
|
+
if (first === void 0) throw new Error("FilteredCollection: invariant violated — empty filter accumulator");
|
|
1427
|
+
return this.#filters.length === 1 ? first : foldAnd(this.#filters);
|
|
1428
|
+
}
|
|
1429
|
+
/**
|
|
1430
|
+
* Update every matching document. `updaterFn` receives a `FieldAccessor`
|
|
1431
|
+
* and returns an array of `TypedUpdateOp` (e.g. `[f.amount.inc(1),
|
|
1432
|
+
* f.status.set('done')]`). Operators are folded into the wire-format
|
|
1433
|
+
* update spec by `foldUpdateOps`, which throws on operator+path
|
|
1434
|
+
* collisions.
|
|
1435
|
+
*/
|
|
1436
|
+
updateMany(updaterFn) {
|
|
1437
|
+
const update = resolveUpdaterCallback(updaterFn);
|
|
1438
|
+
const command = new UpdateManyCommand(this.#ctx.collection, this.#foldedFilter(), update);
|
|
1439
|
+
return {
|
|
1440
|
+
collection: this.#ctx.collection,
|
|
1441
|
+
command,
|
|
1442
|
+
meta: writeMeta(this.#ctx.storageHash)
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* Update at most one matching document. The driver picks the document
|
|
1447
|
+
* (typically the first one matched by the underlying scan); no ordering
|
|
1448
|
+
* guarantee is implied — chain `.sort(...)` and use the M3
|
|
1449
|
+
* `.findOneAndUpdate(...)` terminal when ordering matters.
|
|
1450
|
+
*/
|
|
1451
|
+
updateOne(updaterFn) {
|
|
1452
|
+
const update = resolveUpdaterCallback(updaterFn);
|
|
1453
|
+
const command = new UpdateOneCommand(this.#ctx.collection, this.#foldedFilter(), update);
|
|
1454
|
+
return {
|
|
1455
|
+
collection: this.#ctx.collection,
|
|
1456
|
+
command,
|
|
1457
|
+
meta: writeMeta(this.#ctx.storageHash)
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
/**
|
|
1461
|
+
* Delete every matching document.
|
|
1462
|
+
*/
|
|
1463
|
+
deleteMany() {
|
|
1464
|
+
const command = new DeleteManyCommand(this.#ctx.collection, this.#foldedFilter());
|
|
1465
|
+
return {
|
|
1466
|
+
collection: this.#ctx.collection,
|
|
1467
|
+
command,
|
|
1468
|
+
meta: writeMeta(this.#ctx.storageHash)
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* Delete at most one matching document. See the `updateOne` note about
|
|
1473
|
+
* driver-chosen victim selection.
|
|
1474
|
+
*/
|
|
1475
|
+
deleteOne() {
|
|
1476
|
+
const command = new DeleteOneCommand(this.#ctx.collection, this.#foldedFilter());
|
|
1477
|
+
return {
|
|
1478
|
+
collection: this.#ctx.collection,
|
|
1479
|
+
command,
|
|
1480
|
+
meta: writeMeta(this.#ctx.storageHash)
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
/**
|
|
1484
|
+
* Insert-or-update against the accumulated filter. Maps to
|
|
1485
|
+
* `UpdateOneCommand` with `upsert: true`. Equivalent to
|
|
1486
|
+
* `CollectionHandle.upsertOne(f => filter, updaterFn)` but reuses the
|
|
1487
|
+
* already-accumulated `.match(...)` filter chain.
|
|
1488
|
+
*/
|
|
1489
|
+
upsertOne(updaterFn) {
|
|
1490
|
+
const update = resolveUpdaterCallback(updaterFn);
|
|
1491
|
+
const command = new UpdateOneCommand(this.#ctx.collection, this.#foldedFilter(), update, true);
|
|
1492
|
+
return {
|
|
1493
|
+
collection: this.#ctx.collection,
|
|
1494
|
+
command,
|
|
1495
|
+
meta: writeMeta(this.#ctx.storageHash)
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1498
|
+
/**
|
|
1499
|
+
* Find a single matching document and apply `updaterFn` to it.
|
|
1500
|
+
*
|
|
1501
|
+
* `opts.upsert` (default `false`) toggles insert-on-miss behaviour.
|
|
1502
|
+
* `opts.returnDocument` (default `'after'`) controls whether the row
|
|
1503
|
+
* stream yields the document as it was before or after the update.
|
|
1504
|
+
*/
|
|
1505
|
+
findOneAndUpdate(updaterFn, opts = {}) {
|
|
1506
|
+
const update = resolveUpdaterCallback(updaterFn);
|
|
1507
|
+
const command = new FindOneAndUpdateCommand(this.#ctx.collection, this.#foldedFilter(), update, opts.upsert ?? false, void 0, opts.returnDocument ?? "after");
|
|
1508
|
+
return {
|
|
1509
|
+
collection: this.#ctx.collection,
|
|
1510
|
+
command,
|
|
1511
|
+
meta: writeMeta(this.#ctx.storageHash)
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
/**
|
|
1515
|
+
* Find a single matching document and delete it. Returns the deleted
|
|
1516
|
+
* document via the row stream.
|
|
1517
|
+
*/
|
|
1518
|
+
findOneAndDelete() {
|
|
1519
|
+
const command = new FindOneAndDeleteCommand(this.#ctx.collection, this.#foldedFilter());
|
|
1520
|
+
return {
|
|
1521
|
+
collection: this.#ctx.collection,
|
|
1522
|
+
command,
|
|
1523
|
+
meta: writeMeta(this.#ctx.storageHash)
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
};
|
|
1527
|
+
function foldAnd(filters) {
|
|
1528
|
+
return MongoAndExpr.of(filters);
|
|
1529
|
+
}
|
|
1530
|
+
/**
|
|
1531
|
+
* Narrow a `MongoContractWithTypeMaps`-shaped value down to its underlying
|
|
1532
|
+
* `MongoContract` view. `MongoContractWithTypeMaps<C, ...>` is defined as
|
|
1533
|
+
* `C & { readonly [phantom]?: TTypeMaps }`, so every contract we accept is
|
|
1534
|
+
* structurally a `MongoContract` — the phantom is type-only. This helper
|
|
1535
|
+
* centralises that narrowing so callers don't reach for `as unknown as
|
|
1536
|
+
* MongoContract` double-casts.
|
|
1537
|
+
*/
|
|
1538
|
+
function asMongoContract(contract) {
|
|
1539
|
+
return contract;
|
|
1540
|
+
}
|
|
1541
|
+
/**
|
|
1542
|
+
* Construct a `CollectionHandle` from a validated contract + root name.
|
|
1543
|
+
* Used by `mongoQuery(...).from(name)` to enter the state machine.
|
|
1544
|
+
*/
|
|
1545
|
+
function createCollectionHandle(contract, rootName) {
|
|
1546
|
+
const c = asMongoContract(contract);
|
|
1547
|
+
const modelName = c.roots[rootName];
|
|
1548
|
+
if (!modelName) {
|
|
1549
|
+
const validRoots = Object.keys(c.roots).join(", ");
|
|
1550
|
+
throw new Error(`Unknown root: "${rootName}". Valid roots: ${validRoots}`);
|
|
1551
|
+
}
|
|
1552
|
+
const model = c.models[modelName];
|
|
1553
|
+
if (!model) throw new Error(`Unknown model: "${modelName}" referenced by root "${rootName}".`);
|
|
1554
|
+
const collectionName = model.storage?.collection ?? rootName;
|
|
1555
|
+
if (!c.storage?.storageHash) throw new Error("Contract is missing storage.storageHash. Pass a validated contract to mongoQuery().");
|
|
1556
|
+
return new CollectionHandle({
|
|
1557
|
+
contract,
|
|
1558
|
+
collection: collectionName,
|
|
1559
|
+
storageHash: String(c.storage.storageHash)
|
|
1560
|
+
}, modelName);
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
//#endregion
|
|
1564
|
+
//#region src/query.ts
|
|
1565
|
+
function mongoQuery(options) {
|
|
1566
|
+
const contract = options.contractJson;
|
|
1567
|
+
return {
|
|
1568
|
+
from(rootName) {
|
|
1569
|
+
return createCollectionHandle(contract, rootName);
|
|
1570
|
+
},
|
|
1571
|
+
rawCommand(command) {
|
|
1572
|
+
const storageHash = asMongoContract(contract).storage?.storageHash;
|
|
1573
|
+
if (!storageHash) throw new Error("Contract is missing storage.storageHash. Pass a validated contract to mongoQuery().");
|
|
1574
|
+
const meta = {
|
|
1575
|
+
target: "mongo",
|
|
1576
|
+
storageHash: String(storageHash),
|
|
1577
|
+
lane: "mongo-query",
|
|
1578
|
+
paramDescriptors: []
|
|
1579
|
+
};
|
|
1580
|
+
return {
|
|
1581
|
+
collection: command.collection,
|
|
1582
|
+
command,
|
|
1583
|
+
meta
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
//#endregion
|
|
1590
|
+
export { CollectionHandle, FilteredCollection, PipelineChain, acc, createFieldAccessor, fn, mongoQuery };
|
|
1591
|
+
//# sourceMappingURL=index.mjs.map
|