@zodmon/core 0.10.0 → 0.12.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 +991 -234
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1914 -344
- package/dist/index.d.ts +1914 -344
- package/dist/index.js +980 -234
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -48,6 +48,11 @@ __export(index_exports, {
|
|
|
48
48
|
CollectionHandle: () => CollectionHandle,
|
|
49
49
|
Database: () => Database,
|
|
50
50
|
IndexBuilder: () => IndexBuilder,
|
|
51
|
+
PopulateCursor: () => PopulateCursor,
|
|
52
|
+
PopulateOneOrThrowQuery: () => PopulateOneOrThrowQuery,
|
|
53
|
+
PopulateOneQuery: () => PopulateOneQuery,
|
|
54
|
+
PopulateRefBuilder: () => PopulateRefBuilder,
|
|
55
|
+
TransactionContext: () => TransactionContext,
|
|
51
56
|
TypedFindCursor: () => TypedFindCursor,
|
|
52
57
|
ZodmonAuthError: () => ZodmonAuthError,
|
|
53
58
|
ZodmonBulkWriteError: () => ZodmonBulkWriteError,
|
|
@@ -67,8 +72,11 @@ __export(index_exports, {
|
|
|
67
72
|
createAccumulatorBuilder: () => createAccumulatorBuilder,
|
|
68
73
|
createClient: () => createClient,
|
|
69
74
|
createExpressionBuilder: () => createExpressionBuilder,
|
|
75
|
+
createPopulateCursor: () => createPopulateCursor,
|
|
70
76
|
deleteMany: () => deleteMany,
|
|
71
77
|
deleteOne: () => deleteOne,
|
|
78
|
+
deriveProjectedSchema: () => deriveProjectedSchema,
|
|
79
|
+
executePopulate: () => executePopulate,
|
|
72
80
|
extractComparableOptions: () => extractComparableOptions,
|
|
73
81
|
extractDbName: () => extractDbName,
|
|
74
82
|
extractFieldIndexes: () => extractFieldIndexes,
|
|
@@ -83,14 +91,17 @@ __export(index_exports, {
|
|
|
83
91
|
index: () => index,
|
|
84
92
|
insertMany: () => insertMany,
|
|
85
93
|
insertOne: () => insertOne,
|
|
94
|
+
isInclusionProjection: () => isInclusionProjection,
|
|
86
95
|
isOid: () => isOid,
|
|
87
96
|
objectId: () => objectId,
|
|
88
97
|
oid: () => oid,
|
|
89
98
|
raw: () => raw,
|
|
99
|
+
resolvePopulateStep: () => resolvePopulateStep,
|
|
90
100
|
serializeIndexKey: () => serializeIndexKey,
|
|
91
101
|
syncIndexes: () => syncIndexes,
|
|
92
102
|
toCompoundIndexSpec: () => toCompoundIndexSpec,
|
|
93
103
|
toFieldIndexSpec: () => toFieldIndexSpec,
|
|
104
|
+
unwrapRefSchema: () => unwrapRefSchema,
|
|
94
105
|
updateMany: () => updateMany,
|
|
95
106
|
updateOne: () => updateOne,
|
|
96
107
|
wrapMongoError: () => wrapMongoError
|
|
@@ -110,30 +121,24 @@ var $avg = (field) => ({
|
|
|
110
121
|
__accum: true,
|
|
111
122
|
expr: { $avg: field }
|
|
112
123
|
});
|
|
113
|
-
|
|
114
|
-
__accum: true,
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
expr: { $
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
__accum: true,
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
expr: { $push: field }
|
|
132
|
-
});
|
|
133
|
-
var $addToSet = (field) => ({
|
|
134
|
-
__accum: true,
|
|
135
|
-
expr: { $addToSet: field }
|
|
136
|
-
});
|
|
124
|
+
function $min(field) {
|
|
125
|
+
return { __accum: true, expr: { $min: field } };
|
|
126
|
+
}
|
|
127
|
+
function $max(field) {
|
|
128
|
+
return { __accum: true, expr: { $max: field } };
|
|
129
|
+
}
|
|
130
|
+
function $first(field) {
|
|
131
|
+
return { __accum: true, expr: { $first: field } };
|
|
132
|
+
}
|
|
133
|
+
function $last(field) {
|
|
134
|
+
return { __accum: true, expr: { $last: field } };
|
|
135
|
+
}
|
|
136
|
+
function $push(field) {
|
|
137
|
+
return { __accum: true, expr: { $push: field } };
|
|
138
|
+
}
|
|
139
|
+
function $addToSet(field) {
|
|
140
|
+
return { __accum: true, expr: { $addToSet: field } };
|
|
141
|
+
}
|
|
137
142
|
function createAccumulatorBuilder() {
|
|
138
143
|
return {
|
|
139
144
|
count: () => ({ __accum: true, expr: { $sum: 1 } }),
|
|
@@ -151,50 +156,84 @@ function createAccumulatorBuilder() {
|
|
|
151
156
|
// 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.
|
|
152
157
|
};
|
|
153
158
|
}
|
|
159
|
+
var isExpr = (v) => typeof v === "object" && v !== null && v.__expr === true;
|
|
154
160
|
function createExpressionBuilder() {
|
|
155
|
-
const
|
|
156
|
-
|
|
161
|
+
const resolveArg = (arg) => {
|
|
162
|
+
if (typeof arg === "number") return arg;
|
|
163
|
+
if (isExpr(arg)) return arg.value;
|
|
164
|
+
return `$${arg}`;
|
|
165
|
+
};
|
|
166
|
+
const resolveExprVal = (v) => isExpr(v) ? v.value : v;
|
|
157
167
|
const expr = (value) => ({ __expr: true, value });
|
|
158
168
|
return {
|
|
159
169
|
// Arithmetic
|
|
160
|
-
add: (
|
|
161
|
-
subtract: (
|
|
162
|
-
multiply: (
|
|
163
|
-
divide: (
|
|
164
|
-
mod: (
|
|
165
|
-
abs: (field) => expr({ $abs:
|
|
166
|
-
ceil: (field) => expr({ $ceil:
|
|
167
|
-
floor: (field) => expr({ $floor:
|
|
168
|
-
round: (field, place = 0) => expr({ $round: [
|
|
170
|
+
add: (a, b) => expr({ $add: [resolveArg(a), resolveArg(b)] }),
|
|
171
|
+
subtract: (a, b) => expr({ $subtract: [resolveArg(a), resolveArg(b)] }),
|
|
172
|
+
multiply: (a, b) => expr({ $multiply: [resolveArg(a), resolveArg(b)] }),
|
|
173
|
+
divide: (a, b) => expr({ $divide: [resolveArg(a), resolveArg(b)] }),
|
|
174
|
+
mod: (a, b) => expr({ $mod: [resolveArg(a), resolveArg(b)] }),
|
|
175
|
+
abs: (field) => expr({ $abs: resolveArg(field) }),
|
|
176
|
+
ceil: (field) => expr({ $ceil: resolveArg(field) }),
|
|
177
|
+
floor: (field) => expr({ $floor: resolveArg(field) }),
|
|
178
|
+
round: (field, place = 0) => expr({ $round: [resolveArg(field), place] }),
|
|
169
179
|
// String
|
|
170
180
|
concat: (...parts) => {
|
|
171
181
|
const resolved = parts.map((p) => {
|
|
172
|
-
if (
|
|
182
|
+
if (isExpr(p)) return p.value;
|
|
183
|
+
if (/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(p)) return `$${p}`;
|
|
173
184
|
return p;
|
|
174
185
|
});
|
|
175
186
|
return expr({ $concat: resolved });
|
|
176
187
|
},
|
|
177
|
-
toLower: (field) => expr({ $toLower:
|
|
178
|
-
toUpper: (field) => expr({ $toUpper:
|
|
179
|
-
trim: (field) => expr({ $trim: { input:
|
|
180
|
-
substr: (field, start, length) => expr({ $substrBytes: [
|
|
181
|
-
// Comparison
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
+
toLower: (field) => expr({ $toLower: resolveArg(field) }),
|
|
189
|
+
toUpper: (field) => expr({ $toUpper: resolveArg(field) }),
|
|
190
|
+
trim: (field) => expr({ $trim: { input: resolveArg(field) } }),
|
|
191
|
+
substr: (field, start, length) => expr({ $substrBytes: [resolveArg(field), start, length] }),
|
|
192
|
+
// Comparison — single runtime implementation handles both overloads:
|
|
193
|
+
// field path → resolveArg('name') → '$name'
|
|
194
|
+
// expression → resolveArg(expr.sub(...)) → { $subtract: [...] }
|
|
195
|
+
eq: (field, value) => expr({ $eq: [resolveArg(field), resolveExprVal(value)] }),
|
|
196
|
+
gt: (field, value) => expr({ $gt: [resolveArg(field), resolveExprVal(value)] }),
|
|
197
|
+
gte: (field, value) => expr({ $gte: [resolveArg(field), resolveExprVal(value)] }),
|
|
198
|
+
lt: (field, value) => expr({ $lt: [resolveArg(field), resolveExprVal(value)] }),
|
|
199
|
+
lte: (field, value) => expr({ $lte: [resolveArg(field), resolveExprVal(value)] }),
|
|
200
|
+
ne: (field, value) => expr({ $ne: [resolveArg(field), resolveExprVal(value)] }),
|
|
188
201
|
// Date
|
|
189
|
-
year: (field) => expr({ $year:
|
|
190
|
-
month: (field) => expr({ $month:
|
|
191
|
-
dayOfMonth: (field) => expr({ $dayOfMonth:
|
|
202
|
+
year: (field) => expr({ $year: resolveArg(field) }),
|
|
203
|
+
month: (field) => expr({ $month: resolveArg(field) }),
|
|
204
|
+
dayOfMonth: (field) => expr({ $dayOfMonth: resolveArg(field) }),
|
|
192
205
|
// Array
|
|
193
|
-
size: (field) => expr({ $size:
|
|
206
|
+
size: (field) => expr({ $size: resolveArg(field) }),
|
|
194
207
|
// Conditional
|
|
195
|
-
cond: (condition, thenValue, elseValue) => expr({
|
|
196
|
-
|
|
197
|
-
|
|
208
|
+
cond: (condition, thenValue, elseValue) => expr({
|
|
209
|
+
$cond: [condition.value, resolveExprVal(thenValue), resolveExprVal(elseValue)]
|
|
210
|
+
}),
|
|
211
|
+
ifNull: (field, fallback) => expr({ $ifNull: [resolveArg(field), fallback] }),
|
|
212
|
+
// Date (extended)
|
|
213
|
+
dayOfWeek: (field) => expr({ $dayOfWeek: resolveArg(field) }),
|
|
214
|
+
dateToString: (field, format) => expr({ $dateToString: { format, date: resolveArg(field) } }),
|
|
215
|
+
// $$NOW is a MongoDB system variable string — not a Document, but valid anywhere
|
|
216
|
+
// an aggregation expression is expected. Cast is safe; the MongoDB driver accepts it.
|
|
217
|
+
now: () => ({ __expr: true, value: "$$NOW" }),
|
|
218
|
+
// String conversion
|
|
219
|
+
toString: (field) => expr({ $toString: resolveArg(field) }),
|
|
220
|
+
// Array (extended)
|
|
221
|
+
inArray: (value, array) => expr({ $in: [resolveArg(value), Array.isArray(array) ? array : resolveArg(array)] }),
|
|
222
|
+
arrayElemAt: (field, index2) => expr({ $arrayElemAt: [resolveArg(field), index2] }),
|
|
223
|
+
// Conditional (extended)
|
|
224
|
+
switch: (branches, fallback) => expr({
|
|
225
|
+
$switch: {
|
|
226
|
+
branches: branches.map((b) => ({
|
|
227
|
+
case: b.case.value,
|
|
228
|
+
// biome-ignore lint/suspicious/noThenProperty: MongoDB $switch branch object requires a `then` key
|
|
229
|
+
then: resolveExprVal(b.then)
|
|
230
|
+
})),
|
|
231
|
+
default: resolveExprVal(fallback)
|
|
232
|
+
}
|
|
233
|
+
}),
|
|
234
|
+
// Field reference
|
|
235
|
+
field: (name) => ({ __expr: true, value: `$${name}` })
|
|
236
|
+
// biome-ignore lint/suspicious/noExplicitAny: Runtime implementation uses resolveArg/resolveExprVal — TypeScript cannot verify generic ExpressionBuilder<T> return types match. Safe because type resolution happens at compile time via ExpressionBuilder<T>.
|
|
198
237
|
};
|
|
199
238
|
}
|
|
200
239
|
|
|
@@ -209,9 +248,9 @@ var ZodmonError = class extends Error {
|
|
|
209
248
|
/** The underlying error that caused this error, if any. */
|
|
210
249
|
cause;
|
|
211
250
|
constructor(message, collection2, options) {
|
|
212
|
-
super(message);
|
|
251
|
+
super(message, options?.cause !== void 0 ? { cause: options.cause } : void 0);
|
|
213
252
|
this.collection = collection2;
|
|
214
|
-
if (options?.cause) {
|
|
253
|
+
if (options?.cause !== void 0) {
|
|
215
254
|
this.cause = options.cause;
|
|
216
255
|
}
|
|
217
256
|
}
|
|
@@ -470,10 +509,12 @@ var AggregatePipeline = class _AggregatePipeline {
|
|
|
470
509
|
definition;
|
|
471
510
|
nativeCollection;
|
|
472
511
|
stages;
|
|
473
|
-
|
|
512
|
+
session;
|
|
513
|
+
constructor(definition, nativeCollection, stages, session) {
|
|
474
514
|
this.definition = definition;
|
|
475
515
|
this.nativeCollection = nativeCollection;
|
|
476
516
|
this.stages = stages;
|
|
517
|
+
this.session = session;
|
|
477
518
|
}
|
|
478
519
|
/**
|
|
479
520
|
* Append an arbitrary aggregation stage to the pipeline (escape hatch).
|
|
@@ -504,10 +545,12 @@ var AggregatePipeline = class _AggregatePipeline {
|
|
|
504
545
|
* ```
|
|
505
546
|
*/
|
|
506
547
|
raw(stage) {
|
|
507
|
-
return new _AggregatePipeline(
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
548
|
+
return new _AggregatePipeline(
|
|
549
|
+
this.definition,
|
|
550
|
+
this.nativeCollection,
|
|
551
|
+
[...this.stages, stage],
|
|
552
|
+
this.session
|
|
553
|
+
);
|
|
511
554
|
}
|
|
512
555
|
/**
|
|
513
556
|
* Execute the pipeline and return all results as an array.
|
|
@@ -523,7 +566,10 @@ var AggregatePipeline = class _AggregatePipeline {
|
|
|
523
566
|
*/
|
|
524
567
|
async toArray() {
|
|
525
568
|
try {
|
|
526
|
-
const cursor = this.nativeCollection.aggregate(
|
|
569
|
+
const cursor = this.nativeCollection.aggregate(
|
|
570
|
+
this.stages,
|
|
571
|
+
this.session ? { session: this.session } : {}
|
|
572
|
+
);
|
|
527
573
|
return await cursor.toArray();
|
|
528
574
|
} catch (err) {
|
|
529
575
|
wrapMongoError(err, this.definition.name);
|
|
@@ -543,7 +589,10 @@ var AggregatePipeline = class _AggregatePipeline {
|
|
|
543
589
|
*/
|
|
544
590
|
async *[Symbol.asyncIterator]() {
|
|
545
591
|
try {
|
|
546
|
-
const cursor = this.nativeCollection.aggregate(
|
|
592
|
+
const cursor = this.nativeCollection.aggregate(
|
|
593
|
+
this.stages,
|
|
594
|
+
this.session ? { session: this.session } : {}
|
|
595
|
+
);
|
|
547
596
|
for await (const doc of cursor) {
|
|
548
597
|
yield doc;
|
|
549
598
|
}
|
|
@@ -569,7 +618,10 @@ var AggregatePipeline = class _AggregatePipeline {
|
|
|
569
618
|
*/
|
|
570
619
|
async explain() {
|
|
571
620
|
try {
|
|
572
|
-
const cursor = this.nativeCollection.aggregate(
|
|
621
|
+
const cursor = this.nativeCollection.aggregate(
|
|
622
|
+
this.stages,
|
|
623
|
+
this.session ? { session: this.session } : {}
|
|
624
|
+
);
|
|
573
625
|
return await cursor.explain();
|
|
574
626
|
} catch (err) {
|
|
575
627
|
wrapMongoError(err, this.definition.name);
|
|
@@ -619,12 +671,30 @@ var AggregatePipeline = class _AggregatePipeline {
|
|
|
619
671
|
* .toArray()
|
|
620
672
|
* // subset[0].role → 'engineer' | 'designer'
|
|
621
673
|
* ```
|
|
674
|
+
*
|
|
675
|
+
* @example
|
|
676
|
+
* ```ts
|
|
677
|
+
* // Field-vs-field comparison via $expr callback
|
|
678
|
+
* const overRefunded = await orders.aggregate()
|
|
679
|
+
* .match(
|
|
680
|
+
* { status: 'completed' },
|
|
681
|
+
* (expr) => expr.gt('totalAmount', expr.field('refundedAmount')),
|
|
682
|
+
* )
|
|
683
|
+
* .toArray()
|
|
684
|
+
* ```
|
|
622
685
|
*/
|
|
623
|
-
match(filter) {
|
|
624
|
-
const
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
686
|
+
match(filter, exprCb) {
|
|
687
|
+
const stage = { ...filter };
|
|
688
|
+
if (exprCb) {
|
|
689
|
+
const built = exprCb(createExpressionBuilder());
|
|
690
|
+
stage["$expr"] = built.value;
|
|
691
|
+
}
|
|
692
|
+
const pipeline = new _AggregatePipeline(
|
|
693
|
+
this.definition,
|
|
694
|
+
this.nativeCollection,
|
|
695
|
+
[...this.stages, { $match: stage }],
|
|
696
|
+
this.session
|
|
697
|
+
);
|
|
628
698
|
return pipeline;
|
|
629
699
|
}
|
|
630
700
|
/**
|
|
@@ -644,10 +714,12 @@ var AggregatePipeline = class _AggregatePipeline {
|
|
|
644
714
|
* ```
|
|
645
715
|
*/
|
|
646
716
|
sort(spec) {
|
|
647
|
-
return new _AggregatePipeline(
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
717
|
+
return new _AggregatePipeline(
|
|
718
|
+
this.definition,
|
|
719
|
+
this.nativeCollection,
|
|
720
|
+
[...this.stages, { $sort: spec }],
|
|
721
|
+
this.session
|
|
722
|
+
);
|
|
651
723
|
}
|
|
652
724
|
/**
|
|
653
725
|
* Skip a number of documents in the pipeline.
|
|
@@ -668,10 +740,12 @@ var AggregatePipeline = class _AggregatePipeline {
|
|
|
668
740
|
* ```
|
|
669
741
|
*/
|
|
670
742
|
skip(n) {
|
|
671
|
-
return new _AggregatePipeline(
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
743
|
+
return new _AggregatePipeline(
|
|
744
|
+
this.definition,
|
|
745
|
+
this.nativeCollection,
|
|
746
|
+
[...this.stages, { $skip: n }],
|
|
747
|
+
this.session
|
|
748
|
+
);
|
|
675
749
|
}
|
|
676
750
|
/**
|
|
677
751
|
* Limit the number of documents passing through the pipeline.
|
|
@@ -691,10 +765,12 @@ var AggregatePipeline = class _AggregatePipeline {
|
|
|
691
765
|
* ```
|
|
692
766
|
*/
|
|
693
767
|
limit(n) {
|
|
694
|
-
return new _AggregatePipeline(
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
768
|
+
return new _AggregatePipeline(
|
|
769
|
+
this.definition,
|
|
770
|
+
this.nativeCollection,
|
|
771
|
+
[...this.stages, { $limit: n }],
|
|
772
|
+
this.session
|
|
773
|
+
);
|
|
698
774
|
}
|
|
699
775
|
// ── Shape-transforming projection stages ─────────────────────────
|
|
700
776
|
/**
|
|
@@ -716,10 +792,12 @@ var AggregatePipeline = class _AggregatePipeline {
|
|
|
716
792
|
* ```
|
|
717
793
|
*/
|
|
718
794
|
project(spec) {
|
|
719
|
-
const pipeline = new _AggregatePipeline(
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
795
|
+
const pipeline = new _AggregatePipeline(
|
|
796
|
+
this.definition,
|
|
797
|
+
this.nativeCollection,
|
|
798
|
+
[...this.stages, { $project: spec }],
|
|
799
|
+
this.session
|
|
800
|
+
);
|
|
723
801
|
return pipeline;
|
|
724
802
|
}
|
|
725
803
|
/**
|
|
@@ -740,10 +818,12 @@ var AggregatePipeline = class _AggregatePipeline {
|
|
|
740
818
|
*/
|
|
741
819
|
pick(...fields) {
|
|
742
820
|
const spec = Object.fromEntries(fields.map((f) => [f, 1]));
|
|
743
|
-
const pipeline = new _AggregatePipeline(
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
821
|
+
const pipeline = new _AggregatePipeline(
|
|
822
|
+
this.definition,
|
|
823
|
+
this.nativeCollection,
|
|
824
|
+
[...this.stages, { $project: spec }],
|
|
825
|
+
this.session
|
|
826
|
+
);
|
|
747
827
|
return pipeline;
|
|
748
828
|
}
|
|
749
829
|
/**
|
|
@@ -764,22 +844,41 @@ var AggregatePipeline = class _AggregatePipeline {
|
|
|
764
844
|
*/
|
|
765
845
|
omit(...fields) {
|
|
766
846
|
const spec = Object.fromEntries(fields.map((f) => [f, 0]));
|
|
767
|
-
const pipeline = new _AggregatePipeline(
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
847
|
+
const pipeline = new _AggregatePipeline(
|
|
848
|
+
this.definition,
|
|
849
|
+
this.nativeCollection,
|
|
850
|
+
[...this.stages, { $project: spec }],
|
|
851
|
+
this.session
|
|
852
|
+
);
|
|
771
853
|
return pipeline;
|
|
772
854
|
}
|
|
773
855
|
groupBy(field, accumulators) {
|
|
774
856
|
const resolved = typeof accumulators === "function" ? accumulators(createAccumulatorBuilder()) : accumulators;
|
|
775
|
-
|
|
857
|
+
let _id;
|
|
858
|
+
if (field === null) {
|
|
859
|
+
_id = null;
|
|
860
|
+
} else if (Array.isArray(field)) {
|
|
861
|
+
const entries = field.map((f) => [f.replaceAll(".", "_"), `$${f}`]);
|
|
862
|
+
const keys = entries.map(([k]) => k);
|
|
863
|
+
const dupes = keys.filter((k, i) => keys.indexOf(k) !== i);
|
|
864
|
+
if (dupes.length > 0) {
|
|
865
|
+
throw new Error(
|
|
866
|
+
`Compound groupBy key collision: ${dupes.join(", ")}. Two or more fields produce the same _id key after dot-to-underscore conversion. Use raw() with explicit aliases instead.`
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
_id = Object.fromEntries(entries);
|
|
870
|
+
} else {
|
|
871
|
+
_id = `$${field}`;
|
|
872
|
+
}
|
|
776
873
|
const accumExprs = Object.fromEntries(
|
|
777
874
|
Object.entries(resolved).map(([key, acc]) => [key, acc.expr])
|
|
778
875
|
);
|
|
779
|
-
const pipeline = new _AggregatePipeline(
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
876
|
+
const pipeline = new _AggregatePipeline(
|
|
877
|
+
this.definition,
|
|
878
|
+
this.nativeCollection,
|
|
879
|
+
[...this.stages, { $group: { _id, ...accumExprs } }],
|
|
880
|
+
this.session
|
|
881
|
+
);
|
|
783
882
|
return pipeline;
|
|
784
883
|
}
|
|
785
884
|
// Implementation
|
|
@@ -791,10 +890,12 @@ var AggregatePipeline = class _AggregatePipeline {
|
|
|
791
890
|
v && typeof v === "object" && "__expr" in v ? v.value : v
|
|
792
891
|
])
|
|
793
892
|
);
|
|
794
|
-
const pipeline = new _AggregatePipeline(
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
893
|
+
const pipeline = new _AggregatePipeline(
|
|
894
|
+
this.definition,
|
|
895
|
+
this.nativeCollection,
|
|
896
|
+
[...this.stages, { $addFields: stage }],
|
|
897
|
+
this.session
|
|
898
|
+
);
|
|
798
899
|
return pipeline;
|
|
799
900
|
}
|
|
800
901
|
// ── unwind stage ─────────────────────────────────────────────────
|
|
@@ -820,10 +921,12 @@ var AggregatePipeline = class _AggregatePipeline {
|
|
|
820
921
|
*/
|
|
821
922
|
unwind(field, options) {
|
|
822
923
|
const stage = options?.preserveEmpty ? { $unwind: { path: `$${field}`, preserveNullAndEmptyArrays: true } } : { $unwind: `$${field}` };
|
|
823
|
-
const pipeline = new _AggregatePipeline(
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
924
|
+
const pipeline = new _AggregatePipeline(
|
|
925
|
+
this.definition,
|
|
926
|
+
this.nativeCollection,
|
|
927
|
+
[...this.stages, stage],
|
|
928
|
+
this.session
|
|
929
|
+
);
|
|
827
930
|
return pipeline;
|
|
828
931
|
}
|
|
829
932
|
lookup(fieldOrFrom, options) {
|
|
@@ -871,7 +974,63 @@ var AggregatePipeline = class _AggregatePipeline {
|
|
|
871
974
|
stages.push({ $unwind: { path: `$${asField}`, preserveNullAndEmptyArrays: true } });
|
|
872
975
|
}
|
|
873
976
|
}
|
|
874
|
-
const pipeline = new _AggregatePipeline(
|
|
977
|
+
const pipeline = new _AggregatePipeline(
|
|
978
|
+
this.definition,
|
|
979
|
+
this.nativeCollection,
|
|
980
|
+
stages,
|
|
981
|
+
this.session
|
|
982
|
+
);
|
|
983
|
+
return pipeline;
|
|
984
|
+
}
|
|
985
|
+
// ── facet stage ──────────────────────────────────────────────────
|
|
986
|
+
/**
|
|
987
|
+
* Run multiple sub-pipelines on the same input documents in parallel.
|
|
988
|
+
*
|
|
989
|
+
* Each key in `spec` maps to a callback that receives a fresh `SubPipeline`
|
|
990
|
+
* starting from `TOutput`. The callback chains stages and returns the terminal
|
|
991
|
+
* pipeline. Zodmon extracts the accumulated stages at runtime to build the
|
|
992
|
+
* `$facet` document. The output type is fully inferred — no annotation needed.
|
|
993
|
+
*
|
|
994
|
+
* Sub-pipelines support all stage methods including `.raw()` for operators not
|
|
995
|
+
* yet first-class. Execution methods (`toArray`, `explain`) are not available
|
|
996
|
+
* inside branches.
|
|
997
|
+
*
|
|
998
|
+
* @param spec - An object mapping branch names to sub-pipeline builder callbacks.
|
|
999
|
+
* @returns A new pipeline whose output is one document with each branch name mapped to an array of results.
|
|
1000
|
+
*
|
|
1001
|
+
* @example
|
|
1002
|
+
* ```ts
|
|
1003
|
+
* const [report] = await aggregate(orders)
|
|
1004
|
+
* .facet({
|
|
1005
|
+
* byCategory: (sub) => sub
|
|
1006
|
+
* .groupBy('category', acc => ({ count: acc.count() }))
|
|
1007
|
+
* .sort({ count: -1 }),
|
|
1008
|
+
* totals: (sub) => sub
|
|
1009
|
+
* .groupBy(null, acc => ({ grandTotal: acc.sum('amount') })),
|
|
1010
|
+
* })
|
|
1011
|
+
* .toArray()
|
|
1012
|
+
* // report.byCategory → { _id: 'electronics' | 'books' | 'clothing'; count: number }[]
|
|
1013
|
+
* // report.totals → { _id: null; grandTotal: number }[]
|
|
1014
|
+
* ```
|
|
1015
|
+
*/
|
|
1016
|
+
facet(spec) {
|
|
1017
|
+
const branches = {};
|
|
1018
|
+
for (const [key, cb] of Object.entries(spec)) {
|
|
1019
|
+
const sub = new _AggregatePipeline(
|
|
1020
|
+
this.definition,
|
|
1021
|
+
this.nativeCollection,
|
|
1022
|
+
[],
|
|
1023
|
+
this.session
|
|
1024
|
+
// biome-ignore lint/suspicious/noExplicitAny: sub must be cast to `any` so the concrete `AggregatePipeline<TDef, TOutput>` is accepted where `SubPipeline<TDef, TOutput>` (which lacks execution methods) is expected — safe at runtime because the pipeline instance always has the right shape
|
|
1025
|
+
);
|
|
1026
|
+
branches[key] = cb(sub).getStages();
|
|
1027
|
+
}
|
|
1028
|
+
const pipeline = new _AggregatePipeline(
|
|
1029
|
+
this.definition,
|
|
1030
|
+
this.nativeCollection,
|
|
1031
|
+
[...this.stages, { $facet: branches }],
|
|
1032
|
+
this.session
|
|
1033
|
+
);
|
|
875
1034
|
return pipeline;
|
|
876
1035
|
}
|
|
877
1036
|
// ── Convenience shortcuts ────────────────────────────────────────
|
|
@@ -892,11 +1051,16 @@ var AggregatePipeline = class _AggregatePipeline {
|
|
|
892
1051
|
* ```
|
|
893
1052
|
*/
|
|
894
1053
|
countBy(field) {
|
|
895
|
-
const pipeline = new _AggregatePipeline(
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
1054
|
+
const pipeline = new _AggregatePipeline(
|
|
1055
|
+
this.definition,
|
|
1056
|
+
this.nativeCollection,
|
|
1057
|
+
[
|
|
1058
|
+
...this.stages,
|
|
1059
|
+
{ $group: { _id: `$${field}`, count: { $sum: 1 } } },
|
|
1060
|
+
{ $sort: { count: -1 } }
|
|
1061
|
+
],
|
|
1062
|
+
this.session
|
|
1063
|
+
);
|
|
900
1064
|
return pipeline;
|
|
901
1065
|
}
|
|
902
1066
|
/**
|
|
@@ -917,11 +1081,16 @@ var AggregatePipeline = class _AggregatePipeline {
|
|
|
917
1081
|
* ```
|
|
918
1082
|
*/
|
|
919
1083
|
sumBy(field, sumField) {
|
|
920
|
-
const pipeline = new _AggregatePipeline(
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
1084
|
+
const pipeline = new _AggregatePipeline(
|
|
1085
|
+
this.definition,
|
|
1086
|
+
this.nativeCollection,
|
|
1087
|
+
[
|
|
1088
|
+
...this.stages,
|
|
1089
|
+
{ $group: { _id: `$${field}`, total: { $sum: `$${sumField}` } } },
|
|
1090
|
+
{ $sort: { total: -1 } }
|
|
1091
|
+
],
|
|
1092
|
+
this.session
|
|
1093
|
+
);
|
|
925
1094
|
return pipeline;
|
|
926
1095
|
}
|
|
927
1096
|
/**
|
|
@@ -941,10 +1110,12 @@ var AggregatePipeline = class _AggregatePipeline {
|
|
|
941
1110
|
* ```
|
|
942
1111
|
*/
|
|
943
1112
|
sortBy(field, direction = "asc") {
|
|
944
|
-
return new _AggregatePipeline(
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1113
|
+
return new _AggregatePipeline(
|
|
1114
|
+
this.definition,
|
|
1115
|
+
this.nativeCollection,
|
|
1116
|
+
[...this.stages, { $sort: { [field]: direction === "desc" ? -1 : 1 } }],
|
|
1117
|
+
this.session
|
|
1118
|
+
);
|
|
948
1119
|
}
|
|
949
1120
|
/**
|
|
950
1121
|
* Return the top N documents sorted by a field descending.
|
|
@@ -963,11 +1134,12 @@ var AggregatePipeline = class _AggregatePipeline {
|
|
|
963
1134
|
* ```
|
|
964
1135
|
*/
|
|
965
1136
|
top(n, options) {
|
|
966
|
-
return new _AggregatePipeline(
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
{ $limit: n }
|
|
970
|
-
|
|
1137
|
+
return new _AggregatePipeline(
|
|
1138
|
+
this.definition,
|
|
1139
|
+
this.nativeCollection,
|
|
1140
|
+
[...this.stages, { $sort: { [options.by]: -1 } }, { $limit: n }],
|
|
1141
|
+
this.session
|
|
1142
|
+
);
|
|
971
1143
|
}
|
|
972
1144
|
/**
|
|
973
1145
|
* Return the bottom N documents sorted by a field ascending.
|
|
@@ -986,15 +1158,25 @@ var AggregatePipeline = class _AggregatePipeline {
|
|
|
986
1158
|
* ```
|
|
987
1159
|
*/
|
|
988
1160
|
bottom(n, options) {
|
|
989
|
-
return new _AggregatePipeline(
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
{ $limit: n }
|
|
993
|
-
|
|
1161
|
+
return new _AggregatePipeline(
|
|
1162
|
+
this.definition,
|
|
1163
|
+
this.nativeCollection,
|
|
1164
|
+
[...this.stages, { $sort: { [options.by]: 1 } }, { $limit: n }],
|
|
1165
|
+
this.session
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
/** @internal Used by facet() to extract branch stages. Not part of the public API. */
|
|
1169
|
+
getStages() {
|
|
1170
|
+
return this.stages;
|
|
994
1171
|
}
|
|
995
1172
|
};
|
|
996
1173
|
function aggregate(handle) {
|
|
997
|
-
return new AggregatePipeline(
|
|
1174
|
+
return new AggregatePipeline(
|
|
1175
|
+
handle.definition,
|
|
1176
|
+
handle.native,
|
|
1177
|
+
[],
|
|
1178
|
+
handle.session
|
|
1179
|
+
);
|
|
998
1180
|
}
|
|
999
1181
|
|
|
1000
1182
|
// src/client/client.ts
|
|
@@ -1176,6 +1358,36 @@ async function syncIndexes(handle, options) {
|
|
|
1176
1358
|
};
|
|
1177
1359
|
}
|
|
1178
1360
|
|
|
1361
|
+
// src/transaction/transaction.ts
|
|
1362
|
+
var TransactionContext = class {
|
|
1363
|
+
/** @internal */
|
|
1364
|
+
session;
|
|
1365
|
+
/** @internal */
|
|
1366
|
+
constructor(session) {
|
|
1367
|
+
this.session = session;
|
|
1368
|
+
}
|
|
1369
|
+
/**
|
|
1370
|
+
* Bind a collection handle to this transaction's session.
|
|
1371
|
+
*
|
|
1372
|
+
* Returns a cloned handle whose CRUD operations automatically include
|
|
1373
|
+
* the transaction session. The original handle is not modified.
|
|
1374
|
+
*
|
|
1375
|
+
* @param handle - An existing collection handle from `db.use()`.
|
|
1376
|
+
* @returns A new handle bound to the transaction session.
|
|
1377
|
+
*
|
|
1378
|
+
* @example
|
|
1379
|
+
* ```ts
|
|
1380
|
+
* await db.transaction(async (tx) => {
|
|
1381
|
+
* const txUsers = tx.use(users)
|
|
1382
|
+
* await txUsers.insertOne({ name: 'Ada' })
|
|
1383
|
+
* })
|
|
1384
|
+
* ```
|
|
1385
|
+
*/
|
|
1386
|
+
use(handle) {
|
|
1387
|
+
return handle.withSession(this.session);
|
|
1388
|
+
}
|
|
1389
|
+
};
|
|
1390
|
+
|
|
1179
1391
|
// src/crud/delete.ts
|
|
1180
1392
|
var import_zod2 = require("zod");
|
|
1181
1393
|
|
|
@@ -1200,14 +1412,22 @@ var ZodmonValidationError = class extends ZodmonError {
|
|
|
1200
1412
|
// src/crud/delete.ts
|
|
1201
1413
|
async function deleteOne(handle, filter) {
|
|
1202
1414
|
try {
|
|
1203
|
-
return await handle.native.deleteOne(
|
|
1415
|
+
return await handle.native.deleteOne(
|
|
1416
|
+
// biome-ignore lint/suspicious/noExplicitAny: TypedFilter intersection type is not directly assignable to MongoDB's Filter
|
|
1417
|
+
filter,
|
|
1418
|
+
handle.session ? { session: handle.session } : {}
|
|
1419
|
+
);
|
|
1204
1420
|
} catch (err) {
|
|
1205
1421
|
wrapMongoError(err, handle.definition.name);
|
|
1206
1422
|
}
|
|
1207
1423
|
}
|
|
1208
1424
|
async function deleteMany(handle, filter) {
|
|
1209
1425
|
try {
|
|
1210
|
-
return await handle.native.deleteMany(
|
|
1426
|
+
return await handle.native.deleteMany(
|
|
1427
|
+
// biome-ignore lint/suspicious/noExplicitAny: TypedFilter intersection type is not directly assignable to MongoDB's Filter
|
|
1428
|
+
filter,
|
|
1429
|
+
handle.session ? { session: handle.session } : {}
|
|
1430
|
+
);
|
|
1211
1431
|
} catch (err) {
|
|
1212
1432
|
wrapMongoError(err, handle.definition.name);
|
|
1213
1433
|
}
|
|
@@ -1218,7 +1438,7 @@ async function findOneAndDelete(handle, filter, options) {
|
|
|
1218
1438
|
result = await handle.native.findOneAndDelete(
|
|
1219
1439
|
// biome-ignore lint/suspicious/noExplicitAny: TypedFilter intersection type is not directly assignable to MongoDB's Filter
|
|
1220
1440
|
filter,
|
|
1221
|
-
{ includeResultMetadata: false }
|
|
1441
|
+
handle.session ? { includeResultMetadata: false, session: handle.session } : { includeResultMetadata: false }
|
|
1222
1442
|
);
|
|
1223
1443
|
} catch (err) {
|
|
1224
1444
|
wrapMongoError(err, handle.definition.name);
|
|
@@ -1229,7 +1449,8 @@ async function findOneAndDelete(handle, filter, options) {
|
|
|
1229
1449
|
return result;
|
|
1230
1450
|
}
|
|
1231
1451
|
try {
|
|
1232
|
-
|
|
1452
|
+
const schema = mode === "strict" ? handle.definition.strictSchema : handle.definition.schema;
|
|
1453
|
+
return schema.parse(result);
|
|
1233
1454
|
} catch (err) {
|
|
1234
1455
|
if (err instanceof import_zod2.z.ZodError) {
|
|
1235
1456
|
throw new ZodmonValidationError(handle.definition.name, err, result);
|
|
@@ -1239,7 +1460,7 @@ async function findOneAndDelete(handle, filter, options) {
|
|
|
1239
1460
|
}
|
|
1240
1461
|
|
|
1241
1462
|
// src/crud/find.ts
|
|
1242
|
-
var
|
|
1463
|
+
var import_zod5 = require("zod");
|
|
1243
1464
|
|
|
1244
1465
|
// src/errors/not-found.ts
|
|
1245
1466
|
var ZodmonNotFoundError = class extends ZodmonError {
|
|
@@ -1276,7 +1497,7 @@ function checkUnindexedFields(definition, filter) {
|
|
|
1276
1497
|
}
|
|
1277
1498
|
|
|
1278
1499
|
// src/query/cursor.ts
|
|
1279
|
-
var
|
|
1500
|
+
var import_zod4 = require("zod");
|
|
1280
1501
|
|
|
1281
1502
|
// src/crud/paginate.ts
|
|
1282
1503
|
var import_mongodb2 = require("mongodb");
|
|
@@ -1339,11 +1560,313 @@ function resolveSortKeys(sortSpec) {
|
|
|
1339
1560
|
return entries;
|
|
1340
1561
|
}
|
|
1341
1562
|
|
|
1563
|
+
// src/populate/builder.ts
|
|
1564
|
+
var PopulateRefBuilder = class {
|
|
1565
|
+
/**
|
|
1566
|
+
* Declare a projection to apply when fetching the referenced documents.
|
|
1567
|
+
*
|
|
1568
|
+
* Supported: inclusion (`{ name: 1 }`), exclusion (`{ email: 0 }`), or
|
|
1569
|
+
* `_id` suppression (`{ name: 1, _id: 0 }`).
|
|
1570
|
+
*
|
|
1571
|
+
* @param projection - MongoDB-style inclusion or exclusion projection.
|
|
1572
|
+
* @returns A config object carrying the projection type for compile-time narrowing.
|
|
1573
|
+
*
|
|
1574
|
+
* @example
|
|
1575
|
+
* ```ts
|
|
1576
|
+
* (b) => b.project({ name: 1, email: 1 })
|
|
1577
|
+
* (b) => b.project({ password: 0 })
|
|
1578
|
+
* ```
|
|
1579
|
+
*/
|
|
1580
|
+
project(projection) {
|
|
1581
|
+
return { projection };
|
|
1582
|
+
}
|
|
1583
|
+
};
|
|
1584
|
+
|
|
1585
|
+
// src/populate/execute.ts
|
|
1586
|
+
var import_zod3 = require("zod");
|
|
1587
|
+
function unwrapRefSchema(schema) {
|
|
1588
|
+
const def = schema._zod.def;
|
|
1589
|
+
if (def && typeof def === "object") {
|
|
1590
|
+
if ("innerType" in def && def.innerType instanceof import_zod3.z.ZodType) {
|
|
1591
|
+
return unwrapRefSchema(def.innerType);
|
|
1592
|
+
}
|
|
1593
|
+
if ("element" in def && def.element instanceof import_zod3.z.ZodType) {
|
|
1594
|
+
return unwrapRefSchema(def.element);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
return schema;
|
|
1598
|
+
}
|
|
1599
|
+
function resolveRefField(shape, fieldName, collectionName) {
|
|
1600
|
+
const fieldSchema = shape[fieldName];
|
|
1601
|
+
if (!fieldSchema) {
|
|
1602
|
+
throw new Error(
|
|
1603
|
+
`[zodmon] populate: field '${fieldName}' does not exist on collection '${collectionName}'.`
|
|
1604
|
+
);
|
|
1605
|
+
}
|
|
1606
|
+
const isArray = fieldSchema instanceof import_zod3.z.ZodArray;
|
|
1607
|
+
const inner = unwrapRefSchema(fieldSchema);
|
|
1608
|
+
const ref = getRefMetadata(inner);
|
|
1609
|
+
if (!ref) {
|
|
1610
|
+
throw new Error(
|
|
1611
|
+
`[zodmon] populate: field '${fieldName}' has no .ref() metadata. Only fields declared with .ref(Collection) can be populated.`
|
|
1612
|
+
);
|
|
1613
|
+
}
|
|
1614
|
+
return { isArray, ref };
|
|
1615
|
+
}
|
|
1616
|
+
function resolvePopulateStep(definition, previousSteps, path, as, projection) {
|
|
1617
|
+
const dotIndex = path.indexOf(".");
|
|
1618
|
+
if (dotIndex === -1) {
|
|
1619
|
+
const shape = definition.shape;
|
|
1620
|
+
const { isArray: isArray2, ref: ref2 } = resolveRefField(shape, path, definition.name);
|
|
1621
|
+
return {
|
|
1622
|
+
originalPath: path,
|
|
1623
|
+
leafField: path,
|
|
1624
|
+
as,
|
|
1625
|
+
parentOutputPath: void 0,
|
|
1626
|
+
targetCollection: ref2.collection,
|
|
1627
|
+
isArray: isArray2,
|
|
1628
|
+
...projection !== void 0 ? { projection } : {}
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
const parentPath = path.slice(0, dotIndex);
|
|
1632
|
+
const leafField = path.slice(dotIndex + 1);
|
|
1633
|
+
const parentStep = previousSteps.find((s) => s.as === parentPath);
|
|
1634
|
+
if (!parentStep) {
|
|
1635
|
+
throw new Error(
|
|
1636
|
+
`[zodmon] populate: parent '${parentPath}' has not been populated. Populate '${parentPath}' before populating '${path}'.`
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1639
|
+
const parentShape = parentStep.targetCollection.shape;
|
|
1640
|
+
const { isArray, ref } = resolveRefField(parentShape, leafField, parentStep.targetCollection.name);
|
|
1641
|
+
return {
|
|
1642
|
+
originalPath: path,
|
|
1643
|
+
leafField,
|
|
1644
|
+
as,
|
|
1645
|
+
parentOutputPath: parentPath,
|
|
1646
|
+
targetCollection: ref.collection,
|
|
1647
|
+
isArray,
|
|
1648
|
+
...projection !== void 0 ? { projection } : {}
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
function expandValue(value) {
|
|
1652
|
+
if (value == null) return [];
|
|
1653
|
+
if (Array.isArray(value)) {
|
|
1654
|
+
const result = [];
|
|
1655
|
+
for (const item of value) {
|
|
1656
|
+
if (item != null && typeof item === "object") {
|
|
1657
|
+
result.push(item);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
return result;
|
|
1661
|
+
}
|
|
1662
|
+
if (typeof value === "object") {
|
|
1663
|
+
return [value];
|
|
1664
|
+
}
|
|
1665
|
+
return [];
|
|
1666
|
+
}
|
|
1667
|
+
function getNestedTargets(doc, path) {
|
|
1668
|
+
const parts = path.split(".");
|
|
1669
|
+
let targets = [doc];
|
|
1670
|
+
for (const part of parts) {
|
|
1671
|
+
targets = targets.flatMap((target) => expandValue(target[part]));
|
|
1672
|
+
}
|
|
1673
|
+
return targets;
|
|
1674
|
+
}
|
|
1675
|
+
function addUniqueId(value, idSet, idValues) {
|
|
1676
|
+
const key = String(value);
|
|
1677
|
+
if (!idSet.has(key)) {
|
|
1678
|
+
idSet.add(key);
|
|
1679
|
+
idValues.push(value);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
function collectIds(targets, leafField) {
|
|
1683
|
+
const idSet = /* @__PURE__ */ new Set();
|
|
1684
|
+
const idValues = [];
|
|
1685
|
+
for (const target of targets) {
|
|
1686
|
+
const value = target[leafField];
|
|
1687
|
+
if (value == null) continue;
|
|
1688
|
+
if (Array.isArray(value)) {
|
|
1689
|
+
for (const id of value) {
|
|
1690
|
+
addUniqueId(id, idSet, idValues);
|
|
1691
|
+
}
|
|
1692
|
+
} else {
|
|
1693
|
+
addUniqueId(value, idSet, idValues);
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
return idValues;
|
|
1697
|
+
}
|
|
1698
|
+
function resolvePopulatedValue(value, map) {
|
|
1699
|
+
if (value == null) return value;
|
|
1700
|
+
if (Array.isArray(value)) {
|
|
1701
|
+
return value.map((id) => map.get(String(id))).filter((d) => d != null);
|
|
1702
|
+
}
|
|
1703
|
+
return map.get(String(value)) ?? null;
|
|
1704
|
+
}
|
|
1705
|
+
function mergePopulated(targets, step, map) {
|
|
1706
|
+
for (const target of targets) {
|
|
1707
|
+
const value = target[step.leafField];
|
|
1708
|
+
const populated = resolvePopulatedValue(value, map);
|
|
1709
|
+
if (step.as !== step.leafField) {
|
|
1710
|
+
delete target[step.leafField];
|
|
1711
|
+
}
|
|
1712
|
+
target[step.as] = populated;
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
async function executePopulate(documents, steps, getCollection) {
|
|
1716
|
+
for (const step of steps) {
|
|
1717
|
+
const targets = step.parentOutputPath ? documents.flatMap((doc) => getNestedTargets(doc, step.parentOutputPath)) : documents;
|
|
1718
|
+
const idValues = collectIds(targets, step.leafField);
|
|
1719
|
+
if (idValues.length === 0) continue;
|
|
1720
|
+
const col = getCollection(step.targetCollection.name);
|
|
1721
|
+
const findOptions = step.projection !== void 0 ? { projection: step.projection } : {};
|
|
1722
|
+
const fetched = await col.find({ _id: { $in: idValues } }, findOptions).toArray();
|
|
1723
|
+
const map = /* @__PURE__ */ new Map();
|
|
1724
|
+
for (const doc of fetched) {
|
|
1725
|
+
map.set(String(doc._id), doc);
|
|
1726
|
+
}
|
|
1727
|
+
mergePopulated(targets, step, map);
|
|
1728
|
+
}
|
|
1729
|
+
return documents;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
// src/populate/cursor.ts
|
|
1733
|
+
var PopulateCursor = class _PopulateCursor {
|
|
1734
|
+
cursor;
|
|
1735
|
+
definition;
|
|
1736
|
+
steps;
|
|
1737
|
+
nativeCollection;
|
|
1738
|
+
/** @internal */
|
|
1739
|
+
constructor(cursor, definition, steps, nativeCollection) {
|
|
1740
|
+
this.cursor = cursor;
|
|
1741
|
+
this.definition = definition;
|
|
1742
|
+
this.steps = steps;
|
|
1743
|
+
this.nativeCollection = nativeCollection;
|
|
1744
|
+
}
|
|
1745
|
+
// Implementation -- TypeScript cannot narrow overloaded generics in the
|
|
1746
|
+
// implementation body, so param types are widened and the return is cast.
|
|
1747
|
+
populate(field, asOrConfigure) {
|
|
1748
|
+
let alias;
|
|
1749
|
+
let projection;
|
|
1750
|
+
if (typeof asOrConfigure === "function") {
|
|
1751
|
+
alias = field.includes(".") ? field.split(".").pop() ?? field : field;
|
|
1752
|
+
const config = asOrConfigure(new PopulateRefBuilder());
|
|
1753
|
+
projection = config.projection;
|
|
1754
|
+
} else {
|
|
1755
|
+
alias = asOrConfigure ?? (field.includes(".") ? field.split(".").pop() ?? field : field);
|
|
1756
|
+
projection = void 0;
|
|
1757
|
+
}
|
|
1758
|
+
const step = resolvePopulateStep(this.definition, this.steps, field, alias, projection);
|
|
1759
|
+
const newSteps = [...this.steps, step];
|
|
1760
|
+
return new _PopulateCursor(this.cursor, this.definition, newSteps, this.nativeCollection);
|
|
1761
|
+
}
|
|
1762
|
+
/**
|
|
1763
|
+
* Execute the query and return all matching documents as a populated array.
|
|
1764
|
+
*
|
|
1765
|
+
* Fetches all documents from the underlying cursor, then applies populate
|
|
1766
|
+
* steps in order using batch `$in` queries (no N+1 problem).
|
|
1767
|
+
*
|
|
1768
|
+
* @returns Array of populated documents.
|
|
1769
|
+
*
|
|
1770
|
+
* @example
|
|
1771
|
+
* ```ts
|
|
1772
|
+
* const posts = await db.use(Posts)
|
|
1773
|
+
* .find({})
|
|
1774
|
+
* .populate('authorId', 'author')
|
|
1775
|
+
* .toArray()
|
|
1776
|
+
* ```
|
|
1777
|
+
*/
|
|
1778
|
+
async toArray() {
|
|
1779
|
+
const docs = await this.cursor.toArray();
|
|
1780
|
+
if (this.steps.length === 0) return docs;
|
|
1781
|
+
const populated = await executePopulate(
|
|
1782
|
+
docs,
|
|
1783
|
+
this.steps,
|
|
1784
|
+
(name) => this.nativeCollection.db.collection(name)
|
|
1785
|
+
);
|
|
1786
|
+
return populated;
|
|
1787
|
+
}
|
|
1788
|
+
/**
|
|
1789
|
+
* Async iterator for streaming populated documents.
|
|
1790
|
+
*
|
|
1791
|
+
* Fetches all documents first (populate requires the full batch for
|
|
1792
|
+
* efficient `$in` queries), then yields results one at a time.
|
|
1793
|
+
*
|
|
1794
|
+
* @yields Populated documents one at a time.
|
|
1795
|
+
*
|
|
1796
|
+
* @example
|
|
1797
|
+
* ```ts
|
|
1798
|
+
* for await (const post of db.use(Posts).find({}).populate('authorId', 'author')) {
|
|
1799
|
+
* console.log(post.author.name)
|
|
1800
|
+
* }
|
|
1801
|
+
* ```
|
|
1802
|
+
*/
|
|
1803
|
+
async *[Symbol.asyncIterator]() {
|
|
1804
|
+
const results = await this.toArray();
|
|
1805
|
+
for (const doc of results) {
|
|
1806
|
+
yield doc;
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
};
|
|
1810
|
+
function createPopulateCursor(cursor, definition, steps) {
|
|
1811
|
+
const nativeCollection = cursor.nativeCollection;
|
|
1812
|
+
return new PopulateCursor(cursor, definition, steps, nativeCollection);
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
// src/query/projection.ts
|
|
1816
|
+
function isIncludeValue(value) {
|
|
1817
|
+
return value === 1 || value === true;
|
|
1818
|
+
}
|
|
1819
|
+
function isExcludeValue(value) {
|
|
1820
|
+
return value === 0 || value === false;
|
|
1821
|
+
}
|
|
1822
|
+
function isInclusionProjection(projection) {
|
|
1823
|
+
for (const key of Object.keys(projection)) {
|
|
1824
|
+
if (key === "_id") continue;
|
|
1825
|
+
const value = projection[key];
|
|
1826
|
+
if (value !== void 0 && isIncludeValue(value)) return true;
|
|
1827
|
+
}
|
|
1828
|
+
return false;
|
|
1829
|
+
}
|
|
1830
|
+
function buildPickMask(projection, schemaKeys) {
|
|
1831
|
+
const mask = {};
|
|
1832
|
+
const idValue = projection._id;
|
|
1833
|
+
if (!(idValue !== void 0 && isExcludeValue(idValue)) && schemaKeys.has("_id")) {
|
|
1834
|
+
mask._id = true;
|
|
1835
|
+
}
|
|
1836
|
+
for (const key of Object.keys(projection)) {
|
|
1837
|
+
if (key === "_id") continue;
|
|
1838
|
+
const value = projection[key];
|
|
1839
|
+
if (value !== void 0 && isIncludeValue(value) && schemaKeys.has(key)) {
|
|
1840
|
+
mask[key] = true;
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
return mask;
|
|
1844
|
+
}
|
|
1845
|
+
function buildOmitMask(projection, schemaKeys) {
|
|
1846
|
+
const mask = {};
|
|
1847
|
+
for (const key of Object.keys(projection)) {
|
|
1848
|
+
const value = projection[key];
|
|
1849
|
+
if (value !== void 0 && isExcludeValue(value) && schemaKeys.has(key)) {
|
|
1850
|
+
mask[key] = true;
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
return mask;
|
|
1854
|
+
}
|
|
1855
|
+
function deriveProjectedSchema(schema, projection) {
|
|
1856
|
+
const schemaKeys = new Set(Object.keys(schema.shape));
|
|
1857
|
+
if (isInclusionProjection(projection)) {
|
|
1858
|
+
return schema.pick(buildPickMask(projection, schemaKeys));
|
|
1859
|
+
}
|
|
1860
|
+
return schema.omit(buildOmitMask(projection, schemaKeys));
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1342
1863
|
// src/query/cursor.ts
|
|
1343
1864
|
var TypedFindCursor = class {
|
|
1344
1865
|
/** @internal */
|
|
1345
1866
|
cursor;
|
|
1346
1867
|
/** @internal */
|
|
1868
|
+
definition;
|
|
1869
|
+
/** @internal */
|
|
1347
1870
|
schema;
|
|
1348
1871
|
/** @internal */
|
|
1349
1872
|
collectionName;
|
|
@@ -1355,16 +1878,23 @@ var TypedFindCursor = class {
|
|
|
1355
1878
|
// biome-ignore lint/suspicious/noExplicitAny: TypedFilter is not assignable to MongoDB's Filter; stored opaquely for paginate
|
|
1356
1879
|
filter;
|
|
1357
1880
|
/** @internal */
|
|
1881
|
+
session;
|
|
1882
|
+
/** @internal */
|
|
1358
1883
|
sortSpec;
|
|
1359
1884
|
/** @internal */
|
|
1360
|
-
|
|
1885
|
+
projectedSchema;
|
|
1886
|
+
/** @internal */
|
|
1887
|
+
constructor(cursor, definition, mode, nativeCollection, filter, session) {
|
|
1361
1888
|
this.cursor = cursor;
|
|
1889
|
+
this.definition = definition;
|
|
1362
1890
|
this.schema = definition.schema;
|
|
1363
1891
|
this.collectionName = definition.name;
|
|
1364
1892
|
this.mode = mode;
|
|
1365
1893
|
this.nativeCollection = nativeCollection;
|
|
1366
1894
|
this.filter = filter;
|
|
1895
|
+
this.session = session;
|
|
1367
1896
|
this.sortSpec = null;
|
|
1897
|
+
this.projectedSchema = null;
|
|
1368
1898
|
}
|
|
1369
1899
|
/**
|
|
1370
1900
|
* Set the sort order for the query.
|
|
@@ -1438,6 +1968,48 @@ var TypedFindCursor = class {
|
|
|
1438
1968
|
this.cursor.hint(indexName);
|
|
1439
1969
|
return this;
|
|
1440
1970
|
}
|
|
1971
|
+
/**
|
|
1972
|
+
* Apply a projection to narrow the returned fields.
|
|
1973
|
+
*
|
|
1974
|
+
* Inclusion projections (`{ name: 1 }`) return only the specified fields
|
|
1975
|
+
* plus `_id` (unless `_id: 0`). Exclusion projections (`{ email: 0 }`)
|
|
1976
|
+
* return all fields except those excluded.
|
|
1977
|
+
*
|
|
1978
|
+
* The cursor's output type is narrowed at compile time. A derived Zod
|
|
1979
|
+
* schema is built for runtime validation of the projected fields.
|
|
1980
|
+
*
|
|
1981
|
+
* Projects from the original document type, not from a previous projection.
|
|
1982
|
+
* Calling `.project()` twice overrides the previous projection.
|
|
1983
|
+
*
|
|
1984
|
+
* @param spec - Type-safe projection document.
|
|
1985
|
+
* @returns A new cursor with the narrowed output type.
|
|
1986
|
+
*
|
|
1987
|
+
* @example
|
|
1988
|
+
* ```ts
|
|
1989
|
+
* const names = await find(users, {})
|
|
1990
|
+
* .project({ name: 1 })
|
|
1991
|
+
* .sort({ name: 1 })
|
|
1992
|
+
* .toArray()
|
|
1993
|
+
* // names[0].name ✓
|
|
1994
|
+
* // names[0].email TS error
|
|
1995
|
+
* ```
|
|
1996
|
+
*/
|
|
1997
|
+
project(spec) {
|
|
1998
|
+
this.cursor.project(spec);
|
|
1999
|
+
this.projectedSchema = deriveProjectedSchema(
|
|
2000
|
+
this.schema,
|
|
2001
|
+
spec
|
|
2002
|
+
);
|
|
2003
|
+
return this;
|
|
2004
|
+
}
|
|
2005
|
+
// Implementation — creates a PopulateCursor and delegates the first populate call.
|
|
2006
|
+
// No circular runtime dependency: populate/cursor.ts imports TypedFindCursor as a
|
|
2007
|
+
// *type* only (erased at runtime), so the runtime import flows one way:
|
|
2008
|
+
// query/cursor.ts → populate/cursor.ts.
|
|
2009
|
+
populate(field, asOrConfigure) {
|
|
2010
|
+
const popCursor = createPopulateCursor(this, this.definition, []);
|
|
2011
|
+
return popCursor.populate(field, asOrConfigure);
|
|
2012
|
+
}
|
|
1441
2013
|
async paginate(opts) {
|
|
1442
2014
|
const sortRecord = this.sortSpec ? this.sortSpec : null;
|
|
1443
2015
|
const sortKeys2 = resolveSortKeys(sortRecord);
|
|
@@ -1454,8 +2026,11 @@ var TypedFindCursor = class {
|
|
|
1454
2026
|
try {
|
|
1455
2027
|
;
|
|
1456
2028
|
[total, raw2] = await Promise.all([
|
|
1457
|
-
this.nativeCollection.countDocuments(
|
|
1458
|
-
|
|
2029
|
+
this.nativeCollection.countDocuments(
|
|
2030
|
+
this.filter,
|
|
2031
|
+
this.session ? { session: this.session } : {}
|
|
2032
|
+
),
|
|
2033
|
+
this.nativeCollection.find(this.filter, this.session ? { session: this.session } : void 0).sort(sort).skip((opts.page - 1) * opts.perPage).limit(opts.perPage).toArray()
|
|
1459
2034
|
]);
|
|
1460
2035
|
} catch (err) {
|
|
1461
2036
|
wrapMongoError(err, this.collectionName);
|
|
@@ -1485,7 +2060,7 @@ var TypedFindCursor = class {
|
|
|
1485
2060
|
const effectiveSort = isBackward ? Object.fromEntries(sortKeys2.map(([f, d]) => [f, d === 1 ? -1 : 1])) : sort;
|
|
1486
2061
|
let raw2;
|
|
1487
2062
|
try {
|
|
1488
|
-
raw2 = await this.nativeCollection.find(combinedFilter).sort(effectiveSort).limit(opts.limit + 1).toArray();
|
|
2063
|
+
raw2 = await this.nativeCollection.find(combinedFilter, this.session ? { session: this.session } : void 0).sort(effectiveSort).limit(opts.limit + 1).toArray();
|
|
1489
2064
|
} catch (err) {
|
|
1490
2065
|
wrapMongoError(err, this.collectionName);
|
|
1491
2066
|
}
|
|
@@ -1554,10 +2129,11 @@ var TypedFindCursor = class {
|
|
|
1554
2129
|
if (this.mode === false || this.mode === "passthrough") {
|
|
1555
2130
|
return raw2;
|
|
1556
2131
|
}
|
|
2132
|
+
const schema = this.projectedSchema ?? (this.mode === "strict" ? this.definition.strictSchema : this.schema);
|
|
1557
2133
|
try {
|
|
1558
|
-
return
|
|
2134
|
+
return schema.parse(raw2);
|
|
1559
2135
|
} catch (err) {
|
|
1560
|
-
if (err instanceof
|
|
2136
|
+
if (err instanceof import_zod4.z.ZodError) {
|
|
1561
2137
|
throw new ZodmonValidationError(this.collectionName, err, raw2);
|
|
1562
2138
|
}
|
|
1563
2139
|
throw err;
|
|
@@ -1568,10 +2144,15 @@ var TypedFindCursor = class {
|
|
|
1568
2144
|
// src/crud/find.ts
|
|
1569
2145
|
async function findOne(handle, filter, options) {
|
|
1570
2146
|
checkUnindexedFields(handle.definition, filter);
|
|
1571
|
-
const
|
|
2147
|
+
const project = options && "project" in options ? options.project : void 0;
|
|
2148
|
+
const findOptions = project ? { projection: project } : void 0;
|
|
1572
2149
|
let raw2;
|
|
1573
2150
|
try {
|
|
1574
|
-
raw2 = await handle.native.findOne(
|
|
2151
|
+
raw2 = await handle.native.findOne(
|
|
2152
|
+
// biome-ignore lint/suspicious/noExplicitAny: TypedFilter intersection type is not directly assignable to MongoDB's Filter
|
|
2153
|
+
filter,
|
|
2154
|
+
handle.session ? { ...findOptions, session: handle.session } : findOptions
|
|
2155
|
+
);
|
|
1575
2156
|
} catch (err) {
|
|
1576
2157
|
wrapMongoError(err, handle.definition.name);
|
|
1577
2158
|
}
|
|
@@ -1580,10 +2161,14 @@ async function findOne(handle, filter, options) {
|
|
|
1580
2161
|
if (mode === false || mode === "passthrough") {
|
|
1581
2162
|
return raw2;
|
|
1582
2163
|
}
|
|
2164
|
+
const schema = project ? deriveProjectedSchema(
|
|
2165
|
+
handle.definition.schema,
|
|
2166
|
+
project
|
|
2167
|
+
) : mode === "strict" ? handle.definition.strictSchema : handle.definition.schema;
|
|
1583
2168
|
try {
|
|
1584
|
-
return
|
|
2169
|
+
return schema.parse(raw2);
|
|
1585
2170
|
} catch (err) {
|
|
1586
|
-
if (err instanceof
|
|
2171
|
+
if (err instanceof import_zod5.z.ZodError) {
|
|
1587
2172
|
throw new ZodmonValidationError(handle.definition.name, err, raw2);
|
|
1588
2173
|
}
|
|
1589
2174
|
throw err;
|
|
@@ -1598,26 +2183,42 @@ async function findOneOrThrow(handle, filter, options) {
|
|
|
1598
2183
|
}
|
|
1599
2184
|
function find(handle, filter, options) {
|
|
1600
2185
|
checkUnindexedFields(handle.definition, filter);
|
|
1601
|
-
const raw2 = handle.native.find(
|
|
2186
|
+
const raw2 = handle.native.find(
|
|
2187
|
+
// biome-ignore lint/suspicious/noExplicitAny: TypedFilter intersection type is not directly assignable to MongoDB's Filter
|
|
2188
|
+
filter,
|
|
2189
|
+
handle.session ? { session: handle.session } : void 0
|
|
2190
|
+
);
|
|
1602
2191
|
const cursor = raw2;
|
|
1603
2192
|
const mode = options?.validate !== void 0 ? options.validate : handle.definition.options.validation;
|
|
1604
|
-
|
|
2193
|
+
const typedCursor = new TypedFindCursor(
|
|
2194
|
+
cursor,
|
|
2195
|
+
handle.definition,
|
|
2196
|
+
mode,
|
|
2197
|
+
handle.native,
|
|
2198
|
+
filter,
|
|
2199
|
+
handle.session
|
|
2200
|
+
);
|
|
2201
|
+
const project = options && "project" in options ? options.project : void 0;
|
|
2202
|
+
if (project) {
|
|
2203
|
+
return typedCursor.project(project);
|
|
2204
|
+
}
|
|
2205
|
+
return typedCursor;
|
|
1605
2206
|
}
|
|
1606
2207
|
|
|
1607
2208
|
// src/crud/insert.ts
|
|
1608
|
-
var
|
|
2209
|
+
var import_zod6 = require("zod");
|
|
1609
2210
|
async function insertOne(handle, doc) {
|
|
1610
2211
|
let parsed;
|
|
1611
2212
|
try {
|
|
1612
2213
|
parsed = handle.definition.schema.parse(doc);
|
|
1613
2214
|
} catch (err) {
|
|
1614
|
-
if (err instanceof
|
|
2215
|
+
if (err instanceof import_zod6.z.ZodError) {
|
|
1615
2216
|
throw new ZodmonValidationError(handle.definition.name, err, doc);
|
|
1616
2217
|
}
|
|
1617
2218
|
throw err;
|
|
1618
2219
|
}
|
|
1619
2220
|
try {
|
|
1620
|
-
await handle.native.insertOne(parsed);
|
|
2221
|
+
await handle.native.insertOne(parsed, handle.session ? { session: handle.session } : {});
|
|
1621
2222
|
} catch (err) {
|
|
1622
2223
|
wrapMongoError(err, handle.definition.name);
|
|
1623
2224
|
}
|
|
@@ -1630,14 +2231,14 @@ async function insertMany(handle, docs) {
|
|
|
1630
2231
|
try {
|
|
1631
2232
|
parsed.push(handle.definition.schema.parse(doc));
|
|
1632
2233
|
} catch (err) {
|
|
1633
|
-
if (err instanceof
|
|
2234
|
+
if (err instanceof import_zod6.z.ZodError) {
|
|
1634
2235
|
throw new ZodmonValidationError(handle.definition.name, err, doc);
|
|
1635
2236
|
}
|
|
1636
2237
|
throw err;
|
|
1637
2238
|
}
|
|
1638
2239
|
}
|
|
1639
2240
|
try {
|
|
1640
|
-
await handle.native.insertMany(parsed);
|
|
2241
|
+
await handle.native.insertMany(parsed, handle.session ? { session: handle.session } : {});
|
|
1641
2242
|
} catch (err) {
|
|
1642
2243
|
wrapMongoError(err, handle.definition.name);
|
|
1643
2244
|
}
|
|
@@ -1645,17 +2246,29 @@ async function insertMany(handle, docs) {
|
|
|
1645
2246
|
}
|
|
1646
2247
|
|
|
1647
2248
|
// src/crud/update.ts
|
|
1648
|
-
var
|
|
2249
|
+
var import_zod7 = require("zod");
|
|
1649
2250
|
async function updateOne(handle, filter, update, options) {
|
|
1650
2251
|
try {
|
|
1651
|
-
return await handle.native.updateOne(
|
|
2252
|
+
return await handle.native.updateOne(
|
|
2253
|
+
// biome-ignore lint/suspicious/noExplicitAny: TypedFilter intersection type is not directly assignable to MongoDB's Filter
|
|
2254
|
+
filter,
|
|
2255
|
+
// biome-ignore lint/suspicious/noExplicitAny: TypedUpdateFilter intersection type is not directly assignable to MongoDB's UpdateFilter
|
|
2256
|
+
update,
|
|
2257
|
+
handle.session ? { ...options, session: handle.session } : options
|
|
2258
|
+
);
|
|
1652
2259
|
} catch (err) {
|
|
1653
2260
|
wrapMongoError(err, handle.definition.name);
|
|
1654
2261
|
}
|
|
1655
2262
|
}
|
|
1656
2263
|
async function updateMany(handle, filter, update, options) {
|
|
1657
2264
|
try {
|
|
1658
|
-
return await handle.native.updateMany(
|
|
2265
|
+
return await handle.native.updateMany(
|
|
2266
|
+
// biome-ignore lint/suspicious/noExplicitAny: TypedFilter intersection type is not directly assignable to MongoDB's Filter
|
|
2267
|
+
filter,
|
|
2268
|
+
// biome-ignore lint/suspicious/noExplicitAny: TypedUpdateFilter intersection type is not directly assignable to MongoDB's UpdateFilter
|
|
2269
|
+
update,
|
|
2270
|
+
handle.session ? { ...options, session: handle.session } : options
|
|
2271
|
+
);
|
|
1659
2272
|
} catch (err) {
|
|
1660
2273
|
wrapMongoError(err, handle.definition.name);
|
|
1661
2274
|
}
|
|
@@ -1668,6 +2281,9 @@ async function findOneAndUpdate(handle, filter, update, options) {
|
|
|
1668
2281
|
if (options?.upsert !== void 0) {
|
|
1669
2282
|
driverOptions["upsert"] = options.upsert;
|
|
1670
2283
|
}
|
|
2284
|
+
if (handle.session) {
|
|
2285
|
+
driverOptions["session"] = handle.session;
|
|
2286
|
+
}
|
|
1671
2287
|
let result;
|
|
1672
2288
|
try {
|
|
1673
2289
|
result = await handle.native.findOneAndUpdate(
|
|
@@ -1687,24 +2303,165 @@ async function findOneAndUpdate(handle, filter, update, options) {
|
|
|
1687
2303
|
return result;
|
|
1688
2304
|
}
|
|
1689
2305
|
try {
|
|
1690
|
-
|
|
2306
|
+
const schema = mode === "strict" ? handle.definition.strictSchema : handle.definition.schema;
|
|
2307
|
+
return schema.parse(result);
|
|
1691
2308
|
} catch (err) {
|
|
1692
|
-
if (err instanceof
|
|
2309
|
+
if (err instanceof import_zod7.z.ZodError) {
|
|
1693
2310
|
throw new ZodmonValidationError(handle.definition.name, err, result);
|
|
1694
2311
|
}
|
|
1695
2312
|
throw err;
|
|
1696
2313
|
}
|
|
1697
2314
|
}
|
|
1698
2315
|
|
|
2316
|
+
// src/populate/query.ts
|
|
2317
|
+
var PopulateOneQuery = class _PopulateOneQuery {
|
|
2318
|
+
handle;
|
|
2319
|
+
filter;
|
|
2320
|
+
options;
|
|
2321
|
+
steps;
|
|
2322
|
+
constructor(handle, filter, options, steps = []) {
|
|
2323
|
+
this.handle = handle;
|
|
2324
|
+
this.filter = filter;
|
|
2325
|
+
this.options = options;
|
|
2326
|
+
this.steps = steps;
|
|
2327
|
+
}
|
|
2328
|
+
// Implementation — TypeScript cannot narrow overloaded generics in the
|
|
2329
|
+
// implementation body, so param types are widened and the return is cast.
|
|
2330
|
+
populate(field, asOrConfigure) {
|
|
2331
|
+
let alias;
|
|
2332
|
+
let projection;
|
|
2333
|
+
if (typeof asOrConfigure === "function") {
|
|
2334
|
+
alias = field.includes(".") ? field.split(".").pop() ?? field : field;
|
|
2335
|
+
const config = asOrConfigure(new PopulateRefBuilder());
|
|
2336
|
+
projection = config.projection;
|
|
2337
|
+
} else {
|
|
2338
|
+
alias = asOrConfigure ?? (field.includes(".") ? field.split(".").pop() ?? field : field);
|
|
2339
|
+
projection = void 0;
|
|
2340
|
+
}
|
|
2341
|
+
const step = resolvePopulateStep(this.handle.definition, this.steps, field, alias, projection);
|
|
2342
|
+
const newSteps = [...this.steps, step];
|
|
2343
|
+
return new _PopulateOneQuery(this.handle, this.filter, this.options, newSteps);
|
|
2344
|
+
}
|
|
2345
|
+
/**
|
|
2346
|
+
* Attach fulfillment and rejection handlers to the query promise.
|
|
2347
|
+
*
|
|
2348
|
+
* Executes the base findOne query and applies populate steps if any.
|
|
2349
|
+
* Returns `null` when no document matches the filter.
|
|
2350
|
+
*/
|
|
2351
|
+
// biome-ignore lint/suspicious/noThenProperty: PromiseLike requires a then method
|
|
2352
|
+
then(onfulfilled, onrejected) {
|
|
2353
|
+
const promise = this.execute();
|
|
2354
|
+
return promise.then(onfulfilled, onrejected);
|
|
2355
|
+
}
|
|
2356
|
+
async execute() {
|
|
2357
|
+
const doc = await findOne(this.handle, this.filter, this.options);
|
|
2358
|
+
if (!doc) return null;
|
|
2359
|
+
if (this.steps.length === 0) return doc;
|
|
2360
|
+
const populated = await executePopulate(
|
|
2361
|
+
[doc],
|
|
2362
|
+
this.steps,
|
|
2363
|
+
(name) => this.handle.native.db.collection(name)
|
|
2364
|
+
);
|
|
2365
|
+
return populated[0] ?? null;
|
|
2366
|
+
}
|
|
2367
|
+
};
|
|
2368
|
+
var PopulateOneOrThrowQuery = class _PopulateOneOrThrowQuery {
|
|
2369
|
+
handle;
|
|
2370
|
+
filter;
|
|
2371
|
+
options;
|
|
2372
|
+
steps;
|
|
2373
|
+
constructor(handle, filter, options, steps = []) {
|
|
2374
|
+
this.handle = handle;
|
|
2375
|
+
this.filter = filter;
|
|
2376
|
+
this.options = options;
|
|
2377
|
+
this.steps = steps;
|
|
2378
|
+
}
|
|
2379
|
+
// Implementation — see PopulateOneQuery for reasoning on casts.
|
|
2380
|
+
populate(field, asOrConfigure) {
|
|
2381
|
+
let alias;
|
|
2382
|
+
let projection;
|
|
2383
|
+
if (typeof asOrConfigure === "function") {
|
|
2384
|
+
alias = field.includes(".") ? field.split(".").pop() ?? field : field;
|
|
2385
|
+
const config = asOrConfigure(new PopulateRefBuilder());
|
|
2386
|
+
projection = config.projection;
|
|
2387
|
+
} else {
|
|
2388
|
+
alias = asOrConfigure ?? (field.includes(".") ? field.split(".").pop() ?? field : field);
|
|
2389
|
+
projection = void 0;
|
|
2390
|
+
}
|
|
2391
|
+
const step = resolvePopulateStep(this.handle.definition, this.steps, field, alias, projection);
|
|
2392
|
+
const newSteps = [...this.steps, step];
|
|
2393
|
+
return new _PopulateOneOrThrowQuery(this.handle, this.filter, this.options, newSteps);
|
|
2394
|
+
}
|
|
2395
|
+
/**
|
|
2396
|
+
* Attach fulfillment and rejection handlers to the query promise.
|
|
2397
|
+
*
|
|
2398
|
+
* Executes the base findOneOrThrow query and applies populate steps if any.
|
|
2399
|
+
* Throws {@link ZodmonNotFoundError} when no document matches.
|
|
2400
|
+
*/
|
|
2401
|
+
// biome-ignore lint/suspicious/noThenProperty: PromiseLike requires a then method
|
|
2402
|
+
then(onfulfilled, onrejected) {
|
|
2403
|
+
const promise = this.execute();
|
|
2404
|
+
return promise.then(onfulfilled, onrejected);
|
|
2405
|
+
}
|
|
2406
|
+
async execute() {
|
|
2407
|
+
const doc = await findOne(this.handle, this.filter, this.options);
|
|
2408
|
+
if (!doc) {
|
|
2409
|
+
throw new ZodmonNotFoundError(this.handle.definition.name, this.filter);
|
|
2410
|
+
}
|
|
2411
|
+
if (this.steps.length === 0) return doc;
|
|
2412
|
+
const populated = await executePopulate(
|
|
2413
|
+
[doc],
|
|
2414
|
+
this.steps,
|
|
2415
|
+
(name) => this.handle.native.db.collection(name)
|
|
2416
|
+
);
|
|
2417
|
+
const result = populated[0];
|
|
2418
|
+
if (!result) {
|
|
2419
|
+
throw new ZodmonNotFoundError(this.handle.definition.name, this.filter);
|
|
2420
|
+
}
|
|
2421
|
+
return result;
|
|
2422
|
+
}
|
|
2423
|
+
};
|
|
2424
|
+
|
|
1699
2425
|
// src/client/handle.ts
|
|
1700
|
-
var CollectionHandle = class {
|
|
2426
|
+
var CollectionHandle = class _CollectionHandle {
|
|
1701
2427
|
/** The collection definition containing schema, name, and index metadata. */
|
|
1702
2428
|
definition;
|
|
1703
2429
|
/** The underlying MongoDB driver collection, typed to the inferred document type. */
|
|
1704
2430
|
native;
|
|
1705
|
-
|
|
2431
|
+
/**
|
|
2432
|
+
* The MongoDB client session bound to this handle, if any.
|
|
2433
|
+
*
|
|
2434
|
+
* When set, all CRUD and aggregation operations performed through this
|
|
2435
|
+
* handle will include the session in their options, enabling transactional
|
|
2436
|
+
* reads and writes. Undefined when no session is bound.
|
|
2437
|
+
*/
|
|
2438
|
+
session;
|
|
2439
|
+
constructor(definition, native, session) {
|
|
1706
2440
|
this.definition = definition;
|
|
1707
2441
|
this.native = native;
|
|
2442
|
+
this.session = session;
|
|
2443
|
+
}
|
|
2444
|
+
/**
|
|
2445
|
+
* Create a new handle bound to the given MongoDB client session.
|
|
2446
|
+
*
|
|
2447
|
+
* Returns a new {@link CollectionHandle} that shares the same collection
|
|
2448
|
+
* definition and native driver collection, but passes `session` to every
|
|
2449
|
+
* CRUD and aggregation operation. The original handle is not modified.
|
|
2450
|
+
*
|
|
2451
|
+
* @param session - The MongoDB `ClientSession` to bind.
|
|
2452
|
+
* @returns A new handle with the session attached.
|
|
2453
|
+
*
|
|
2454
|
+
* @example
|
|
2455
|
+
* ```ts
|
|
2456
|
+
* const users = db.use(Users)
|
|
2457
|
+
* await db.client.withSession(async (session) => {
|
|
2458
|
+
* const bound = users.withSession(session)
|
|
2459
|
+
* await bound.insertOne({ name: 'Ada' }) // uses session
|
|
2460
|
+
* })
|
|
2461
|
+
* ```
|
|
2462
|
+
*/
|
|
2463
|
+
withSession(session) {
|
|
2464
|
+
return new _CollectionHandle(this.definition, this.native, session);
|
|
1708
2465
|
}
|
|
1709
2466
|
/**
|
|
1710
2467
|
* Insert a single document into the collection.
|
|
@@ -1750,70 +2507,18 @@ var CollectionHandle = class {
|
|
|
1750
2507
|
async insertMany(docs) {
|
|
1751
2508
|
return await insertMany(this, docs);
|
|
1752
2509
|
}
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
* back to the collection-level default (which defaults to `'strict'`).
|
|
1759
|
-
*
|
|
1760
|
-
* @param filter - Type-safe filter to match documents.
|
|
1761
|
-
* @param options - Optional projection and validation overrides.
|
|
1762
|
-
* @returns The matched document, or `null` if no document matches.
|
|
1763
|
-
* @throws {ZodmonValidationError} When the fetched document fails schema validation in strict mode.
|
|
1764
|
-
*
|
|
1765
|
-
* @example
|
|
1766
|
-
* ```ts
|
|
1767
|
-
* const users = db.use(Users)
|
|
1768
|
-
* const user = await users.findOne({ name: 'Ada' })
|
|
1769
|
-
* if (user) console.log(user.role)
|
|
1770
|
-
* ```
|
|
1771
|
-
*/
|
|
1772
|
-
async findOne(filter, options) {
|
|
1773
|
-
return await findOne(this, filter, options);
|
|
2510
|
+
findOne(filter, options) {
|
|
2511
|
+
if (options && "project" in options) {
|
|
2512
|
+
return findOne(this, filter, options);
|
|
2513
|
+
}
|
|
2514
|
+
return new PopulateOneQuery(this, filter, options);
|
|
1774
2515
|
}
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
*
|
|
1781
|
-
* @param filter - Type-safe filter to match documents.
|
|
1782
|
-
* @param options - Optional projection and validation overrides.
|
|
1783
|
-
* @returns The matched document (never null).
|
|
1784
|
-
* @throws {ZodmonNotFoundError} When no document matches the filter.
|
|
1785
|
-
* @throws {ZodmonValidationError} When the fetched document fails schema validation in strict mode.
|
|
1786
|
-
*
|
|
1787
|
-
* @example
|
|
1788
|
-
* ```ts
|
|
1789
|
-
* const users = db.use(Users)
|
|
1790
|
-
* const user = await users.findOneOrThrow({ name: 'Ada' })
|
|
1791
|
-
* console.log(user.role) // guaranteed non-null
|
|
1792
|
-
* ```
|
|
1793
|
-
*/
|
|
1794
|
-
async findOneOrThrow(filter, options) {
|
|
1795
|
-
return await findOneOrThrow(this, filter, options);
|
|
2516
|
+
findOneOrThrow(filter, options) {
|
|
2517
|
+
if (options && "project" in options) {
|
|
2518
|
+
return findOneOrThrow(this, filter, options);
|
|
2519
|
+
}
|
|
2520
|
+
return new PopulateOneOrThrowQuery(this, filter, options);
|
|
1796
2521
|
}
|
|
1797
|
-
/**
|
|
1798
|
-
* Find all documents matching the filter, returning a chainable typed cursor.
|
|
1799
|
-
*
|
|
1800
|
-
* The cursor is lazy — no query is executed until a terminal method
|
|
1801
|
-
* (`toArray`, `for await`) is called. Use `sort`, `skip`, and `limit`
|
|
1802
|
-
* to shape the query before executing.
|
|
1803
|
-
*
|
|
1804
|
-
* @param filter - Type-safe filter to match documents.
|
|
1805
|
-
* @param options - Optional validation overrides.
|
|
1806
|
-
* @returns A typed cursor for chaining query modifiers.
|
|
1807
|
-
*
|
|
1808
|
-
* @example
|
|
1809
|
-
* ```ts
|
|
1810
|
-
* const users = db.use(Users)
|
|
1811
|
-
* const admins = await users.find({ role: 'admin' })
|
|
1812
|
-
* .sort({ name: 1 })
|
|
1813
|
-
* .limit(10)
|
|
1814
|
-
* .toArray()
|
|
1815
|
-
* ```
|
|
1816
|
-
*/
|
|
1817
2522
|
find(filter, options) {
|
|
1818
2523
|
return find(this, filter, options);
|
|
1819
2524
|
}
|
|
@@ -2062,12 +2767,51 @@ var Database = class {
|
|
|
2062
2767
|
return results;
|
|
2063
2768
|
}
|
|
2064
2769
|
/**
|
|
2065
|
-
* Execute a function within a MongoDB transaction
|
|
2770
|
+
* Execute a function within a MongoDB transaction.
|
|
2771
|
+
*
|
|
2772
|
+
* Starts a client session and runs the callback inside
|
|
2773
|
+
* `session.withTransaction()`. The driver handles commit on success,
|
|
2774
|
+
* abort on error, and automatic retries for transient transaction errors.
|
|
2775
|
+
*
|
|
2776
|
+
* The return value of `fn` is forwarded as the return value of this method.
|
|
2066
2777
|
*
|
|
2067
|
-
*
|
|
2778
|
+
* @param fn - Async callback receiving a {@link TransactionContext}.
|
|
2779
|
+
* @returns The value returned by `fn`.
|
|
2780
|
+
*
|
|
2781
|
+
* @example
|
|
2782
|
+
* ```ts
|
|
2783
|
+
* const user = await db.transaction(async (tx) => {
|
|
2784
|
+
* const txUsers = tx.use(users)
|
|
2785
|
+
* return await txUsers.insertOne({ name: 'Ada' })
|
|
2786
|
+
* })
|
|
2787
|
+
* ```
|
|
2788
|
+
*
|
|
2789
|
+
* @example
|
|
2790
|
+
* ```ts
|
|
2791
|
+
* // Rollback on error
|
|
2792
|
+
* try {
|
|
2793
|
+
* await db.transaction(async (tx) => {
|
|
2794
|
+
* const txUsers = tx.use(users)
|
|
2795
|
+
* await txUsers.insertOne({ name: 'Ada' })
|
|
2796
|
+
* throw new Error('abort!')
|
|
2797
|
+
* })
|
|
2798
|
+
* } catch (err) {
|
|
2799
|
+
* // insert was rolled back, err is the original error
|
|
2800
|
+
* }
|
|
2801
|
+
* ```
|
|
2068
2802
|
*/
|
|
2069
|
-
transaction(
|
|
2070
|
-
|
|
2803
|
+
async transaction(fn) {
|
|
2804
|
+
const session = this._client.startSession();
|
|
2805
|
+
try {
|
|
2806
|
+
let result;
|
|
2807
|
+
await session.withTransaction(async () => {
|
|
2808
|
+
const tx = new TransactionContext(session);
|
|
2809
|
+
result = await fn(tx);
|
|
2810
|
+
});
|
|
2811
|
+
return result;
|
|
2812
|
+
} finally {
|
|
2813
|
+
await session.endSession();
|
|
2814
|
+
}
|
|
2071
2815
|
}
|
|
2072
2816
|
/**
|
|
2073
2817
|
* Close the underlying `MongoClient` connection. Safe to call even if
|
|
@@ -2100,10 +2844,10 @@ function createClient(uri, dbNameOrOptions, maybeOptions) {
|
|
|
2100
2844
|
|
|
2101
2845
|
// src/collection/collection.ts
|
|
2102
2846
|
var import_mongodb5 = require("mongodb");
|
|
2103
|
-
var
|
|
2847
|
+
var import_zod10 = require("zod");
|
|
2104
2848
|
|
|
2105
2849
|
// src/schema/extensions.ts
|
|
2106
|
-
var
|
|
2850
|
+
var import_zod8 = require("zod");
|
|
2107
2851
|
var indexMetadata = /* @__PURE__ */ new WeakMap();
|
|
2108
2852
|
function getIndexMetadata(schema) {
|
|
2109
2853
|
if (typeof schema !== "object" || schema === null) return void 0;
|
|
@@ -2111,7 +2855,7 @@ function getIndexMetadata(schema) {
|
|
|
2111
2855
|
}
|
|
2112
2856
|
var GUARD = /* @__PURE__ */ Symbol.for("zodmon_extensions");
|
|
2113
2857
|
function installExtensions() {
|
|
2114
|
-
const proto =
|
|
2858
|
+
const proto = import_zod8.z.ZodType.prototype;
|
|
2115
2859
|
if (GUARD in proto) return;
|
|
2116
2860
|
Object.defineProperty(proto, "index", {
|
|
2117
2861
|
/**
|
|
@@ -2210,10 +2954,10 @@ installExtensions();
|
|
|
2210
2954
|
|
|
2211
2955
|
// src/schema/object-id.ts
|
|
2212
2956
|
var import_mongodb4 = require("mongodb");
|
|
2213
|
-
var
|
|
2957
|
+
var import_zod9 = require("zod");
|
|
2214
2958
|
var OBJECT_ID_HEX = /^[a-f\d]{24}$/i;
|
|
2215
2959
|
function objectId() {
|
|
2216
|
-
return
|
|
2960
|
+
return import_zod9.z.custom((val) => {
|
|
2217
2961
|
if (val instanceof import_mongodb4.ObjectId) return true;
|
|
2218
2962
|
return typeof val === "string" && OBJECT_ID_HEX.test(val);
|
|
2219
2963
|
}, "Invalid ObjectId").transform((val) => val instanceof import_mongodb4.ObjectId ? val : import_mongodb4.ObjectId.createFromHexString(val));
|
|
@@ -2232,7 +2976,8 @@ function extractFieldIndexes(shape) {
|
|
|
2232
2976
|
}
|
|
2233
2977
|
function collection(name, shape, options) {
|
|
2234
2978
|
const resolvedShape = "_id" in shape ? shape : { _id: objectId().default(() => new import_mongodb5.ObjectId()), ...shape };
|
|
2235
|
-
const schema =
|
|
2979
|
+
const schema = import_zod10.z.object(resolvedShape);
|
|
2980
|
+
const strictSchema = schema.strict();
|
|
2236
2981
|
const fieldIndexes = extractFieldIndexes(shape);
|
|
2237
2982
|
const { indexes: compoundIndexes, validation, ...rest } = options ?? {};
|
|
2238
2983
|
return {
|
|
@@ -2242,6 +2987,7 @@ function collection(name, shape, options) {
|
|
|
2242
2987
|
// not assignable to ZodObject<ResolvedShape<TShape>>. The cast is safe because
|
|
2243
2988
|
// the runtime shape is correct — only the readonly modifier differs.
|
|
2244
2989
|
schema,
|
|
2990
|
+
strictSchema,
|
|
2245
2991
|
shape,
|
|
2246
2992
|
fieldIndexes,
|
|
2247
2993
|
// Safe cast: compoundIndexes is TIndexes at runtime (or an empty array when
|
|
@@ -2381,6 +3127,11 @@ var $ = {
|
|
|
2381
3127
|
CollectionHandle,
|
|
2382
3128
|
Database,
|
|
2383
3129
|
IndexBuilder,
|
|
3130
|
+
PopulateCursor,
|
|
3131
|
+
PopulateOneOrThrowQuery,
|
|
3132
|
+
PopulateOneQuery,
|
|
3133
|
+
PopulateRefBuilder,
|
|
3134
|
+
TransactionContext,
|
|
2384
3135
|
TypedFindCursor,
|
|
2385
3136
|
ZodmonAuthError,
|
|
2386
3137
|
ZodmonBulkWriteError,
|
|
@@ -2400,8 +3151,11 @@ var $ = {
|
|
|
2400
3151
|
createAccumulatorBuilder,
|
|
2401
3152
|
createClient,
|
|
2402
3153
|
createExpressionBuilder,
|
|
3154
|
+
createPopulateCursor,
|
|
2403
3155
|
deleteMany,
|
|
2404
3156
|
deleteOne,
|
|
3157
|
+
deriveProjectedSchema,
|
|
3158
|
+
executePopulate,
|
|
2405
3159
|
extractComparableOptions,
|
|
2406
3160
|
extractDbName,
|
|
2407
3161
|
extractFieldIndexes,
|
|
@@ -2416,14 +3170,17 @@ var $ = {
|
|
|
2416
3170
|
index,
|
|
2417
3171
|
insertMany,
|
|
2418
3172
|
insertOne,
|
|
3173
|
+
isInclusionProjection,
|
|
2419
3174
|
isOid,
|
|
2420
3175
|
objectId,
|
|
2421
3176
|
oid,
|
|
2422
3177
|
raw,
|
|
3178
|
+
resolvePopulateStep,
|
|
2423
3179
|
serializeIndexKey,
|
|
2424
3180
|
syncIndexes,
|
|
2425
3181
|
toCompoundIndexSpec,
|
|
2426
3182
|
toFieldIndexSpec,
|
|
3183
|
+
unwrapRefSchema,
|
|
2427
3184
|
updateMany,
|
|
2428
3185
|
updateOne,
|
|
2429
3186
|
wrapMongoError
|