@zodmon/core 0.11.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,9 +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,
72
78
  deriveProjectedSchema: () => deriveProjectedSchema,
79
+ executePopulate: () => executePopulate,
73
80
  extractComparableOptions: () => extractComparableOptions,
74
81
  extractDbName: () => extractDbName,
75
82
  extractFieldIndexes: () => extractFieldIndexes,
@@ -89,10 +96,12 @@ __export(index_exports, {
89
96
  objectId: () => objectId,
90
97
  oid: () => oid,
91
98
  raw: () => raw,
99
+ resolvePopulateStep: () => resolvePopulateStep,
92
100
  serializeIndexKey: () => serializeIndexKey,
93
101
  syncIndexes: () => syncIndexes,
94
102
  toCompoundIndexSpec: () => toCompoundIndexSpec,
95
103
  toFieldIndexSpec: () => toFieldIndexSpec,
104
+ unwrapRefSchema: () => unwrapRefSchema,
96
105
  updateMany: () => updateMany,
97
106
  updateOne: () => updateOne,
98
107
  wrapMongoError: () => wrapMongoError
@@ -147,50 +156,84 @@ function createAccumulatorBuilder() {
147
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.
148
157
  };
149
158
  }
159
+ var isExpr = (v) => typeof v === "object" && v !== null && v.__expr === true;
150
160
  function createExpressionBuilder() {
151
- const $2 = (field) => `$${field}`;
152
- 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;
153
167
  const expr = (value) => ({ __expr: true, value });
154
168
  return {
155
169
  // Arithmetic
156
- add: (field, value) => expr({ $add: [$2(field), val(value)] }),
157
- subtract: (field, value) => expr({ $subtract: [$2(field), val(value)] }),
158
- multiply: (field, value) => expr({ $multiply: [$2(field), val(value)] }),
159
- divide: (field, value) => expr({ $divide: [$2(field), val(value)] }),
160
- mod: (field, value) => expr({ $mod: [$2(field), val(value)] }),
161
- abs: (field) => expr({ $abs: $2(field) }),
162
- ceil: (field) => expr({ $ceil: $2(field) }),
163
- floor: (field) => expr({ $floor: $2(field) }),
164
- 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] }),
165
179
  // String
166
180
  concat: (...parts) => {
167
181
  const resolved = parts.map((p) => {
168
- 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}`;
169
184
  return p;
170
185
  });
171
186
  return expr({ $concat: resolved });
172
187
  },
173
- toLower: (field) => expr({ $toLower: $2(field) }),
174
- toUpper: (field) => expr({ $toUpper: $2(field) }),
175
- trim: (field) => expr({ $trim: { input: $2(field) } }),
176
- substr: (field, start, length) => expr({ $substrBytes: [$2(field), start, length] }),
177
- // Comparison
178
- eq: (field, value) => expr({ $eq: [$2(field), value] }),
179
- gt: (field, value) => expr({ $gt: [$2(field), value] }),
180
- gte: (field, value) => expr({ $gte: [$2(field), value] }),
181
- lt: (field, value) => expr({ $lt: [$2(field), value] }),
182
- lte: (field, value) => expr({ $lte: [$2(field), value] }),
183
- 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)] }),
184
201
  // Date
185
- year: (field) => expr({ $year: $2(field) }),
186
- month: (field) => expr({ $month: $2(field) }),
187
- 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) }),
188
205
  // Array
189
- size: (field) => expr({ $size: $2(field) }),
206
+ size: (field) => expr({ $size: resolveArg(field) }),
190
207
  // Conditional
191
- cond: (condition, thenValue, elseValue) => expr({ $cond: [condition.value, thenValue, elseValue] }),
192
- ifNull: (field, fallback) => expr({ $ifNull: [$2(field), fallback] })
193
- // 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>.
194
237
  };
195
238
  }
196
239
 
@@ -466,10 +509,12 @@ var AggregatePipeline = class _AggregatePipeline {
466
509
  definition;
467
510
  nativeCollection;
468
511
  stages;
469
- constructor(definition, nativeCollection, stages) {
512
+ session;
513
+ constructor(definition, nativeCollection, stages, session) {
470
514
  this.definition = definition;
471
515
  this.nativeCollection = nativeCollection;
472
516
  this.stages = stages;
517
+ this.session = session;
473
518
  }
474
519
  /**
475
520
  * Append an arbitrary aggregation stage to the pipeline (escape hatch).
@@ -500,10 +545,12 @@ var AggregatePipeline = class _AggregatePipeline {
500
545
  * ```
501
546
  */
502
547
  raw(stage) {
503
- return new _AggregatePipeline(this.definition, this.nativeCollection, [
504
- ...this.stages,
505
- stage
506
- ]);
548
+ return new _AggregatePipeline(
549
+ this.definition,
550
+ this.nativeCollection,
551
+ [...this.stages, stage],
552
+ this.session
553
+ );
507
554
  }
508
555
  /**
509
556
  * Execute the pipeline and return all results as an array.
@@ -519,7 +566,10 @@ var AggregatePipeline = class _AggregatePipeline {
519
566
  */
520
567
  async toArray() {
521
568
  try {
522
- const cursor = this.nativeCollection.aggregate(this.stages);
569
+ const cursor = this.nativeCollection.aggregate(
570
+ this.stages,
571
+ this.session ? { session: this.session } : {}
572
+ );
523
573
  return await cursor.toArray();
524
574
  } catch (err) {
525
575
  wrapMongoError(err, this.definition.name);
@@ -539,7 +589,10 @@ var AggregatePipeline = class _AggregatePipeline {
539
589
  */
540
590
  async *[Symbol.asyncIterator]() {
541
591
  try {
542
- const cursor = this.nativeCollection.aggregate(this.stages);
592
+ const cursor = this.nativeCollection.aggregate(
593
+ this.stages,
594
+ this.session ? { session: this.session } : {}
595
+ );
543
596
  for await (const doc of cursor) {
544
597
  yield doc;
545
598
  }
@@ -565,7 +618,10 @@ var AggregatePipeline = class _AggregatePipeline {
565
618
  */
566
619
  async explain() {
567
620
  try {
568
- const cursor = this.nativeCollection.aggregate(this.stages);
621
+ const cursor = this.nativeCollection.aggregate(
622
+ this.stages,
623
+ this.session ? { session: this.session } : {}
624
+ );
569
625
  return await cursor.explain();
570
626
  } catch (err) {
571
627
  wrapMongoError(err, this.definition.name);
@@ -615,12 +671,30 @@ var AggregatePipeline = class _AggregatePipeline {
615
671
  * .toArray()
616
672
  * // subset[0].role → 'engineer' | 'designer'
617
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
+ * ```
618
685
  */
619
- match(filter) {
620
- const pipeline = new _AggregatePipeline(this.definition, this.nativeCollection, [
621
- ...this.stages,
622
- { $match: filter }
623
- ]);
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
+ );
624
698
  return pipeline;
625
699
  }
626
700
  /**
@@ -640,10 +714,12 @@ var AggregatePipeline = class _AggregatePipeline {
640
714
  * ```
641
715
  */
642
716
  sort(spec) {
643
- return new _AggregatePipeline(this.definition, this.nativeCollection, [
644
- ...this.stages,
645
- { $sort: spec }
646
- ]);
717
+ return new _AggregatePipeline(
718
+ this.definition,
719
+ this.nativeCollection,
720
+ [...this.stages, { $sort: spec }],
721
+ this.session
722
+ );
647
723
  }
648
724
  /**
649
725
  * Skip a number of documents in the pipeline.
@@ -664,10 +740,12 @@ var AggregatePipeline = class _AggregatePipeline {
664
740
  * ```
665
741
  */
666
742
  skip(n) {
667
- return new _AggregatePipeline(this.definition, this.nativeCollection, [
668
- ...this.stages,
669
- { $skip: n }
670
- ]);
743
+ return new _AggregatePipeline(
744
+ this.definition,
745
+ this.nativeCollection,
746
+ [...this.stages, { $skip: n }],
747
+ this.session
748
+ );
671
749
  }
672
750
  /**
673
751
  * Limit the number of documents passing through the pipeline.
@@ -687,10 +765,12 @@ var AggregatePipeline = class _AggregatePipeline {
687
765
  * ```
688
766
  */
689
767
  limit(n) {
690
- return new _AggregatePipeline(this.definition, this.nativeCollection, [
691
- ...this.stages,
692
- { $limit: n }
693
- ]);
768
+ return new _AggregatePipeline(
769
+ this.definition,
770
+ this.nativeCollection,
771
+ [...this.stages, { $limit: n }],
772
+ this.session
773
+ );
694
774
  }
695
775
  // ── Shape-transforming projection stages ─────────────────────────
696
776
  /**
@@ -712,10 +792,12 @@ var AggregatePipeline = class _AggregatePipeline {
712
792
  * ```
713
793
  */
714
794
  project(spec) {
715
- const pipeline = new _AggregatePipeline(this.definition, this.nativeCollection, [
716
- ...this.stages,
717
- { $project: spec }
718
- ]);
795
+ const pipeline = new _AggregatePipeline(
796
+ this.definition,
797
+ this.nativeCollection,
798
+ [...this.stages, { $project: spec }],
799
+ this.session
800
+ );
719
801
  return pipeline;
720
802
  }
721
803
  /**
@@ -736,10 +818,12 @@ var AggregatePipeline = class _AggregatePipeline {
736
818
  */
737
819
  pick(...fields) {
738
820
  const spec = Object.fromEntries(fields.map((f) => [f, 1]));
739
- const pipeline = new _AggregatePipeline(this.definition, this.nativeCollection, [
740
- ...this.stages,
741
- { $project: spec }
742
- ]);
821
+ const pipeline = new _AggregatePipeline(
822
+ this.definition,
823
+ this.nativeCollection,
824
+ [...this.stages, { $project: spec }],
825
+ this.session
826
+ );
743
827
  return pipeline;
744
828
  }
745
829
  /**
@@ -760,22 +844,41 @@ var AggregatePipeline = class _AggregatePipeline {
760
844
  */
761
845
  omit(...fields) {
762
846
  const spec = Object.fromEntries(fields.map((f) => [f, 0]));
763
- const pipeline = new _AggregatePipeline(this.definition, this.nativeCollection, [
764
- ...this.stages,
765
- { $project: spec }
766
- ]);
847
+ const pipeline = new _AggregatePipeline(
848
+ this.definition,
849
+ this.nativeCollection,
850
+ [...this.stages, { $project: spec }],
851
+ this.session
852
+ );
767
853
  return pipeline;
768
854
  }
769
855
  groupBy(field, accumulators) {
770
856
  const resolved = typeof accumulators === "function" ? accumulators(createAccumulatorBuilder()) : accumulators;
771
- 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
+ }
772
873
  const accumExprs = Object.fromEntries(
773
874
  Object.entries(resolved).map(([key, acc]) => [key, acc.expr])
774
875
  );
775
- const pipeline = new _AggregatePipeline(this.definition, this.nativeCollection, [
776
- ...this.stages,
777
- { $group: { _id, ...accumExprs } }
778
- ]);
876
+ const pipeline = new _AggregatePipeline(
877
+ this.definition,
878
+ this.nativeCollection,
879
+ [...this.stages, { $group: { _id, ...accumExprs } }],
880
+ this.session
881
+ );
779
882
  return pipeline;
780
883
  }
781
884
  // Implementation
@@ -787,10 +890,12 @@ var AggregatePipeline = class _AggregatePipeline {
787
890
  v && typeof v === "object" && "__expr" in v ? v.value : v
788
891
  ])
789
892
  );
790
- const pipeline = new _AggregatePipeline(this.definition, this.nativeCollection, [
791
- ...this.stages,
792
- { $addFields: stage }
793
- ]);
893
+ const pipeline = new _AggregatePipeline(
894
+ this.definition,
895
+ this.nativeCollection,
896
+ [...this.stages, { $addFields: stage }],
897
+ this.session
898
+ );
794
899
  return pipeline;
795
900
  }
796
901
  // ── unwind stage ─────────────────────────────────────────────────
@@ -816,10 +921,12 @@ var AggregatePipeline = class _AggregatePipeline {
816
921
  */
817
922
  unwind(field, options) {
818
923
  const stage = options?.preserveEmpty ? { $unwind: { path: `$${field}`, preserveNullAndEmptyArrays: true } } : { $unwind: `$${field}` };
819
- const pipeline = new _AggregatePipeline(this.definition, this.nativeCollection, [
820
- ...this.stages,
821
- stage
822
- ]);
924
+ const pipeline = new _AggregatePipeline(
925
+ this.definition,
926
+ this.nativeCollection,
927
+ [...this.stages, stage],
928
+ this.session
929
+ );
823
930
  return pipeline;
824
931
  }
825
932
  lookup(fieldOrFrom, options) {
@@ -867,7 +974,63 @@ var AggregatePipeline = class _AggregatePipeline {
867
974
  stages.push({ $unwind: { path: `$${asField}`, preserveNullAndEmptyArrays: true } });
868
975
  }
869
976
  }
870
- 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
+ );
871
1034
  return pipeline;
872
1035
  }
873
1036
  // ── Convenience shortcuts ────────────────────────────────────────
@@ -888,11 +1051,16 @@ var AggregatePipeline = class _AggregatePipeline {
888
1051
  * ```
889
1052
  */
890
1053
  countBy(field) {
891
- const pipeline = new _AggregatePipeline(this.definition, this.nativeCollection, [
892
- ...this.stages,
893
- { $group: { _id: `$${field}`, count: { $sum: 1 } } },
894
- { $sort: { count: -1 } }
895
- ]);
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
+ );
896
1064
  return pipeline;
897
1065
  }
898
1066
  /**
@@ -913,11 +1081,16 @@ var AggregatePipeline = class _AggregatePipeline {
913
1081
  * ```
914
1082
  */
915
1083
  sumBy(field, sumField) {
916
- const pipeline = new _AggregatePipeline(this.definition, this.nativeCollection, [
917
- ...this.stages,
918
- { $group: { _id: `$${field}`, total: { $sum: `$${sumField}` } } },
919
- { $sort: { total: -1 } }
920
- ]);
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
+ );
921
1094
  return pipeline;
922
1095
  }
923
1096
  /**
@@ -937,10 +1110,12 @@ var AggregatePipeline = class _AggregatePipeline {
937
1110
  * ```
938
1111
  */
939
1112
  sortBy(field, direction = "asc") {
940
- return new _AggregatePipeline(this.definition, this.nativeCollection, [
941
- ...this.stages,
942
- { $sort: { [field]: direction === "desc" ? -1 : 1 } }
943
- ]);
1113
+ return new _AggregatePipeline(
1114
+ this.definition,
1115
+ this.nativeCollection,
1116
+ [...this.stages, { $sort: { [field]: direction === "desc" ? -1 : 1 } }],
1117
+ this.session
1118
+ );
944
1119
  }
945
1120
  /**
946
1121
  * Return the top N documents sorted by a field descending.
@@ -959,11 +1134,12 @@ var AggregatePipeline = class _AggregatePipeline {
959
1134
  * ```
960
1135
  */
961
1136
  top(n, options) {
962
- return new _AggregatePipeline(this.definition, this.nativeCollection, [
963
- ...this.stages,
964
- { $sort: { [options.by]: -1 } },
965
- { $limit: n }
966
- ]);
1137
+ return new _AggregatePipeline(
1138
+ this.definition,
1139
+ this.nativeCollection,
1140
+ [...this.stages, { $sort: { [options.by]: -1 } }, { $limit: n }],
1141
+ this.session
1142
+ );
967
1143
  }
968
1144
  /**
969
1145
  * Return the bottom N documents sorted by a field ascending.
@@ -982,15 +1158,25 @@ var AggregatePipeline = class _AggregatePipeline {
982
1158
  * ```
983
1159
  */
984
1160
  bottom(n, options) {
985
- return new _AggregatePipeline(this.definition, this.nativeCollection, [
986
- ...this.stages,
987
- { $sort: { [options.by]: 1 } },
988
- { $limit: n }
989
- ]);
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;
990
1171
  }
991
1172
  };
992
1173
  function aggregate(handle) {
993
- return new AggregatePipeline(handle.definition, handle.native, []);
1174
+ return new AggregatePipeline(
1175
+ handle.definition,
1176
+ handle.native,
1177
+ [],
1178
+ handle.session
1179
+ );
994
1180
  }
995
1181
 
996
1182
  // src/client/client.ts
@@ -1172,6 +1358,36 @@ async function syncIndexes(handle, options) {
1172
1358
  };
1173
1359
  }
1174
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
+
1175
1391
  // src/crud/delete.ts
1176
1392
  var import_zod2 = require("zod");
1177
1393
 
@@ -1196,14 +1412,22 @@ var ZodmonValidationError = class extends ZodmonError {
1196
1412
  // src/crud/delete.ts
1197
1413
  async function deleteOne(handle, filter) {
1198
1414
  try {
1199
- 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
+ );
1200
1420
  } catch (err) {
1201
1421
  wrapMongoError(err, handle.definition.name);
1202
1422
  }
1203
1423
  }
1204
1424
  async function deleteMany(handle, filter) {
1205
1425
  try {
1206
- 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
+ );
1207
1431
  } catch (err) {
1208
1432
  wrapMongoError(err, handle.definition.name);
1209
1433
  }
@@ -1214,7 +1438,7 @@ async function findOneAndDelete(handle, filter, options) {
1214
1438
  result = await handle.native.findOneAndDelete(
1215
1439
  // biome-ignore lint/suspicious/noExplicitAny: TypedFilter intersection type is not directly assignable to MongoDB's Filter
1216
1440
  filter,
1217
- { includeResultMetadata: false }
1441
+ handle.session ? { includeResultMetadata: false, session: handle.session } : { includeResultMetadata: false }
1218
1442
  );
1219
1443
  } catch (err) {
1220
1444
  wrapMongoError(err, handle.definition.name);
@@ -1225,7 +1449,8 @@ async function findOneAndDelete(handle, filter, options) {
1225
1449
  return result;
1226
1450
  }
1227
1451
  try {
1228
- return handle.definition.schema.parse(result);
1452
+ const schema = mode === "strict" ? handle.definition.strictSchema : handle.definition.schema;
1453
+ return schema.parse(result);
1229
1454
  } catch (err) {
1230
1455
  if (err instanceof import_zod2.z.ZodError) {
1231
1456
  throw new ZodmonValidationError(handle.definition.name, err, result);
@@ -1235,7 +1460,7 @@ async function findOneAndDelete(handle, filter, options) {
1235
1460
  }
1236
1461
 
1237
1462
  // src/crud/find.ts
1238
- var import_zod4 = require("zod");
1463
+ var import_zod5 = require("zod");
1239
1464
 
1240
1465
  // src/errors/not-found.ts
1241
1466
  var ZodmonNotFoundError = class extends ZodmonError {
@@ -1272,7 +1497,7 @@ function checkUnindexedFields(definition, filter) {
1272
1497
  }
1273
1498
 
1274
1499
  // src/query/cursor.ts
1275
- var import_zod3 = require("zod");
1500
+ var import_zod4 = require("zod");
1276
1501
 
1277
1502
  // src/crud/paginate.ts
1278
1503
  var import_mongodb2 = require("mongodb");
@@ -1335,6 +1560,258 @@ function resolveSortKeys(sortSpec) {
1335
1560
  return entries;
1336
1561
  }
1337
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
+
1338
1815
  // src/query/projection.ts
1339
1816
  function isIncludeValue(value) {
1340
1817
  return value === 1 || value === true;
@@ -1388,6 +1865,8 @@ var TypedFindCursor = class {
1388
1865
  /** @internal */
1389
1866
  cursor;
1390
1867
  /** @internal */
1868
+ definition;
1869
+ /** @internal */
1391
1870
  schema;
1392
1871
  /** @internal */
1393
1872
  collectionName;
@@ -1399,17 +1878,21 @@ var TypedFindCursor = class {
1399
1878
  // biome-ignore lint/suspicious/noExplicitAny: TypedFilter is not assignable to MongoDB's Filter; stored opaquely for paginate
1400
1879
  filter;
1401
1880
  /** @internal */
1881
+ session;
1882
+ /** @internal */
1402
1883
  sortSpec;
1403
1884
  /** @internal */
1404
1885
  projectedSchema;
1405
1886
  /** @internal */
1406
- constructor(cursor, definition, mode, nativeCollection, filter) {
1887
+ constructor(cursor, definition, mode, nativeCollection, filter, session) {
1407
1888
  this.cursor = cursor;
1889
+ this.definition = definition;
1408
1890
  this.schema = definition.schema;
1409
1891
  this.collectionName = definition.name;
1410
1892
  this.mode = mode;
1411
1893
  this.nativeCollection = nativeCollection;
1412
1894
  this.filter = filter;
1895
+ this.session = session;
1413
1896
  this.sortSpec = null;
1414
1897
  this.projectedSchema = null;
1415
1898
  }
@@ -1519,6 +2002,14 @@ var TypedFindCursor = class {
1519
2002
  );
1520
2003
  return this;
1521
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
+ }
1522
2013
  async paginate(opts) {
1523
2014
  const sortRecord = this.sortSpec ? this.sortSpec : null;
1524
2015
  const sortKeys2 = resolveSortKeys(sortRecord);
@@ -1535,8 +2026,11 @@ var TypedFindCursor = class {
1535
2026
  try {
1536
2027
  ;
1537
2028
  [total, raw2] = await Promise.all([
1538
- this.nativeCollection.countDocuments(this.filter),
1539
- 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()
1540
2034
  ]);
1541
2035
  } catch (err) {
1542
2036
  wrapMongoError(err, this.collectionName);
@@ -1566,7 +2060,7 @@ var TypedFindCursor = class {
1566
2060
  const effectiveSort = isBackward ? Object.fromEntries(sortKeys2.map(([f, d]) => [f, d === 1 ? -1 : 1])) : sort;
1567
2061
  let raw2;
1568
2062
  try {
1569
- 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();
1570
2064
  } catch (err) {
1571
2065
  wrapMongoError(err, this.collectionName);
1572
2066
  }
@@ -1635,11 +2129,11 @@ var TypedFindCursor = class {
1635
2129
  if (this.mode === false || this.mode === "passthrough") {
1636
2130
  return raw2;
1637
2131
  }
1638
- const schema = this.projectedSchema ?? this.schema;
2132
+ const schema = this.projectedSchema ?? (this.mode === "strict" ? this.definition.strictSchema : this.schema);
1639
2133
  try {
1640
2134
  return schema.parse(raw2);
1641
2135
  } catch (err) {
1642
- if (err instanceof import_zod3.z.ZodError) {
2136
+ if (err instanceof import_zod4.z.ZodError) {
1643
2137
  throw new ZodmonValidationError(this.collectionName, err, raw2);
1644
2138
  }
1645
2139
  throw err;
@@ -1654,7 +2148,11 @@ async function findOne(handle, filter, options) {
1654
2148
  const findOptions = project ? { projection: project } : void 0;
1655
2149
  let raw2;
1656
2150
  try {
1657
- 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
+ );
1658
2156
  } catch (err) {
1659
2157
  wrapMongoError(err, handle.definition.name);
1660
2158
  }
@@ -1666,11 +2164,11 @@ async function findOne(handle, filter, options) {
1666
2164
  const schema = project ? deriveProjectedSchema(
1667
2165
  handle.definition.schema,
1668
2166
  project
1669
- ) : handle.definition.schema;
2167
+ ) : mode === "strict" ? handle.definition.strictSchema : handle.definition.schema;
1670
2168
  try {
1671
2169
  return schema.parse(raw2);
1672
2170
  } catch (err) {
1673
- if (err instanceof import_zod4.z.ZodError) {
2171
+ if (err instanceof import_zod5.z.ZodError) {
1674
2172
  throw new ZodmonValidationError(handle.definition.name, err, raw2);
1675
2173
  }
1676
2174
  throw err;
@@ -1685,10 +2183,21 @@ async function findOneOrThrow(handle, filter, options) {
1685
2183
  }
1686
2184
  function find(handle, filter, options) {
1687
2185
  checkUnindexedFields(handle.definition, filter);
1688
- 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
+ );
1689
2191
  const cursor = raw2;
1690
2192
  const mode = options?.validate !== void 0 ? options.validate : handle.definition.options.validation;
1691
- const typedCursor = 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
+ );
1692
2201
  const project = options && "project" in options ? options.project : void 0;
1693
2202
  if (project) {
1694
2203
  return typedCursor.project(project);
@@ -1697,19 +2206,19 @@ function find(handle, filter, options) {
1697
2206
  }
1698
2207
 
1699
2208
  // src/crud/insert.ts
1700
- var import_zod5 = require("zod");
2209
+ var import_zod6 = require("zod");
1701
2210
  async function insertOne(handle, doc) {
1702
2211
  let parsed;
1703
2212
  try {
1704
2213
  parsed = handle.definition.schema.parse(doc);
1705
2214
  } catch (err) {
1706
- if (err instanceof import_zod5.z.ZodError) {
2215
+ if (err instanceof import_zod6.z.ZodError) {
1707
2216
  throw new ZodmonValidationError(handle.definition.name, err, doc);
1708
2217
  }
1709
2218
  throw err;
1710
2219
  }
1711
2220
  try {
1712
- await handle.native.insertOne(parsed);
2221
+ await handle.native.insertOne(parsed, handle.session ? { session: handle.session } : {});
1713
2222
  } catch (err) {
1714
2223
  wrapMongoError(err, handle.definition.name);
1715
2224
  }
@@ -1722,14 +2231,14 @@ async function insertMany(handle, docs) {
1722
2231
  try {
1723
2232
  parsed.push(handle.definition.schema.parse(doc));
1724
2233
  } catch (err) {
1725
- if (err instanceof import_zod5.z.ZodError) {
2234
+ if (err instanceof import_zod6.z.ZodError) {
1726
2235
  throw new ZodmonValidationError(handle.definition.name, err, doc);
1727
2236
  }
1728
2237
  throw err;
1729
2238
  }
1730
2239
  }
1731
2240
  try {
1732
- await handle.native.insertMany(parsed);
2241
+ await handle.native.insertMany(parsed, handle.session ? { session: handle.session } : {});
1733
2242
  } catch (err) {
1734
2243
  wrapMongoError(err, handle.definition.name);
1735
2244
  }
@@ -1737,17 +2246,29 @@ async function insertMany(handle, docs) {
1737
2246
  }
1738
2247
 
1739
2248
  // src/crud/update.ts
1740
- var import_zod6 = require("zod");
2249
+ var import_zod7 = require("zod");
1741
2250
  async function updateOne(handle, filter, update, options) {
1742
2251
  try {
1743
- 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
+ );
1744
2259
  } catch (err) {
1745
2260
  wrapMongoError(err, handle.definition.name);
1746
2261
  }
1747
2262
  }
1748
2263
  async function updateMany(handle, filter, update, options) {
1749
2264
  try {
1750
- 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
+ );
1751
2272
  } catch (err) {
1752
2273
  wrapMongoError(err, handle.definition.name);
1753
2274
  }
@@ -1760,6 +2281,9 @@ async function findOneAndUpdate(handle, filter, update, options) {
1760
2281
  if (options?.upsert !== void 0) {
1761
2282
  driverOptions["upsert"] = options.upsert;
1762
2283
  }
2284
+ if (handle.session) {
2285
+ driverOptions["session"] = handle.session;
2286
+ }
1763
2287
  let result;
1764
2288
  try {
1765
2289
  result = await handle.native.findOneAndUpdate(
@@ -1779,24 +2303,165 @@ async function findOneAndUpdate(handle, filter, update, options) {
1779
2303
  return result;
1780
2304
  }
1781
2305
  try {
1782
- return handle.definition.schema.parse(result);
2306
+ const schema = mode === "strict" ? handle.definition.strictSchema : handle.definition.schema;
2307
+ return schema.parse(result);
1783
2308
  } catch (err) {
1784
- if (err instanceof import_zod6.z.ZodError) {
2309
+ if (err instanceof import_zod7.z.ZodError) {
1785
2310
  throw new ZodmonValidationError(handle.definition.name, err, result);
1786
2311
  }
1787
2312
  throw err;
1788
2313
  }
1789
2314
  }
1790
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
+
1791
2425
  // src/client/handle.ts
1792
- var CollectionHandle = class {
2426
+ var CollectionHandle = class _CollectionHandle {
1793
2427
  /** The collection definition containing schema, name, and index metadata. */
1794
2428
  definition;
1795
2429
  /** The underlying MongoDB driver collection, typed to the inferred document type. */
1796
2430
  native;
1797
- 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) {
1798
2440
  this.definition = definition;
1799
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);
1800
2465
  }
1801
2466
  /**
1802
2467
  * Insert a single document into the collection.
@@ -1842,11 +2507,17 @@ var CollectionHandle = class {
1842
2507
  async insertMany(docs) {
1843
2508
  return await insertMany(this, docs);
1844
2509
  }
1845
- async findOne(filter, options) {
1846
- 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);
1847
2515
  }
1848
- async findOneOrThrow(filter, options) {
1849
- 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);
1850
2521
  }
1851
2522
  find(filter, options) {
1852
2523
  return find(this, filter, options);
@@ -2096,12 +2767,51 @@ var Database = class {
2096
2767
  return results;
2097
2768
  }
2098
2769
  /**
2099
- * 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.
2777
+ *
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
+ * ```
2100
2788
  *
2101
- * Stub — full implementation in TASK-106.
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
+ * ```
2102
2802
  */
2103
- transaction(_fn) {
2104
- 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
+ }
2105
2815
  }
2106
2816
  /**
2107
2817
  * Close the underlying `MongoClient` connection. Safe to call even if
@@ -2134,10 +2844,10 @@ function createClient(uri, dbNameOrOptions, maybeOptions) {
2134
2844
 
2135
2845
  // src/collection/collection.ts
2136
2846
  var import_mongodb5 = require("mongodb");
2137
- var import_zod9 = require("zod");
2847
+ var import_zod10 = require("zod");
2138
2848
 
2139
2849
  // src/schema/extensions.ts
2140
- var import_zod7 = require("zod");
2850
+ var import_zod8 = require("zod");
2141
2851
  var indexMetadata = /* @__PURE__ */ new WeakMap();
2142
2852
  function getIndexMetadata(schema) {
2143
2853
  if (typeof schema !== "object" || schema === null) return void 0;
@@ -2145,7 +2855,7 @@ function getIndexMetadata(schema) {
2145
2855
  }
2146
2856
  var GUARD = /* @__PURE__ */ Symbol.for("zodmon_extensions");
2147
2857
  function installExtensions() {
2148
- const proto = import_zod7.z.ZodType.prototype;
2858
+ const proto = import_zod8.z.ZodType.prototype;
2149
2859
  if (GUARD in proto) return;
2150
2860
  Object.defineProperty(proto, "index", {
2151
2861
  /**
@@ -2244,10 +2954,10 @@ installExtensions();
2244
2954
 
2245
2955
  // src/schema/object-id.ts
2246
2956
  var import_mongodb4 = require("mongodb");
2247
- var import_zod8 = require("zod");
2957
+ var import_zod9 = require("zod");
2248
2958
  var OBJECT_ID_HEX = /^[a-f\d]{24}$/i;
2249
2959
  function objectId() {
2250
- return import_zod8.z.custom((val) => {
2960
+ return import_zod9.z.custom((val) => {
2251
2961
  if (val instanceof import_mongodb4.ObjectId) return true;
2252
2962
  return typeof val === "string" && OBJECT_ID_HEX.test(val);
2253
2963
  }, "Invalid ObjectId").transform((val) => val instanceof import_mongodb4.ObjectId ? val : import_mongodb4.ObjectId.createFromHexString(val));
@@ -2266,7 +2976,8 @@ function extractFieldIndexes(shape) {
2266
2976
  }
2267
2977
  function collection(name, shape, options) {
2268
2978
  const resolvedShape = "_id" in shape ? shape : { _id: objectId().default(() => new import_mongodb5.ObjectId()), ...shape };
2269
- const schema = import_zod9.z.object(resolvedShape);
2979
+ const schema = import_zod10.z.object(resolvedShape);
2980
+ const strictSchema = schema.strict();
2270
2981
  const fieldIndexes = extractFieldIndexes(shape);
2271
2982
  const { indexes: compoundIndexes, validation, ...rest } = options ?? {};
2272
2983
  return {
@@ -2276,6 +2987,7 @@ function collection(name, shape, options) {
2276
2987
  // not assignable to ZodObject<ResolvedShape<TShape>>. The cast is safe because
2277
2988
  // the runtime shape is correct — only the readonly modifier differs.
2278
2989
  schema,
2990
+ strictSchema,
2279
2991
  shape,
2280
2992
  fieldIndexes,
2281
2993
  // Safe cast: compoundIndexes is TIndexes at runtime (or an empty array when
@@ -2415,6 +3127,11 @@ var $ = {
2415
3127
  CollectionHandle,
2416
3128
  Database,
2417
3129
  IndexBuilder,
3130
+ PopulateCursor,
3131
+ PopulateOneOrThrowQuery,
3132
+ PopulateOneQuery,
3133
+ PopulateRefBuilder,
3134
+ TransactionContext,
2418
3135
  TypedFindCursor,
2419
3136
  ZodmonAuthError,
2420
3137
  ZodmonBulkWriteError,
@@ -2434,9 +3151,11 @@ var $ = {
2434
3151
  createAccumulatorBuilder,
2435
3152
  createClient,
2436
3153
  createExpressionBuilder,
3154
+ createPopulateCursor,
2437
3155
  deleteMany,
2438
3156
  deleteOne,
2439
3157
  deriveProjectedSchema,
3158
+ executePopulate,
2440
3159
  extractComparableOptions,
2441
3160
  extractDbName,
2442
3161
  extractFieldIndexes,
@@ -2456,10 +3175,12 @@ var $ = {
2456
3175
  objectId,
2457
3176
  oid,
2458
3177
  raw,
3178
+ resolvePopulateStep,
2459
3179
  serializeIndexKey,
2460
3180
  syncIndexes,
2461
3181
  toCompoundIndexSpec,
2462
3182
  toFieldIndexSpec,
3183
+ unwrapRefSchema,
2463
3184
  updateMany,
2464
3185
  updateOne,
2465
3186
  wrapMongoError