@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 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
- var $min = (field) => ({
114
- __accum: true,
115
- expr: { $min: field }
116
- });
117
- var $max = (field) => ({
118
- __accum: true,
119
- expr: { $max: field }
120
- });
121
- var $first = (field) => ({
122
- __accum: true,
123
- expr: { $first: field }
124
- });
125
- var $last = (field) => ({
126
- __accum: true,
127
- expr: { $last: field }
128
- });
129
- var $push = (field) => ({
130
- __accum: true,
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 $2 = (field) => `$${field}`;
156
- const val = (v) => typeof v === "number" ? v : `$${v}`;
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: (field, value) => expr({ $add: [$2(field), val(value)] }),
161
- subtract: (field, value) => expr({ $subtract: [$2(field), val(value)] }),
162
- multiply: (field, value) => expr({ $multiply: [$2(field), val(value)] }),
163
- divide: (field, value) => expr({ $divide: [$2(field), val(value)] }),
164
- mod: (field, value) => expr({ $mod: [$2(field), val(value)] }),
165
- abs: (field) => expr({ $abs: $2(field) }),
166
- ceil: (field) => expr({ $ceil: $2(field) }),
167
- floor: (field) => expr({ $floor: $2(field) }),
168
- round: (field, place = 0) => expr({ $round: [$2(field), place] }),
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 (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(p)) return $2(p);
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: $2(field) }),
178
- toUpper: (field) => expr({ $toUpper: $2(field) }),
179
- trim: (field) => expr({ $trim: { input: $2(field) } }),
180
- substr: (field, start, length) => expr({ $substrBytes: [$2(field), start, length] }),
181
- // Comparison
182
- eq: (field, value) => expr({ $eq: [$2(field), value] }),
183
- gt: (field, value) => expr({ $gt: [$2(field), value] }),
184
- gte: (field, value) => expr({ $gte: [$2(field), value] }),
185
- lt: (field, value) => expr({ $lt: [$2(field), value] }),
186
- lte: (field, value) => expr({ $lte: [$2(field), value] }),
187
- ne: (field, value) => expr({ $ne: [$2(field), value] }),
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: $2(field) }),
190
- month: (field) => expr({ $month: $2(field) }),
191
- dayOfMonth: (field) => expr({ $dayOfMonth: $2(field) }),
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: $2(field) }),
206
+ size: (field) => expr({ $size: resolveArg(field) }),
194
207
  // Conditional
195
- cond: (condition, thenValue, elseValue) => expr({ $cond: [condition.value, thenValue, elseValue] }),
196
- ifNull: (field, fallback) => expr({ $ifNull: [$2(field), fallback] })
197
- // 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.
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
- constructor(definition, nativeCollection, stages) {
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(this.definition, this.nativeCollection, [
508
- ...this.stages,
509
- stage
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(this.stages);
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(this.stages);
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(this.stages);
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 pipeline = new _AggregatePipeline(this.definition, this.nativeCollection, [
625
- ...this.stages,
626
- { $match: filter }
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(this.definition, this.nativeCollection, [
648
- ...this.stages,
649
- { $sort: spec }
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(this.definition, this.nativeCollection, [
672
- ...this.stages,
673
- { $skip: n }
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(this.definition, this.nativeCollection, [
695
- ...this.stages,
696
- { $limit: n }
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(this.definition, this.nativeCollection, [
720
- ...this.stages,
721
- { $project: spec }
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(this.definition, this.nativeCollection, [
744
- ...this.stages,
745
- { $project: spec }
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(this.definition, this.nativeCollection, [
768
- ...this.stages,
769
- { $project: spec }
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
- const _id = Array.isArray(field) ? Object.fromEntries(field.map((f) => [f, `$${f}`])) : `$${field}`;
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(this.definition, this.nativeCollection, [
780
- ...this.stages,
781
- { $group: { _id, ...accumExprs } }
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(this.definition, this.nativeCollection, [
795
- ...this.stages,
796
- { $addFields: stage }
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(this.definition, this.nativeCollection, [
824
- ...this.stages,
825
- stage
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(this.definition, this.nativeCollection, stages);
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(this.definition, this.nativeCollection, [
896
- ...this.stages,
897
- { $group: { _id: `$${field}`, count: { $sum: 1 } } },
898
- { $sort: { count: -1 } }
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(this.definition, this.nativeCollection, [
921
- ...this.stages,
922
- { $group: { _id: `$${field}`, total: { $sum: `$${sumField}` } } },
923
- { $sort: { total: -1 } }
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(this.definition, this.nativeCollection, [
945
- ...this.stages,
946
- { $sort: { [field]: direction === "desc" ? -1 : 1 } }
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(this.definition, this.nativeCollection, [
967
- ...this.stages,
968
- { $sort: { [options.by]: -1 } },
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(this.definition, this.nativeCollection, [
990
- ...this.stages,
991
- { $sort: { [options.by]: 1 } },
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(handle.definition, handle.native, []);
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(filter);
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(filter);
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
- return handle.definition.schema.parse(result);
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 import_zod4 = require("zod");
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 import_zod3 = require("zod");
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
- constructor(cursor, definition, mode, nativeCollection, filter) {
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(this.filter),
1458
- this.nativeCollection.find(this.filter).sort(sort).skip((opts.page - 1) * opts.perPage).limit(opts.perPage).toArray()
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 this.schema.parse(raw2);
2134
+ return schema.parse(raw2);
1559
2135
  } catch (err) {
1560
- if (err instanceof import_zod3.z.ZodError) {
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 findOptions = options?.project ? { projection: options.project } : void 0;
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(filter, findOptions);
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 handle.definition.schema.parse(raw2);
2169
+ return schema.parse(raw2);
1585
2170
  } catch (err) {
1586
- if (err instanceof import_zod4.z.ZodError) {
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(filter);
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
- return new TypedFindCursor(cursor, handle.definition, mode, handle.native, filter);
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 import_zod5 = require("zod");
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 import_zod5.z.ZodError) {
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 import_zod5.z.ZodError) {
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 import_zod6 = require("zod");
2249
+ var import_zod7 = require("zod");
1649
2250
  async function updateOne(handle, filter, update, options) {
1650
2251
  try {
1651
- return await handle.native.updateOne(filter, update, options);
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(filter, update, options);
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
- return handle.definition.schema.parse(result);
2306
+ const schema = mode === "strict" ? handle.definition.strictSchema : handle.definition.schema;
2307
+ return schema.parse(result);
1691
2308
  } catch (err) {
1692
- if (err instanceof import_zod6.z.ZodError) {
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
- constructor(definition, native) {
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
- * Find a single document matching the filter.
1755
- *
1756
- * Queries MongoDB, then validates the fetched document against the collection's
1757
- * Zod schema. Validation mode is resolved from the per-query option, falling
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
- * Find a single document matching the filter, or throw if none exists.
1777
- *
1778
- * Behaves identically to {@link findOne} but throws {@link ZodmonNotFoundError}
1779
- * instead of returning `null` when no document matches the filter.
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 with auto-commit/rollback.
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
- * Stub full implementation in TASK-106.
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(_fn) {
2070
- throw new Error("Not implemented");
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 import_zod9 = require("zod");
2847
+ var import_zod10 = require("zod");
2104
2848
 
2105
2849
  // src/schema/extensions.ts
2106
- var import_zod7 = require("zod");
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 = import_zod7.z.ZodType.prototype;
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 import_zod8 = require("zod");
2957
+ var import_zod9 = require("zod");
2214
2958
  var OBJECT_ID_HEX = /^[a-f\d]{24}$/i;
2215
2959
  function objectId() {
2216
- return import_zod8.z.custom((val) => {
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 = import_zod9.z.object(resolvedShape);
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