@zodmon/core 0.8.0 → 0.9.0
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/index.cjs +708 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2101 -954
- package/dist/index.d.ts +2101 -954
- package/dist/index.js +695 -44
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,3 +1,652 @@
|
|
|
1
|
+
// src/aggregate/expressions.ts
|
|
2
|
+
var $count = () => ({
|
|
3
|
+
__accum: true,
|
|
4
|
+
expr: { $sum: 1 }
|
|
5
|
+
});
|
|
6
|
+
var $sum = (field) => ({
|
|
7
|
+
__accum: true,
|
|
8
|
+
expr: { $sum: field }
|
|
9
|
+
});
|
|
10
|
+
var $avg = (field) => ({
|
|
11
|
+
__accum: true,
|
|
12
|
+
expr: { $avg: field }
|
|
13
|
+
});
|
|
14
|
+
var $min = (field) => ({
|
|
15
|
+
__accum: true,
|
|
16
|
+
expr: { $min: field }
|
|
17
|
+
});
|
|
18
|
+
var $max = (field) => ({
|
|
19
|
+
__accum: true,
|
|
20
|
+
expr: { $max: field }
|
|
21
|
+
});
|
|
22
|
+
var $first = (field) => ({
|
|
23
|
+
__accum: true,
|
|
24
|
+
expr: { $first: field }
|
|
25
|
+
});
|
|
26
|
+
var $last = (field) => ({
|
|
27
|
+
__accum: true,
|
|
28
|
+
expr: { $last: field }
|
|
29
|
+
});
|
|
30
|
+
var $push = (field) => ({
|
|
31
|
+
__accum: true,
|
|
32
|
+
expr: { $push: field }
|
|
33
|
+
});
|
|
34
|
+
var $addToSet = (field) => ({
|
|
35
|
+
__accum: true,
|
|
36
|
+
expr: { $addToSet: field }
|
|
37
|
+
});
|
|
38
|
+
function createAccumulatorBuilder() {
|
|
39
|
+
return {
|
|
40
|
+
count: () => ({ __accum: true, expr: { $sum: 1 } }),
|
|
41
|
+
sum: (field) => ({
|
|
42
|
+
__accum: true,
|
|
43
|
+
expr: { $sum: typeof field === "number" ? field : `$${field}` }
|
|
44
|
+
}),
|
|
45
|
+
avg: (field) => ({ __accum: true, expr: { $avg: `$${field}` } }),
|
|
46
|
+
min: (field) => ({ __accum: true, expr: { $min: `$${field}` } }),
|
|
47
|
+
max: (field) => ({ __accum: true, expr: { $max: `$${field}` } }),
|
|
48
|
+
first: (field) => ({ __accum: true, expr: { $first: `$${field}` } }),
|
|
49
|
+
last: (field) => ({ __accum: true, expr: { $last: `$${field}` } }),
|
|
50
|
+
push: (field) => ({ __accum: true, expr: { $push: `$${field}` } }),
|
|
51
|
+
addToSet: (field) => ({ __accum: true, expr: { $addToSet: `$${field}` } })
|
|
52
|
+
// biome-ignore lint/suspicious/noExplicitAny: Runtime implementation uses string field names and returns plain objects — TypeScript cannot verify that the runtime Accumulator objects match the generic AccumulatorBuilder<T> return types. Safe because type resolution happens at compile time via AccumulatorBuilder<T>, and runtime values are identical to what the standalone $min/$max/etc. produce.
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function createExpressionBuilder() {
|
|
56
|
+
const $2 = (field) => `$${field}`;
|
|
57
|
+
const val = (v) => typeof v === "number" ? v : `$${v}`;
|
|
58
|
+
const expr = (value) => ({ __expr: true, value });
|
|
59
|
+
return {
|
|
60
|
+
// Arithmetic
|
|
61
|
+
add: (field, value) => expr({ $add: [$2(field), val(value)] }),
|
|
62
|
+
subtract: (field, value) => expr({ $subtract: [$2(field), val(value)] }),
|
|
63
|
+
multiply: (field, value) => expr({ $multiply: [$2(field), val(value)] }),
|
|
64
|
+
divide: (field, value) => expr({ $divide: [$2(field), val(value)] }),
|
|
65
|
+
mod: (field, value) => expr({ $mod: [$2(field), val(value)] }),
|
|
66
|
+
abs: (field) => expr({ $abs: $2(field) }),
|
|
67
|
+
ceil: (field) => expr({ $ceil: $2(field) }),
|
|
68
|
+
floor: (field) => expr({ $floor: $2(field) }),
|
|
69
|
+
round: (field, place = 0) => expr({ $round: [$2(field), place] }),
|
|
70
|
+
// String
|
|
71
|
+
concat: (...parts) => {
|
|
72
|
+
const resolved = parts.map((p) => {
|
|
73
|
+
if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(p)) return $2(p);
|
|
74
|
+
return p;
|
|
75
|
+
});
|
|
76
|
+
return expr({ $concat: resolved });
|
|
77
|
+
},
|
|
78
|
+
toLower: (field) => expr({ $toLower: $2(field) }),
|
|
79
|
+
toUpper: (field) => expr({ $toUpper: $2(field) }),
|
|
80
|
+
trim: (field) => expr({ $trim: { input: $2(field) } }),
|
|
81
|
+
substr: (field, start, length) => expr({ $substrBytes: [$2(field), start, length] }),
|
|
82
|
+
// Comparison
|
|
83
|
+
eq: (field, value) => expr({ $eq: [$2(field), value] }),
|
|
84
|
+
gt: (field, value) => expr({ $gt: [$2(field), value] }),
|
|
85
|
+
gte: (field, value) => expr({ $gte: [$2(field), value] }),
|
|
86
|
+
lt: (field, value) => expr({ $lt: [$2(field), value] }),
|
|
87
|
+
lte: (field, value) => expr({ $lte: [$2(field), value] }),
|
|
88
|
+
ne: (field, value) => expr({ $ne: [$2(field), value] }),
|
|
89
|
+
// Date
|
|
90
|
+
year: (field) => expr({ $year: $2(field) }),
|
|
91
|
+
month: (field) => expr({ $month: $2(field) }),
|
|
92
|
+
dayOfMonth: (field) => expr({ $dayOfMonth: $2(field) }),
|
|
93
|
+
// Array
|
|
94
|
+
size: (field) => expr({ $size: $2(field) }),
|
|
95
|
+
// Conditional
|
|
96
|
+
cond: (condition, thenValue, elseValue) => expr({ $cond: [condition.value, thenValue, elseValue] }),
|
|
97
|
+
ifNull: (field, fallback) => expr({ $ifNull: [$2(field), fallback] })
|
|
98
|
+
// biome-ignore lint/suspicious/noExplicitAny: Runtime implementation uses string field names — TypeScript cannot verify generic ExpressionBuilder<T> return types match. Safe because type resolution happens at compile time.
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/schema/ref.ts
|
|
103
|
+
import { z } from "zod";
|
|
104
|
+
var refMetadata = /* @__PURE__ */ new WeakMap();
|
|
105
|
+
function getRefMetadata(schema) {
|
|
106
|
+
if (typeof schema !== "object" || schema === null) return void 0;
|
|
107
|
+
return refMetadata.get(schema);
|
|
108
|
+
}
|
|
109
|
+
var REF_GUARD = /* @__PURE__ */ Symbol.for("zodmon_ref");
|
|
110
|
+
function installRefExtension() {
|
|
111
|
+
const proto = z.ZodType.prototype;
|
|
112
|
+
if (REF_GUARD in proto) return;
|
|
113
|
+
Object.defineProperty(proto, "ref", {
|
|
114
|
+
value(collection2) {
|
|
115
|
+
refMetadata.set(this, { collection: collection2 });
|
|
116
|
+
return this;
|
|
117
|
+
},
|
|
118
|
+
enumerable: true,
|
|
119
|
+
configurable: true,
|
|
120
|
+
writable: true
|
|
121
|
+
});
|
|
122
|
+
Object.defineProperty(proto, REF_GUARD, {
|
|
123
|
+
value: true,
|
|
124
|
+
enumerable: false,
|
|
125
|
+
configurable: false,
|
|
126
|
+
writable: false
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/aggregate/pipeline.ts
|
|
131
|
+
var AggregatePipeline = class _AggregatePipeline {
|
|
132
|
+
definition;
|
|
133
|
+
nativeCollection;
|
|
134
|
+
stages;
|
|
135
|
+
constructor(definition, nativeCollection, stages) {
|
|
136
|
+
this.definition = definition;
|
|
137
|
+
this.nativeCollection = nativeCollection;
|
|
138
|
+
this.stages = stages;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Append an arbitrary aggregation stage to the pipeline (escape hatch).
|
|
142
|
+
*
|
|
143
|
+
* Returns a new pipeline instance with the stage appended — the
|
|
144
|
+
* original pipeline is not modified.
|
|
145
|
+
*
|
|
146
|
+
* Optionally accepts a type parameter `TNew` to change the output
|
|
147
|
+
* type when the stage transforms the document shape.
|
|
148
|
+
*
|
|
149
|
+
* @typeParam TNew - The output type after this stage. Defaults to the current output type.
|
|
150
|
+
* @param stage - A raw MongoDB aggregation stage document (e.g. `{ $match: { ... } }`).
|
|
151
|
+
* @returns A new pipeline with the stage appended.
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```ts
|
|
155
|
+
* const admins = aggregate(users)
|
|
156
|
+
* .raw({ $match: { role: 'admin' } })
|
|
157
|
+
* .toArray()
|
|
158
|
+
* ```
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* ```ts
|
|
162
|
+
* // Change output type with a $project stage
|
|
163
|
+
* const names = aggregate(users)
|
|
164
|
+
* .raw<{ name: string }>({ $project: { name: 1, _id: 0 } })
|
|
165
|
+
* .toArray()
|
|
166
|
+
* ```
|
|
167
|
+
*/
|
|
168
|
+
raw(stage) {
|
|
169
|
+
return new _AggregatePipeline(this.definition, this.nativeCollection, [
|
|
170
|
+
...this.stages,
|
|
171
|
+
stage
|
|
172
|
+
]);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Execute the pipeline and return all results as an array.
|
|
176
|
+
*
|
|
177
|
+
* @returns A promise resolving to the array of output documents.
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* ```ts
|
|
181
|
+
* const results = await aggregate(users)
|
|
182
|
+
* .raw({ $match: { age: { $gte: 18 } } })
|
|
183
|
+
* .toArray()
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
async toArray() {
|
|
187
|
+
const cursor = this.nativeCollection.aggregate(this.stages);
|
|
188
|
+
return await cursor.toArray();
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Stream pipeline results one document at a time via `for await...of`.
|
|
192
|
+
*
|
|
193
|
+
* @returns An async generator yielding output documents.
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* ```ts
|
|
197
|
+
* for await (const user of aggregate(users).raw({ $match: { role: 'admin' } })) {
|
|
198
|
+
* console.log(user.name)
|
|
199
|
+
* }
|
|
200
|
+
* ```
|
|
201
|
+
*/
|
|
202
|
+
async *[Symbol.asyncIterator]() {
|
|
203
|
+
const cursor = this.nativeCollection.aggregate(this.stages);
|
|
204
|
+
for await (const doc of cursor) {
|
|
205
|
+
yield doc;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Return the query execution plan without running the pipeline.
|
|
210
|
+
*
|
|
211
|
+
* Useful for debugging and understanding how MongoDB will process
|
|
212
|
+
* the pipeline stages.
|
|
213
|
+
*
|
|
214
|
+
* @returns A promise resolving to the explain output document.
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
* ```ts
|
|
218
|
+
* const plan = await aggregate(users)
|
|
219
|
+
* .raw({ $match: { role: 'admin' } })
|
|
220
|
+
* .explain()
|
|
221
|
+
* console.log(plan)
|
|
222
|
+
* ```
|
|
223
|
+
*/
|
|
224
|
+
async explain() {
|
|
225
|
+
const cursor = this.nativeCollection.aggregate(this.stages);
|
|
226
|
+
return await cursor.explain();
|
|
227
|
+
}
|
|
228
|
+
// ── Shape-preserving stages ──────────────────────────────────────
|
|
229
|
+
/**
|
|
230
|
+
* Filter documents using a type-safe match expression.
|
|
231
|
+
*
|
|
232
|
+
* Appends a `$match` stage to the pipeline. The filter is constrained
|
|
233
|
+
* to the current output type, so only valid fields and operators are accepted.
|
|
234
|
+
*
|
|
235
|
+
* Supports two forms of type narrowing:
|
|
236
|
+
*
|
|
237
|
+
* **Tier 1 — Explicit type parameter:**
|
|
238
|
+
* ```ts
|
|
239
|
+
* .match<{ role: 'engineer' | 'designer' }>({ role: { $in: ['engineer', 'designer'] } })
|
|
240
|
+
* // role narrows to 'engineer' | 'designer'
|
|
241
|
+
* ```
|
|
242
|
+
*
|
|
243
|
+
* **Tier 2 — Automatic inference from filter literals:**
|
|
244
|
+
* ```ts
|
|
245
|
+
* .match({ role: 'engineer' }) // role narrows to 'engineer'
|
|
246
|
+
* .match({ role: { $ne: 'intern' } }) // role narrows to Exclude<Role, 'intern'>
|
|
247
|
+
* .match({ role: { $in: ['engineer', 'designer'] as const } }) // needs as const
|
|
248
|
+
* ```
|
|
249
|
+
*
|
|
250
|
+
* When no type parameter is provided and the filter doesn't contain
|
|
251
|
+
* inferrable literals, the output type is unchanged (backward compatible).
|
|
252
|
+
*
|
|
253
|
+
* @typeParam TNarrow - Optional object mapping field names to narrowed types. Must be a subtype of the corresponding fields in TOutput.
|
|
254
|
+
* @typeParam F - Inferred from the filter argument. Do not provide explicitly.
|
|
255
|
+
* @param filter - A type-safe filter for the current output type.
|
|
256
|
+
* @returns A new pipeline with the `$match` stage appended and output type narrowed.
|
|
257
|
+
*
|
|
258
|
+
* @example
|
|
259
|
+
* ```ts
|
|
260
|
+
* // Explicit narrowing
|
|
261
|
+
* const filtered = await users.aggregate()
|
|
262
|
+
* .match<{ role: 'engineer' }>({ role: 'engineer' })
|
|
263
|
+
* .toArray()
|
|
264
|
+
* // filtered[0].role → 'engineer'
|
|
265
|
+
*
|
|
266
|
+
* // Automatic narrowing with $in (requires as const)
|
|
267
|
+
* const subset = await users.aggregate()
|
|
268
|
+
* .match({ role: { $in: ['engineer', 'designer'] as const } })
|
|
269
|
+
* .toArray()
|
|
270
|
+
* // subset[0].role → 'engineer' | 'designer'
|
|
271
|
+
* ```
|
|
272
|
+
*/
|
|
273
|
+
match(filter) {
|
|
274
|
+
const pipeline = new _AggregatePipeline(this.definition, this.nativeCollection, [
|
|
275
|
+
...this.stages,
|
|
276
|
+
{ $match: filter }
|
|
277
|
+
]);
|
|
278
|
+
return pipeline;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Sort documents by one or more fields.
|
|
282
|
+
*
|
|
283
|
+
* Appends a `$sort` stage. Keys are constrained to `keyof TOutput & string`
|
|
284
|
+
* and values must be `1` (ascending) or `-1` (descending).
|
|
285
|
+
*
|
|
286
|
+
* @param spec - A sort specification mapping field names to sort direction.
|
|
287
|
+
* @returns A new pipeline with the `$sort` stage appended.
|
|
288
|
+
*
|
|
289
|
+
* @example
|
|
290
|
+
* ```ts
|
|
291
|
+
* const sorted = await aggregate(users)
|
|
292
|
+
* .sort({ age: -1, name: 1 })
|
|
293
|
+
* .toArray()
|
|
294
|
+
* ```
|
|
295
|
+
*/
|
|
296
|
+
sort(spec) {
|
|
297
|
+
return new _AggregatePipeline(this.definition, this.nativeCollection, [
|
|
298
|
+
...this.stages,
|
|
299
|
+
{ $sort: spec }
|
|
300
|
+
]);
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Skip a number of documents in the pipeline.
|
|
304
|
+
*
|
|
305
|
+
* Appends a `$skip` stage. Commonly used with {@link limit} for pagination.
|
|
306
|
+
*
|
|
307
|
+
* @param n - The number of documents to skip.
|
|
308
|
+
* @returns A new pipeline with the `$skip` stage appended.
|
|
309
|
+
*
|
|
310
|
+
* @example
|
|
311
|
+
* ```ts
|
|
312
|
+
* // Page 2 (10 items per page)
|
|
313
|
+
* const page2 = await aggregate(users)
|
|
314
|
+
* .sort({ name: 1 })
|
|
315
|
+
* .skip(10)
|
|
316
|
+
* .limit(10)
|
|
317
|
+
* .toArray()
|
|
318
|
+
* ```
|
|
319
|
+
*/
|
|
320
|
+
skip(n) {
|
|
321
|
+
return new _AggregatePipeline(this.definition, this.nativeCollection, [
|
|
322
|
+
...this.stages,
|
|
323
|
+
{ $skip: n }
|
|
324
|
+
]);
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Limit the number of documents passing through the pipeline.
|
|
328
|
+
*
|
|
329
|
+
* Appends a `$limit` stage. Commonly used with {@link skip} for pagination,
|
|
330
|
+
* or after {@link sort} to get top/bottom N results.
|
|
331
|
+
*
|
|
332
|
+
* @param n - The maximum number of documents to pass through.
|
|
333
|
+
* @returns A new pipeline with the `$limit` stage appended.
|
|
334
|
+
*
|
|
335
|
+
* @example
|
|
336
|
+
* ```ts
|
|
337
|
+
* const top5 = await aggregate(users)
|
|
338
|
+
* .sort({ score: -1 })
|
|
339
|
+
* .limit(5)
|
|
340
|
+
* .toArray()
|
|
341
|
+
* ```
|
|
342
|
+
*/
|
|
343
|
+
limit(n) {
|
|
344
|
+
return new _AggregatePipeline(this.definition, this.nativeCollection, [
|
|
345
|
+
...this.stages,
|
|
346
|
+
{ $limit: n }
|
|
347
|
+
]);
|
|
348
|
+
}
|
|
349
|
+
// ── Shape-transforming projection stages ─────────────────────────
|
|
350
|
+
/**
|
|
351
|
+
* Include only specified fields in the output.
|
|
352
|
+
*
|
|
353
|
+
* Appends a `$project` stage with inclusion (`1`) for each key.
|
|
354
|
+
* The `_id` field is always included. The output type narrows to
|
|
355
|
+
* `Pick<TOutput, K | '_id'>`.
|
|
356
|
+
*
|
|
357
|
+
* @param spec - An object mapping field names to `1` for inclusion.
|
|
358
|
+
* @returns A new pipeline with the `$project` stage appended.
|
|
359
|
+
*
|
|
360
|
+
* @example
|
|
361
|
+
* ```ts
|
|
362
|
+
* const namesOnly = await aggregate(users)
|
|
363
|
+
* .project({ name: 1 })
|
|
364
|
+
* .toArray()
|
|
365
|
+
* // [{ _id: ..., name: 'Ada' }, ...]
|
|
366
|
+
* ```
|
|
367
|
+
*/
|
|
368
|
+
project(spec) {
|
|
369
|
+
const pipeline = new _AggregatePipeline(this.definition, this.nativeCollection, [
|
|
370
|
+
...this.stages,
|
|
371
|
+
{ $project: spec }
|
|
372
|
+
]);
|
|
373
|
+
return pipeline;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Variadic shorthand for {@link project} — pick fields to include.
|
|
377
|
+
*
|
|
378
|
+
* Generates a `$project` stage that includes only the listed fields
|
|
379
|
+
* (plus `_id`). Equivalent to `.project({ field1: 1, field2: 1 })`.
|
|
380
|
+
*
|
|
381
|
+
* @param fields - Field names to include in the output.
|
|
382
|
+
* @returns A new pipeline with the `$project` stage appended.
|
|
383
|
+
*
|
|
384
|
+
* @example
|
|
385
|
+
* ```ts
|
|
386
|
+
* const namesAndRoles = await aggregate(users)
|
|
387
|
+
* .pick('name', 'role')
|
|
388
|
+
* .toArray()
|
|
389
|
+
* ```
|
|
390
|
+
*/
|
|
391
|
+
pick(...fields) {
|
|
392
|
+
const spec = Object.fromEntries(fields.map((f) => [f, 1]));
|
|
393
|
+
const pipeline = new _AggregatePipeline(this.definition, this.nativeCollection, [
|
|
394
|
+
...this.stages,
|
|
395
|
+
{ $project: spec }
|
|
396
|
+
]);
|
|
397
|
+
return pipeline;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Exclude specified fields from the output.
|
|
401
|
+
*
|
|
402
|
+
* Appends a `$project` stage with exclusion (`0`) for each key.
|
|
403
|
+
* All other fields pass through. The output type becomes `Omit<TOutput, K>`.
|
|
404
|
+
*
|
|
405
|
+
* @param fields - Field names to exclude from the output.
|
|
406
|
+
* @returns A new pipeline with the `$project` stage appended.
|
|
407
|
+
*
|
|
408
|
+
* @example
|
|
409
|
+
* ```ts
|
|
410
|
+
* const noAge = await aggregate(users)
|
|
411
|
+
* .omit('age')
|
|
412
|
+
* .toArray()
|
|
413
|
+
* ```
|
|
414
|
+
*/
|
|
415
|
+
omit(...fields) {
|
|
416
|
+
const spec = Object.fromEntries(fields.map((f) => [f, 0]));
|
|
417
|
+
const pipeline = new _AggregatePipeline(this.definition, this.nativeCollection, [
|
|
418
|
+
...this.stages,
|
|
419
|
+
{ $project: spec }
|
|
420
|
+
]);
|
|
421
|
+
return pipeline;
|
|
422
|
+
}
|
|
423
|
+
groupBy(field, accumulators) {
|
|
424
|
+
const resolved = typeof accumulators === "function" ? accumulators(createAccumulatorBuilder()) : accumulators;
|
|
425
|
+
const _id = Array.isArray(field) ? Object.fromEntries(field.map((f) => [f, `$${f}`])) : `$${field}`;
|
|
426
|
+
const accumExprs = Object.fromEntries(
|
|
427
|
+
Object.entries(resolved).map(([key, acc]) => [key, acc.expr])
|
|
428
|
+
);
|
|
429
|
+
const pipeline = new _AggregatePipeline(this.definition, this.nativeCollection, [
|
|
430
|
+
...this.stages,
|
|
431
|
+
{ $group: { _id, ...accumExprs } }
|
|
432
|
+
]);
|
|
433
|
+
return pipeline;
|
|
434
|
+
}
|
|
435
|
+
// Implementation
|
|
436
|
+
addFields(fields) {
|
|
437
|
+
const resolved = typeof fields === "function" ? fields(createExpressionBuilder()) : fields;
|
|
438
|
+
const stage = Object.fromEntries(
|
|
439
|
+
Object.entries(resolved).map(([k, v]) => [
|
|
440
|
+
k,
|
|
441
|
+
v && typeof v === "object" && "__expr" in v ? v.value : v
|
|
442
|
+
])
|
|
443
|
+
);
|
|
444
|
+
const pipeline = new _AggregatePipeline(this.definition, this.nativeCollection, [
|
|
445
|
+
...this.stages,
|
|
446
|
+
{ $addFields: stage }
|
|
447
|
+
]);
|
|
448
|
+
return pipeline;
|
|
449
|
+
}
|
|
450
|
+
// ── unwind stage ─────────────────────────────────────────────────
|
|
451
|
+
/**
|
|
452
|
+
* Deconstruct an array field, outputting one document per array element.
|
|
453
|
+
*
|
|
454
|
+
* Appends an `$unwind` stage. The unwound field's type changes from
|
|
455
|
+
* `T[]` to `T` in the output type. Documents with empty or missing
|
|
456
|
+
* arrays are dropped unless `preserveEmpty` is `true`.
|
|
457
|
+
*
|
|
458
|
+
* @param field - The name of the array field to unwind.
|
|
459
|
+
* @param options - Optional settings for the unwind stage.
|
|
460
|
+
* @param options.preserveEmpty - If `true`, documents with null, missing, or empty arrays are preserved.
|
|
461
|
+
* @returns A new pipeline with the `$unwind` stage appended.
|
|
462
|
+
*
|
|
463
|
+
* @example
|
|
464
|
+
* ```ts
|
|
465
|
+
* const flat = await aggregate(orders)
|
|
466
|
+
* .unwind('items')
|
|
467
|
+
* .toArray()
|
|
468
|
+
* // Each result has a single `items` value instead of an array
|
|
469
|
+
* ```
|
|
470
|
+
*/
|
|
471
|
+
unwind(field, options) {
|
|
472
|
+
const stage = options?.preserveEmpty ? { $unwind: { path: `$${field}`, preserveNullAndEmptyArrays: true } } : { $unwind: `$${field}` };
|
|
473
|
+
const pipeline = new _AggregatePipeline(this.definition, this.nativeCollection, [
|
|
474
|
+
...this.stages,
|
|
475
|
+
stage
|
|
476
|
+
]);
|
|
477
|
+
return pipeline;
|
|
478
|
+
}
|
|
479
|
+
lookup(fieldOrFrom, options) {
|
|
480
|
+
const stages = [...this.stages];
|
|
481
|
+
if (typeof fieldOrFrom === "object") {
|
|
482
|
+
const foreignName = fieldOrFrom.name;
|
|
483
|
+
const foreignField = options?.on;
|
|
484
|
+
if (!foreignField) {
|
|
485
|
+
throw new Error(
|
|
486
|
+
`[zodmon] lookup: reverse lookup on '${foreignName}' requires an 'on' option specifying which field on the foreign collection references this collection.`
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
const asField = options?.as ?? foreignName;
|
|
490
|
+
stages.push({
|
|
491
|
+
$lookup: {
|
|
492
|
+
from: foreignName,
|
|
493
|
+
localField: "_id",
|
|
494
|
+
foreignField,
|
|
495
|
+
as: asField
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
if (options?.unwind) {
|
|
499
|
+
stages.push({ $unwind: { path: `$${asField}`, preserveNullAndEmptyArrays: true } });
|
|
500
|
+
}
|
|
501
|
+
} else {
|
|
502
|
+
const shape = this.definition.shape;
|
|
503
|
+
const fieldSchema = shape[fieldOrFrom];
|
|
504
|
+
const ref = getRefMetadata(fieldSchema);
|
|
505
|
+
if (!ref) {
|
|
506
|
+
throw new Error(
|
|
507
|
+
`[zodmon] lookup: field '${fieldOrFrom}' has no .ref() metadata. Use .lookup(CollectionDef, { on: foreignKey }) for reverse lookups, or add .ref(TargetCollection) to the field schema.`
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
const targetName = ref.collection.name;
|
|
511
|
+
const asField = options?.as ?? targetName;
|
|
512
|
+
stages.push({
|
|
513
|
+
$lookup: {
|
|
514
|
+
from: targetName,
|
|
515
|
+
localField: fieldOrFrom,
|
|
516
|
+
foreignField: "_id",
|
|
517
|
+
as: asField
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
if (options?.unwind) {
|
|
521
|
+
stages.push({ $unwind: { path: `$${asField}`, preserveNullAndEmptyArrays: true } });
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
const pipeline = new _AggregatePipeline(this.definition, this.nativeCollection, stages);
|
|
525
|
+
return pipeline;
|
|
526
|
+
}
|
|
527
|
+
// ── Convenience shortcuts ────────────────────────────────────────
|
|
528
|
+
/**
|
|
529
|
+
* Count documents per group, sorted by count descending.
|
|
530
|
+
*
|
|
531
|
+
* Shorthand for `.groupBy(field, { count: $count() }).sort({ count: -1 })`.
|
|
532
|
+
*
|
|
533
|
+
* @param field - The field to group and count by.
|
|
534
|
+
* @returns A new pipeline producing `{ _id: TOutput[K], count: number }` results.
|
|
535
|
+
*
|
|
536
|
+
* @example
|
|
537
|
+
* ```ts
|
|
538
|
+
* const roleCounts = await aggregate(users)
|
|
539
|
+
* .countBy('role')
|
|
540
|
+
* .toArray()
|
|
541
|
+
* // [{ _id: 'user', count: 3 }, { _id: 'admin', count: 2 }]
|
|
542
|
+
* ```
|
|
543
|
+
*/
|
|
544
|
+
countBy(field) {
|
|
545
|
+
const pipeline = new _AggregatePipeline(this.definition, this.nativeCollection, [
|
|
546
|
+
...this.stages,
|
|
547
|
+
{ $group: { _id: `$${field}`, count: { $sum: 1 } } },
|
|
548
|
+
{ $sort: { count: -1 } }
|
|
549
|
+
]);
|
|
550
|
+
return pipeline;
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Sum a numeric field per group, sorted by total descending.
|
|
554
|
+
*
|
|
555
|
+
* Shorthand for `.groupBy(field, { total: $sum('$sumField') }).sort({ total: -1 })`.
|
|
556
|
+
*
|
|
557
|
+
* @param field - The field to group by.
|
|
558
|
+
* @param sumField - The numeric field to sum.
|
|
559
|
+
* @returns A new pipeline producing `{ _id: TOutput[K], total: number }` results.
|
|
560
|
+
*
|
|
561
|
+
* @example
|
|
562
|
+
* ```ts
|
|
563
|
+
* const revenueByCategory = await aggregate(orders)
|
|
564
|
+
* .sumBy('category', 'amount')
|
|
565
|
+
* .toArray()
|
|
566
|
+
* // [{ _id: 'electronics', total: 5000 }, ...]
|
|
567
|
+
* ```
|
|
568
|
+
*/
|
|
569
|
+
sumBy(field, sumField) {
|
|
570
|
+
const pipeline = new _AggregatePipeline(this.definition, this.nativeCollection, [
|
|
571
|
+
...this.stages,
|
|
572
|
+
{ $group: { _id: `$${field}`, total: { $sum: `$${sumField}` } } },
|
|
573
|
+
{ $sort: { total: -1 } }
|
|
574
|
+
]);
|
|
575
|
+
return pipeline;
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Sort by a single field with a friendly direction name.
|
|
579
|
+
*
|
|
580
|
+
* Shorthand for `.sort({ [field]: direction === 'desc' ? -1 : 1 })`.
|
|
581
|
+
*
|
|
582
|
+
* @param field - The field to sort by.
|
|
583
|
+
* @param direction - Sort direction: `'asc'` (default) or `'desc'`.
|
|
584
|
+
* @returns A new pipeline with the `$sort` stage appended.
|
|
585
|
+
*
|
|
586
|
+
* @example
|
|
587
|
+
* ```ts
|
|
588
|
+
* const youngest = await aggregate(users)
|
|
589
|
+
* .sortBy('age')
|
|
590
|
+
* .toArray()
|
|
591
|
+
* ```
|
|
592
|
+
*/
|
|
593
|
+
sortBy(field, direction = "asc") {
|
|
594
|
+
return new _AggregatePipeline(this.definition, this.nativeCollection, [
|
|
595
|
+
...this.stages,
|
|
596
|
+
{ $sort: { [field]: direction === "desc" ? -1 : 1 } }
|
|
597
|
+
]);
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Return the top N documents sorted by a field descending.
|
|
601
|
+
*
|
|
602
|
+
* Shorthand for `.sort({ [by]: -1 }).limit(n)`.
|
|
603
|
+
*
|
|
604
|
+
* @param n - The number of documents to return.
|
|
605
|
+
* @param options - An object with a `by` field specifying the sort key.
|
|
606
|
+
* @returns A new pipeline with `$sort` and `$limit` stages appended.
|
|
607
|
+
*
|
|
608
|
+
* @example
|
|
609
|
+
* ```ts
|
|
610
|
+
* const top3 = await aggregate(users)
|
|
611
|
+
* .top(3, { by: 'score' })
|
|
612
|
+
* .toArray()
|
|
613
|
+
* ```
|
|
614
|
+
*/
|
|
615
|
+
top(n, options) {
|
|
616
|
+
return new _AggregatePipeline(this.definition, this.nativeCollection, [
|
|
617
|
+
...this.stages,
|
|
618
|
+
{ $sort: { [options.by]: -1 } },
|
|
619
|
+
{ $limit: n }
|
|
620
|
+
]);
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Return the bottom N documents sorted by a field ascending.
|
|
624
|
+
*
|
|
625
|
+
* Shorthand for `.sort({ [by]: 1 }).limit(n)`.
|
|
626
|
+
*
|
|
627
|
+
* @param n - The number of documents to return.
|
|
628
|
+
* @param options - An object with a `by` field specifying the sort key.
|
|
629
|
+
* @returns A new pipeline with `$sort` and `$limit` stages appended.
|
|
630
|
+
*
|
|
631
|
+
* @example
|
|
632
|
+
* ```ts
|
|
633
|
+
* const bottom3 = await aggregate(users)
|
|
634
|
+
* .bottom(3, { by: 'score' })
|
|
635
|
+
* .toArray()
|
|
636
|
+
* ```
|
|
637
|
+
*/
|
|
638
|
+
bottom(n, options) {
|
|
639
|
+
return new _AggregatePipeline(this.definition, this.nativeCollection, [
|
|
640
|
+
...this.stages,
|
|
641
|
+
{ $sort: { [options.by]: 1 } },
|
|
642
|
+
{ $limit: n }
|
|
643
|
+
]);
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
function aggregate(handle) {
|
|
647
|
+
return new AggregatePipeline(handle.definition, handle.native, []);
|
|
648
|
+
}
|
|
649
|
+
|
|
1
650
|
// src/client/client.ts
|
|
2
651
|
import { MongoClient } from "mongodb";
|
|
3
652
|
|
|
@@ -164,7 +813,7 @@ async function syncIndexes(handle, options) {
|
|
|
164
813
|
}
|
|
165
814
|
|
|
166
815
|
// src/crud/delete.ts
|
|
167
|
-
import { z } from "zod";
|
|
816
|
+
import { z as z2 } from "zod";
|
|
168
817
|
|
|
169
818
|
// src/errors/validation.ts
|
|
170
819
|
var ZodmonValidationError = class extends Error {
|
|
@@ -205,7 +854,7 @@ async function findOneAndDelete(handle, filter, options) {
|
|
|
205
854
|
try {
|
|
206
855
|
return handle.definition.schema.parse(result);
|
|
207
856
|
} catch (err) {
|
|
208
|
-
if (err instanceof
|
|
857
|
+
if (err instanceof z2.ZodError) {
|
|
209
858
|
throw new ZodmonValidationError(handle.definition.name, err);
|
|
210
859
|
}
|
|
211
860
|
throw err;
|
|
@@ -213,7 +862,7 @@ async function findOneAndDelete(handle, filter, options) {
|
|
|
213
862
|
}
|
|
214
863
|
|
|
215
864
|
// src/crud/find.ts
|
|
216
|
-
import { z as
|
|
865
|
+
import { z as z4 } from "zod";
|
|
217
866
|
|
|
218
867
|
// src/errors/not-found.ts
|
|
219
868
|
var ZodmonNotFoundError = class extends Error {
|
|
@@ -250,7 +899,7 @@ function checkUnindexedFields(definition, filter) {
|
|
|
250
899
|
}
|
|
251
900
|
|
|
252
901
|
// src/query/cursor.ts
|
|
253
|
-
import { z as
|
|
902
|
+
import { z as z3 } from "zod";
|
|
254
903
|
|
|
255
904
|
// src/crud/paginate.ts
|
|
256
905
|
import { ObjectId } from "mongodb";
|
|
@@ -510,7 +1159,7 @@ var TypedFindCursor = class {
|
|
|
510
1159
|
try {
|
|
511
1160
|
return this.schema.parse(raw2);
|
|
512
1161
|
} catch (err) {
|
|
513
|
-
if (err instanceof
|
|
1162
|
+
if (err instanceof z3.ZodError) {
|
|
514
1163
|
throw new ZodmonValidationError(this.collectionName, err);
|
|
515
1164
|
}
|
|
516
1165
|
throw err;
|
|
@@ -531,7 +1180,7 @@ async function findOne(handle, filter, options) {
|
|
|
531
1180
|
try {
|
|
532
1181
|
return handle.definition.schema.parse(raw2);
|
|
533
1182
|
} catch (err) {
|
|
534
|
-
if (err instanceof
|
|
1183
|
+
if (err instanceof z4.ZodError) {
|
|
535
1184
|
throw new ZodmonValidationError(handle.definition.name, err);
|
|
536
1185
|
}
|
|
537
1186
|
throw err;
|
|
@@ -553,13 +1202,13 @@ function find(handle, filter, options) {
|
|
|
553
1202
|
}
|
|
554
1203
|
|
|
555
1204
|
// src/crud/insert.ts
|
|
556
|
-
import { z as
|
|
1205
|
+
import { z as z5 } from "zod";
|
|
557
1206
|
async function insertOne(handle, doc) {
|
|
558
1207
|
let parsed;
|
|
559
1208
|
try {
|
|
560
1209
|
parsed = handle.definition.schema.parse(doc);
|
|
561
1210
|
} catch (err) {
|
|
562
|
-
if (err instanceof
|
|
1211
|
+
if (err instanceof z5.ZodError) {
|
|
563
1212
|
throw new ZodmonValidationError(handle.definition.name, err);
|
|
564
1213
|
}
|
|
565
1214
|
throw err;
|
|
@@ -574,7 +1223,7 @@ async function insertMany(handle, docs) {
|
|
|
574
1223
|
try {
|
|
575
1224
|
parsed.push(handle.definition.schema.parse(doc));
|
|
576
1225
|
} catch (err) {
|
|
577
|
-
if (err instanceof
|
|
1226
|
+
if (err instanceof z5.ZodError) {
|
|
578
1227
|
throw new ZodmonValidationError(handle.definition.name, err);
|
|
579
1228
|
}
|
|
580
1229
|
throw err;
|
|
@@ -585,7 +1234,7 @@ async function insertMany(handle, docs) {
|
|
|
585
1234
|
}
|
|
586
1235
|
|
|
587
1236
|
// src/crud/update.ts
|
|
588
|
-
import { z as
|
|
1237
|
+
import { z as z6 } from "zod";
|
|
589
1238
|
async function updateOne(handle, filter, update, options) {
|
|
590
1239
|
return await handle.native.updateOne(filter, update, options);
|
|
591
1240
|
}
|
|
@@ -616,7 +1265,7 @@ async function findOneAndUpdate(handle, filter, update, options) {
|
|
|
616
1265
|
try {
|
|
617
1266
|
return handle.definition.schema.parse(result);
|
|
618
1267
|
} catch (err) {
|
|
619
|
-
if (err instanceof
|
|
1268
|
+
if (err instanceof z6.ZodError) {
|
|
620
1269
|
throw new ZodmonValidationError(handle.definition.name, err);
|
|
621
1270
|
}
|
|
622
1271
|
throw err;
|
|
@@ -907,6 +1556,27 @@ var CollectionHandle = class {
|
|
|
907
1556
|
async syncIndexes(options) {
|
|
908
1557
|
return await syncIndexes(this, options);
|
|
909
1558
|
}
|
|
1559
|
+
/**
|
|
1560
|
+
* Start a type-safe aggregation pipeline on this collection.
|
|
1561
|
+
*
|
|
1562
|
+
* Returns a fluent pipeline builder that tracks the output document
|
|
1563
|
+
* shape through each stage. The pipeline is lazy — no query executes
|
|
1564
|
+
* until a terminal method (`toArray`, `for await`, `explain`) is called.
|
|
1565
|
+
*
|
|
1566
|
+
* @returns A new pipeline builder starting with this collection's document type.
|
|
1567
|
+
*
|
|
1568
|
+
* @example
|
|
1569
|
+
* ```ts
|
|
1570
|
+
* const users = db.use(Users)
|
|
1571
|
+
* const result = await users.aggregate()
|
|
1572
|
+
* .match({ role: 'admin' })
|
|
1573
|
+
* .groupBy('role', { count: $count() })
|
|
1574
|
+
* .toArray()
|
|
1575
|
+
* ```
|
|
1576
|
+
*/
|
|
1577
|
+
aggregate() {
|
|
1578
|
+
return aggregate(this);
|
|
1579
|
+
}
|
|
910
1580
|
};
|
|
911
1581
|
|
|
912
1582
|
// src/client/client.ts
|
|
@@ -932,9 +1602,7 @@ var Database = class {
|
|
|
932
1602
|
*/
|
|
933
1603
|
use(def) {
|
|
934
1604
|
this._collections.set(def.name, def);
|
|
935
|
-
const native = this._db.collection(
|
|
936
|
-
def.name
|
|
937
|
-
);
|
|
1605
|
+
const native = this._db.collection(def.name);
|
|
938
1606
|
return new CollectionHandle(
|
|
939
1607
|
def,
|
|
940
1608
|
native
|
|
@@ -1012,36 +1680,6 @@ import { z as z9 } from "zod";
|
|
|
1012
1680
|
|
|
1013
1681
|
// src/schema/extensions.ts
|
|
1014
1682
|
import { z as z7 } from "zod";
|
|
1015
|
-
|
|
1016
|
-
// src/schema/ref.ts
|
|
1017
|
-
import { z as z6 } from "zod";
|
|
1018
|
-
var refMetadata = /* @__PURE__ */ new WeakMap();
|
|
1019
|
-
function getRefMetadata(schema) {
|
|
1020
|
-
if (typeof schema !== "object" || schema === null) return void 0;
|
|
1021
|
-
return refMetadata.get(schema);
|
|
1022
|
-
}
|
|
1023
|
-
var REF_GUARD = /* @__PURE__ */ Symbol.for("zodmon_ref");
|
|
1024
|
-
function installRefExtension() {
|
|
1025
|
-
const proto = z6.ZodType.prototype;
|
|
1026
|
-
if (REF_GUARD in proto) return;
|
|
1027
|
-
Object.defineProperty(proto, "ref", {
|
|
1028
|
-
value(collection2) {
|
|
1029
|
-
refMetadata.set(this, { collection: collection2 });
|
|
1030
|
-
return this;
|
|
1031
|
-
},
|
|
1032
|
-
enumerable: true,
|
|
1033
|
-
configurable: true,
|
|
1034
|
-
writable: true
|
|
1035
|
-
});
|
|
1036
|
-
Object.defineProperty(proto, REF_GUARD, {
|
|
1037
|
-
value: true,
|
|
1038
|
-
enumerable: false,
|
|
1039
|
-
configurable: false,
|
|
1040
|
-
writable: false
|
|
1041
|
-
});
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
// src/schema/extensions.ts
|
|
1045
1683
|
var indexMetadata = /* @__PURE__ */ new WeakMap();
|
|
1046
1684
|
function getIndexMetadata(schema) {
|
|
1047
1685
|
if (typeof schema !== "object" || schema === null) return void 0;
|
|
@@ -1291,29 +1929,42 @@ var $ = {
|
|
|
1291
1929
|
};
|
|
1292
1930
|
export {
|
|
1293
1931
|
$,
|
|
1932
|
+
$addToSet,
|
|
1294
1933
|
$and,
|
|
1934
|
+
$avg,
|
|
1935
|
+
$count,
|
|
1295
1936
|
$eq,
|
|
1296
1937
|
$exists,
|
|
1938
|
+
$first,
|
|
1297
1939
|
$gt,
|
|
1298
1940
|
$gte,
|
|
1299
1941
|
$in,
|
|
1942
|
+
$last,
|
|
1300
1943
|
$lt,
|
|
1301
1944
|
$lte,
|
|
1945
|
+
$max,
|
|
1946
|
+
$min,
|
|
1302
1947
|
$ne,
|
|
1303
1948
|
$nin,
|
|
1304
1949
|
$nor,
|
|
1305
1950
|
$not,
|
|
1306
1951
|
$or,
|
|
1952
|
+
$push,
|
|
1307
1953
|
$regex,
|
|
1954
|
+
$sum,
|
|
1955
|
+
AggregatePipeline,
|
|
1308
1956
|
CollectionHandle,
|
|
1309
1957
|
Database,
|
|
1310
1958
|
IndexBuilder,
|
|
1311
1959
|
TypedFindCursor,
|
|
1312
1960
|
ZodmonNotFoundError,
|
|
1313
1961
|
ZodmonValidationError,
|
|
1962
|
+
aggregate,
|
|
1314
1963
|
checkUnindexedFields,
|
|
1315
1964
|
collection,
|
|
1965
|
+
createAccumulatorBuilder,
|
|
1316
1966
|
createClient,
|
|
1967
|
+
createExpressionBuilder,
|
|
1317
1968
|
deleteMany,
|
|
1318
1969
|
deleteOne,
|
|
1319
1970
|
extractComparableOptions,
|