@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.js CHANGED
@@ -46,50 +46,84 @@ function createAccumulatorBuilder() {
46
46
  // 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.
47
47
  };
48
48
  }
49
+ var isExpr = (v) => typeof v === "object" && v !== null && v.__expr === true;
49
50
  function createExpressionBuilder() {
50
- const $2 = (field) => `$${field}`;
51
- const val = (v) => typeof v === "number" ? v : `$${v}`;
51
+ const resolveArg = (arg) => {
52
+ if (typeof arg === "number") return arg;
53
+ if (isExpr(arg)) return arg.value;
54
+ return `$${arg}`;
55
+ };
56
+ const resolveExprVal = (v) => isExpr(v) ? v.value : v;
52
57
  const expr = (value) => ({ __expr: true, value });
53
58
  return {
54
59
  // Arithmetic
55
- add: (field, value) => expr({ $add: [$2(field), val(value)] }),
56
- subtract: (field, value) => expr({ $subtract: [$2(field), val(value)] }),
57
- multiply: (field, value) => expr({ $multiply: [$2(field), val(value)] }),
58
- divide: (field, value) => expr({ $divide: [$2(field), val(value)] }),
59
- mod: (field, value) => expr({ $mod: [$2(field), val(value)] }),
60
- abs: (field) => expr({ $abs: $2(field) }),
61
- ceil: (field) => expr({ $ceil: $2(field) }),
62
- floor: (field) => expr({ $floor: $2(field) }),
63
- round: (field, place = 0) => expr({ $round: [$2(field), place] }),
60
+ add: (a, b) => expr({ $add: [resolveArg(a), resolveArg(b)] }),
61
+ subtract: (a, b) => expr({ $subtract: [resolveArg(a), resolveArg(b)] }),
62
+ multiply: (a, b) => expr({ $multiply: [resolveArg(a), resolveArg(b)] }),
63
+ divide: (a, b) => expr({ $divide: [resolveArg(a), resolveArg(b)] }),
64
+ mod: (a, b) => expr({ $mod: [resolveArg(a), resolveArg(b)] }),
65
+ abs: (field) => expr({ $abs: resolveArg(field) }),
66
+ ceil: (field) => expr({ $ceil: resolveArg(field) }),
67
+ floor: (field) => expr({ $floor: resolveArg(field) }),
68
+ round: (field, place = 0) => expr({ $round: [resolveArg(field), place] }),
64
69
  // String
65
70
  concat: (...parts) => {
66
71
  const resolved = parts.map((p) => {
67
- if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(p)) return $2(p);
72
+ if (isExpr(p)) return p.value;
73
+ if (/^[a-zA-Z_][a-zA-Z0-9_.]*$/.test(p)) return `$${p}`;
68
74
  return p;
69
75
  });
70
76
  return expr({ $concat: resolved });
71
77
  },
72
- toLower: (field) => expr({ $toLower: $2(field) }),
73
- toUpper: (field) => expr({ $toUpper: $2(field) }),
74
- trim: (field) => expr({ $trim: { input: $2(field) } }),
75
- substr: (field, start, length) => expr({ $substrBytes: [$2(field), start, length] }),
76
- // Comparison
77
- eq: (field, value) => expr({ $eq: [$2(field), value] }),
78
- gt: (field, value) => expr({ $gt: [$2(field), value] }),
79
- gte: (field, value) => expr({ $gte: [$2(field), value] }),
80
- lt: (field, value) => expr({ $lt: [$2(field), value] }),
81
- lte: (field, value) => expr({ $lte: [$2(field), value] }),
82
- ne: (field, value) => expr({ $ne: [$2(field), value] }),
78
+ toLower: (field) => expr({ $toLower: resolveArg(field) }),
79
+ toUpper: (field) => expr({ $toUpper: resolveArg(field) }),
80
+ trim: (field) => expr({ $trim: { input: resolveArg(field) } }),
81
+ substr: (field, start, length) => expr({ $substrBytes: [resolveArg(field), start, length] }),
82
+ // Comparison — single runtime implementation handles both overloads:
83
+ // field path → resolveArg('name') → '$name'
84
+ // expression → resolveArg(expr.sub(...)) { $subtract: [...] }
85
+ eq: (field, value) => expr({ $eq: [resolveArg(field), resolveExprVal(value)] }),
86
+ gt: (field, value) => expr({ $gt: [resolveArg(field), resolveExprVal(value)] }),
87
+ gte: (field, value) => expr({ $gte: [resolveArg(field), resolveExprVal(value)] }),
88
+ lt: (field, value) => expr({ $lt: [resolveArg(field), resolveExprVal(value)] }),
89
+ lte: (field, value) => expr({ $lte: [resolveArg(field), resolveExprVal(value)] }),
90
+ ne: (field, value) => expr({ $ne: [resolveArg(field), resolveExprVal(value)] }),
83
91
  // Date
84
- year: (field) => expr({ $year: $2(field) }),
85
- month: (field) => expr({ $month: $2(field) }),
86
- dayOfMonth: (field) => expr({ $dayOfMonth: $2(field) }),
92
+ year: (field) => expr({ $year: resolveArg(field) }),
93
+ month: (field) => expr({ $month: resolveArg(field) }),
94
+ dayOfMonth: (field) => expr({ $dayOfMonth: resolveArg(field) }),
87
95
  // Array
88
- size: (field) => expr({ $size: $2(field) }),
96
+ size: (field) => expr({ $size: resolveArg(field) }),
89
97
  // Conditional
90
- cond: (condition, thenValue, elseValue) => expr({ $cond: [condition.value, thenValue, elseValue] }),
91
- ifNull: (field, fallback) => expr({ $ifNull: [$2(field), fallback] })
92
- // 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.
98
+ cond: (condition, thenValue, elseValue) => expr({
99
+ $cond: [condition.value, resolveExprVal(thenValue), resolveExprVal(elseValue)]
100
+ }),
101
+ ifNull: (field, fallback) => expr({ $ifNull: [resolveArg(field), fallback] }),
102
+ // Date (extended)
103
+ dayOfWeek: (field) => expr({ $dayOfWeek: resolveArg(field) }),
104
+ dateToString: (field, format) => expr({ $dateToString: { format, date: resolveArg(field) } }),
105
+ // $$NOW is a MongoDB system variable string — not a Document, but valid anywhere
106
+ // an aggregation expression is expected. Cast is safe; the MongoDB driver accepts it.
107
+ now: () => ({ __expr: true, value: "$$NOW" }),
108
+ // String conversion
109
+ toString: (field) => expr({ $toString: resolveArg(field) }),
110
+ // Array (extended)
111
+ inArray: (value, array) => expr({ $in: [resolveArg(value), Array.isArray(array) ? array : resolveArg(array)] }),
112
+ arrayElemAt: (field, index2) => expr({ $arrayElemAt: [resolveArg(field), index2] }),
113
+ // Conditional (extended)
114
+ switch: (branches, fallback) => expr({
115
+ $switch: {
116
+ branches: branches.map((b) => ({
117
+ case: b.case.value,
118
+ // biome-ignore lint/suspicious/noThenProperty: MongoDB $switch branch object requires a `then` key
119
+ then: resolveExprVal(b.then)
120
+ })),
121
+ default: resolveExprVal(fallback)
122
+ }
123
+ }),
124
+ // Field reference
125
+ field: (name) => ({ __expr: true, value: `$${name}` })
126
+ // 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>.
93
127
  };
94
128
  }
95
129
 
@@ -365,10 +399,12 @@ var AggregatePipeline = class _AggregatePipeline {
365
399
  definition;
366
400
  nativeCollection;
367
401
  stages;
368
- constructor(definition, nativeCollection, stages) {
402
+ session;
403
+ constructor(definition, nativeCollection, stages, session) {
369
404
  this.definition = definition;
370
405
  this.nativeCollection = nativeCollection;
371
406
  this.stages = stages;
407
+ this.session = session;
372
408
  }
373
409
  /**
374
410
  * Append an arbitrary aggregation stage to the pipeline (escape hatch).
@@ -399,10 +435,12 @@ var AggregatePipeline = class _AggregatePipeline {
399
435
  * ```
400
436
  */
401
437
  raw(stage) {
402
- return new _AggregatePipeline(this.definition, this.nativeCollection, [
403
- ...this.stages,
404
- stage
405
- ]);
438
+ return new _AggregatePipeline(
439
+ this.definition,
440
+ this.nativeCollection,
441
+ [...this.stages, stage],
442
+ this.session
443
+ );
406
444
  }
407
445
  /**
408
446
  * Execute the pipeline and return all results as an array.
@@ -418,7 +456,10 @@ var AggregatePipeline = class _AggregatePipeline {
418
456
  */
419
457
  async toArray() {
420
458
  try {
421
- const cursor = this.nativeCollection.aggregate(this.stages);
459
+ const cursor = this.nativeCollection.aggregate(
460
+ this.stages,
461
+ this.session ? { session: this.session } : {}
462
+ );
422
463
  return await cursor.toArray();
423
464
  } catch (err) {
424
465
  wrapMongoError(err, this.definition.name);
@@ -438,7 +479,10 @@ var AggregatePipeline = class _AggregatePipeline {
438
479
  */
439
480
  async *[Symbol.asyncIterator]() {
440
481
  try {
441
- const cursor = this.nativeCollection.aggregate(this.stages);
482
+ const cursor = this.nativeCollection.aggregate(
483
+ this.stages,
484
+ this.session ? { session: this.session } : {}
485
+ );
442
486
  for await (const doc of cursor) {
443
487
  yield doc;
444
488
  }
@@ -464,7 +508,10 @@ var AggregatePipeline = class _AggregatePipeline {
464
508
  */
465
509
  async explain() {
466
510
  try {
467
- const cursor = this.nativeCollection.aggregate(this.stages);
511
+ const cursor = this.nativeCollection.aggregate(
512
+ this.stages,
513
+ this.session ? { session: this.session } : {}
514
+ );
468
515
  return await cursor.explain();
469
516
  } catch (err) {
470
517
  wrapMongoError(err, this.definition.name);
@@ -514,12 +561,30 @@ var AggregatePipeline = class _AggregatePipeline {
514
561
  * .toArray()
515
562
  * // subset[0].role → 'engineer' | 'designer'
516
563
  * ```
564
+ *
565
+ * @example
566
+ * ```ts
567
+ * // Field-vs-field comparison via $expr callback
568
+ * const overRefunded = await orders.aggregate()
569
+ * .match(
570
+ * { status: 'completed' },
571
+ * (expr) => expr.gt('totalAmount', expr.field('refundedAmount')),
572
+ * )
573
+ * .toArray()
574
+ * ```
517
575
  */
518
- match(filter) {
519
- const pipeline = new _AggregatePipeline(this.definition, this.nativeCollection, [
520
- ...this.stages,
521
- { $match: filter }
522
- ]);
576
+ match(filter, exprCb) {
577
+ const stage = { ...filter };
578
+ if (exprCb) {
579
+ const built = exprCb(createExpressionBuilder());
580
+ stage["$expr"] = built.value;
581
+ }
582
+ const pipeline = new _AggregatePipeline(
583
+ this.definition,
584
+ this.nativeCollection,
585
+ [...this.stages, { $match: stage }],
586
+ this.session
587
+ );
523
588
  return pipeline;
524
589
  }
525
590
  /**
@@ -539,10 +604,12 @@ var AggregatePipeline = class _AggregatePipeline {
539
604
  * ```
540
605
  */
541
606
  sort(spec) {
542
- return new _AggregatePipeline(this.definition, this.nativeCollection, [
543
- ...this.stages,
544
- { $sort: spec }
545
- ]);
607
+ return new _AggregatePipeline(
608
+ this.definition,
609
+ this.nativeCollection,
610
+ [...this.stages, { $sort: spec }],
611
+ this.session
612
+ );
546
613
  }
547
614
  /**
548
615
  * Skip a number of documents in the pipeline.
@@ -563,10 +630,12 @@ var AggregatePipeline = class _AggregatePipeline {
563
630
  * ```
564
631
  */
565
632
  skip(n) {
566
- return new _AggregatePipeline(this.definition, this.nativeCollection, [
567
- ...this.stages,
568
- { $skip: n }
569
- ]);
633
+ return new _AggregatePipeline(
634
+ this.definition,
635
+ this.nativeCollection,
636
+ [...this.stages, { $skip: n }],
637
+ this.session
638
+ );
570
639
  }
571
640
  /**
572
641
  * Limit the number of documents passing through the pipeline.
@@ -586,10 +655,12 @@ var AggregatePipeline = class _AggregatePipeline {
586
655
  * ```
587
656
  */
588
657
  limit(n) {
589
- return new _AggregatePipeline(this.definition, this.nativeCollection, [
590
- ...this.stages,
591
- { $limit: n }
592
- ]);
658
+ return new _AggregatePipeline(
659
+ this.definition,
660
+ this.nativeCollection,
661
+ [...this.stages, { $limit: n }],
662
+ this.session
663
+ );
593
664
  }
594
665
  // ── Shape-transforming projection stages ─────────────────────────
595
666
  /**
@@ -611,10 +682,12 @@ var AggregatePipeline = class _AggregatePipeline {
611
682
  * ```
612
683
  */
613
684
  project(spec) {
614
- const pipeline = new _AggregatePipeline(this.definition, this.nativeCollection, [
615
- ...this.stages,
616
- { $project: spec }
617
- ]);
685
+ const pipeline = new _AggregatePipeline(
686
+ this.definition,
687
+ this.nativeCollection,
688
+ [...this.stages, { $project: spec }],
689
+ this.session
690
+ );
618
691
  return pipeline;
619
692
  }
620
693
  /**
@@ -635,10 +708,12 @@ var AggregatePipeline = class _AggregatePipeline {
635
708
  */
636
709
  pick(...fields) {
637
710
  const spec = Object.fromEntries(fields.map((f) => [f, 1]));
638
- const pipeline = new _AggregatePipeline(this.definition, this.nativeCollection, [
639
- ...this.stages,
640
- { $project: spec }
641
- ]);
711
+ const pipeline = new _AggregatePipeline(
712
+ this.definition,
713
+ this.nativeCollection,
714
+ [...this.stages, { $project: spec }],
715
+ this.session
716
+ );
642
717
  return pipeline;
643
718
  }
644
719
  /**
@@ -659,22 +734,41 @@ var AggregatePipeline = class _AggregatePipeline {
659
734
  */
660
735
  omit(...fields) {
661
736
  const spec = Object.fromEntries(fields.map((f) => [f, 0]));
662
- const pipeline = new _AggregatePipeline(this.definition, this.nativeCollection, [
663
- ...this.stages,
664
- { $project: spec }
665
- ]);
737
+ const pipeline = new _AggregatePipeline(
738
+ this.definition,
739
+ this.nativeCollection,
740
+ [...this.stages, { $project: spec }],
741
+ this.session
742
+ );
666
743
  return pipeline;
667
744
  }
668
745
  groupBy(field, accumulators) {
669
746
  const resolved = typeof accumulators === "function" ? accumulators(createAccumulatorBuilder()) : accumulators;
670
- const _id = Array.isArray(field) ? Object.fromEntries(field.map((f) => [f, `$${f}`])) : `$${field}`;
747
+ let _id;
748
+ if (field === null) {
749
+ _id = null;
750
+ } else if (Array.isArray(field)) {
751
+ const entries = field.map((f) => [f.replaceAll(".", "_"), `$${f}`]);
752
+ const keys = entries.map(([k]) => k);
753
+ const dupes = keys.filter((k, i) => keys.indexOf(k) !== i);
754
+ if (dupes.length > 0) {
755
+ throw new Error(
756
+ `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.`
757
+ );
758
+ }
759
+ _id = Object.fromEntries(entries);
760
+ } else {
761
+ _id = `$${field}`;
762
+ }
671
763
  const accumExprs = Object.fromEntries(
672
764
  Object.entries(resolved).map(([key, acc]) => [key, acc.expr])
673
765
  );
674
- const pipeline = new _AggregatePipeline(this.definition, this.nativeCollection, [
675
- ...this.stages,
676
- { $group: { _id, ...accumExprs } }
677
- ]);
766
+ const pipeline = new _AggregatePipeline(
767
+ this.definition,
768
+ this.nativeCollection,
769
+ [...this.stages, { $group: { _id, ...accumExprs } }],
770
+ this.session
771
+ );
678
772
  return pipeline;
679
773
  }
680
774
  // Implementation
@@ -686,10 +780,12 @@ var AggregatePipeline = class _AggregatePipeline {
686
780
  v && typeof v === "object" && "__expr" in v ? v.value : v
687
781
  ])
688
782
  );
689
- const pipeline = new _AggregatePipeline(this.definition, this.nativeCollection, [
690
- ...this.stages,
691
- { $addFields: stage }
692
- ]);
783
+ const pipeline = new _AggregatePipeline(
784
+ this.definition,
785
+ this.nativeCollection,
786
+ [...this.stages, { $addFields: stage }],
787
+ this.session
788
+ );
693
789
  return pipeline;
694
790
  }
695
791
  // ── unwind stage ─────────────────────────────────────────────────
@@ -715,10 +811,12 @@ var AggregatePipeline = class _AggregatePipeline {
715
811
  */
716
812
  unwind(field, options) {
717
813
  const stage = options?.preserveEmpty ? { $unwind: { path: `$${field}`, preserveNullAndEmptyArrays: true } } : { $unwind: `$${field}` };
718
- const pipeline = new _AggregatePipeline(this.definition, this.nativeCollection, [
719
- ...this.stages,
720
- stage
721
- ]);
814
+ const pipeline = new _AggregatePipeline(
815
+ this.definition,
816
+ this.nativeCollection,
817
+ [...this.stages, stage],
818
+ this.session
819
+ );
722
820
  return pipeline;
723
821
  }
724
822
  lookup(fieldOrFrom, options) {
@@ -766,7 +864,63 @@ var AggregatePipeline = class _AggregatePipeline {
766
864
  stages.push({ $unwind: { path: `$${asField}`, preserveNullAndEmptyArrays: true } });
767
865
  }
768
866
  }
769
- const pipeline = new _AggregatePipeline(this.definition, this.nativeCollection, stages);
867
+ const pipeline = new _AggregatePipeline(
868
+ this.definition,
869
+ this.nativeCollection,
870
+ stages,
871
+ this.session
872
+ );
873
+ return pipeline;
874
+ }
875
+ // ── facet stage ──────────────────────────────────────────────────
876
+ /**
877
+ * Run multiple sub-pipelines on the same input documents in parallel.
878
+ *
879
+ * Each key in `spec` maps to a callback that receives a fresh `SubPipeline`
880
+ * starting from `TOutput`. The callback chains stages and returns the terminal
881
+ * pipeline. Zodmon extracts the accumulated stages at runtime to build the
882
+ * `$facet` document. The output type is fully inferred — no annotation needed.
883
+ *
884
+ * Sub-pipelines support all stage methods including `.raw()` for operators not
885
+ * yet first-class. Execution methods (`toArray`, `explain`) are not available
886
+ * inside branches.
887
+ *
888
+ * @param spec - An object mapping branch names to sub-pipeline builder callbacks.
889
+ * @returns A new pipeline whose output is one document with each branch name mapped to an array of results.
890
+ *
891
+ * @example
892
+ * ```ts
893
+ * const [report] = await aggregate(orders)
894
+ * .facet({
895
+ * byCategory: (sub) => sub
896
+ * .groupBy('category', acc => ({ count: acc.count() }))
897
+ * .sort({ count: -1 }),
898
+ * totals: (sub) => sub
899
+ * .groupBy(null, acc => ({ grandTotal: acc.sum('amount') })),
900
+ * })
901
+ * .toArray()
902
+ * // report.byCategory → { _id: 'electronics' | 'books' | 'clothing'; count: number }[]
903
+ * // report.totals → { _id: null; grandTotal: number }[]
904
+ * ```
905
+ */
906
+ facet(spec) {
907
+ const branches = {};
908
+ for (const [key, cb] of Object.entries(spec)) {
909
+ const sub = new _AggregatePipeline(
910
+ this.definition,
911
+ this.nativeCollection,
912
+ [],
913
+ this.session
914
+ // 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
915
+ );
916
+ branches[key] = cb(sub).getStages();
917
+ }
918
+ const pipeline = new _AggregatePipeline(
919
+ this.definition,
920
+ this.nativeCollection,
921
+ [...this.stages, { $facet: branches }],
922
+ this.session
923
+ );
770
924
  return pipeline;
771
925
  }
772
926
  // ── Convenience shortcuts ────────────────────────────────────────
@@ -787,11 +941,16 @@ var AggregatePipeline = class _AggregatePipeline {
787
941
  * ```
788
942
  */
789
943
  countBy(field) {
790
- const pipeline = new _AggregatePipeline(this.definition, this.nativeCollection, [
791
- ...this.stages,
792
- { $group: { _id: `$${field}`, count: { $sum: 1 } } },
793
- { $sort: { count: -1 } }
794
- ]);
944
+ const pipeline = new _AggregatePipeline(
945
+ this.definition,
946
+ this.nativeCollection,
947
+ [
948
+ ...this.stages,
949
+ { $group: { _id: `$${field}`, count: { $sum: 1 } } },
950
+ { $sort: { count: -1 } }
951
+ ],
952
+ this.session
953
+ );
795
954
  return pipeline;
796
955
  }
797
956
  /**
@@ -812,11 +971,16 @@ var AggregatePipeline = class _AggregatePipeline {
812
971
  * ```
813
972
  */
814
973
  sumBy(field, sumField) {
815
- const pipeline = new _AggregatePipeline(this.definition, this.nativeCollection, [
816
- ...this.stages,
817
- { $group: { _id: `$${field}`, total: { $sum: `$${sumField}` } } },
818
- { $sort: { total: -1 } }
819
- ]);
974
+ const pipeline = new _AggregatePipeline(
975
+ this.definition,
976
+ this.nativeCollection,
977
+ [
978
+ ...this.stages,
979
+ { $group: { _id: `$${field}`, total: { $sum: `$${sumField}` } } },
980
+ { $sort: { total: -1 } }
981
+ ],
982
+ this.session
983
+ );
820
984
  return pipeline;
821
985
  }
822
986
  /**
@@ -836,10 +1000,12 @@ var AggregatePipeline = class _AggregatePipeline {
836
1000
  * ```
837
1001
  */
838
1002
  sortBy(field, direction = "asc") {
839
- return new _AggregatePipeline(this.definition, this.nativeCollection, [
840
- ...this.stages,
841
- { $sort: { [field]: direction === "desc" ? -1 : 1 } }
842
- ]);
1003
+ return new _AggregatePipeline(
1004
+ this.definition,
1005
+ this.nativeCollection,
1006
+ [...this.stages, { $sort: { [field]: direction === "desc" ? -1 : 1 } }],
1007
+ this.session
1008
+ );
843
1009
  }
844
1010
  /**
845
1011
  * Return the top N documents sorted by a field descending.
@@ -858,11 +1024,12 @@ var AggregatePipeline = class _AggregatePipeline {
858
1024
  * ```
859
1025
  */
860
1026
  top(n, options) {
861
- return new _AggregatePipeline(this.definition, this.nativeCollection, [
862
- ...this.stages,
863
- { $sort: { [options.by]: -1 } },
864
- { $limit: n }
865
- ]);
1027
+ return new _AggregatePipeline(
1028
+ this.definition,
1029
+ this.nativeCollection,
1030
+ [...this.stages, { $sort: { [options.by]: -1 } }, { $limit: n }],
1031
+ this.session
1032
+ );
866
1033
  }
867
1034
  /**
868
1035
  * Return the bottom N documents sorted by a field ascending.
@@ -881,15 +1048,25 @@ var AggregatePipeline = class _AggregatePipeline {
881
1048
  * ```
882
1049
  */
883
1050
  bottom(n, options) {
884
- return new _AggregatePipeline(this.definition, this.nativeCollection, [
885
- ...this.stages,
886
- { $sort: { [options.by]: 1 } },
887
- { $limit: n }
888
- ]);
1051
+ return new _AggregatePipeline(
1052
+ this.definition,
1053
+ this.nativeCollection,
1054
+ [...this.stages, { $sort: { [options.by]: 1 } }, { $limit: n }],
1055
+ this.session
1056
+ );
1057
+ }
1058
+ /** @internal Used by facet() to extract branch stages. Not part of the public API. */
1059
+ getStages() {
1060
+ return this.stages;
889
1061
  }
890
1062
  };
891
1063
  function aggregate(handle) {
892
- return new AggregatePipeline(handle.definition, handle.native, []);
1064
+ return new AggregatePipeline(
1065
+ handle.definition,
1066
+ handle.native,
1067
+ [],
1068
+ handle.session
1069
+ );
893
1070
  }
894
1071
 
895
1072
  // src/client/client.ts
@@ -1071,6 +1248,36 @@ async function syncIndexes(handle, options) {
1071
1248
  };
1072
1249
  }
1073
1250
 
1251
+ // src/transaction/transaction.ts
1252
+ var TransactionContext = class {
1253
+ /** @internal */
1254
+ session;
1255
+ /** @internal */
1256
+ constructor(session) {
1257
+ this.session = session;
1258
+ }
1259
+ /**
1260
+ * Bind a collection handle to this transaction's session.
1261
+ *
1262
+ * Returns a cloned handle whose CRUD operations automatically include
1263
+ * the transaction session. The original handle is not modified.
1264
+ *
1265
+ * @param handle - An existing collection handle from `db.use()`.
1266
+ * @returns A new handle bound to the transaction session.
1267
+ *
1268
+ * @example
1269
+ * ```ts
1270
+ * await db.transaction(async (tx) => {
1271
+ * const txUsers = tx.use(users)
1272
+ * await txUsers.insertOne({ name: 'Ada' })
1273
+ * })
1274
+ * ```
1275
+ */
1276
+ use(handle) {
1277
+ return handle.withSession(this.session);
1278
+ }
1279
+ };
1280
+
1074
1281
  // src/crud/delete.ts
1075
1282
  import { z as z2 } from "zod";
1076
1283
 
@@ -1095,14 +1302,22 @@ var ZodmonValidationError = class extends ZodmonError {
1095
1302
  // src/crud/delete.ts
1096
1303
  async function deleteOne(handle, filter) {
1097
1304
  try {
1098
- return await handle.native.deleteOne(filter);
1305
+ return await handle.native.deleteOne(
1306
+ // biome-ignore lint/suspicious/noExplicitAny: TypedFilter intersection type is not directly assignable to MongoDB's Filter
1307
+ filter,
1308
+ handle.session ? { session: handle.session } : {}
1309
+ );
1099
1310
  } catch (err) {
1100
1311
  wrapMongoError(err, handle.definition.name);
1101
1312
  }
1102
1313
  }
1103
1314
  async function deleteMany(handle, filter) {
1104
1315
  try {
1105
- return await handle.native.deleteMany(filter);
1316
+ return await handle.native.deleteMany(
1317
+ // biome-ignore lint/suspicious/noExplicitAny: TypedFilter intersection type is not directly assignable to MongoDB's Filter
1318
+ filter,
1319
+ handle.session ? { session: handle.session } : {}
1320
+ );
1106
1321
  } catch (err) {
1107
1322
  wrapMongoError(err, handle.definition.name);
1108
1323
  }
@@ -1113,7 +1328,7 @@ async function findOneAndDelete(handle, filter, options) {
1113
1328
  result = await handle.native.findOneAndDelete(
1114
1329
  // biome-ignore lint/suspicious/noExplicitAny: TypedFilter intersection type is not directly assignable to MongoDB's Filter
1115
1330
  filter,
1116
- { includeResultMetadata: false }
1331
+ handle.session ? { includeResultMetadata: false, session: handle.session } : { includeResultMetadata: false }
1117
1332
  );
1118
1333
  } catch (err) {
1119
1334
  wrapMongoError(err, handle.definition.name);
@@ -1124,7 +1339,8 @@ async function findOneAndDelete(handle, filter, options) {
1124
1339
  return result;
1125
1340
  }
1126
1341
  try {
1127
- return handle.definition.schema.parse(result);
1342
+ const schema = mode === "strict" ? handle.definition.strictSchema : handle.definition.schema;
1343
+ return schema.parse(result);
1128
1344
  } catch (err) {
1129
1345
  if (err instanceof z2.ZodError) {
1130
1346
  throw new ZodmonValidationError(handle.definition.name, err, result);
@@ -1134,7 +1350,7 @@ async function findOneAndDelete(handle, filter, options) {
1134
1350
  }
1135
1351
 
1136
1352
  // src/crud/find.ts
1137
- import { z as z4 } from "zod";
1353
+ import { z as z5 } from "zod";
1138
1354
 
1139
1355
  // src/errors/not-found.ts
1140
1356
  var ZodmonNotFoundError = class extends ZodmonError {
@@ -1171,7 +1387,7 @@ function checkUnindexedFields(definition, filter) {
1171
1387
  }
1172
1388
 
1173
1389
  // src/query/cursor.ts
1174
- import { z as z3 } from "zod";
1390
+ import { z as z4 } from "zod";
1175
1391
 
1176
1392
  // src/crud/paginate.ts
1177
1393
  import { ObjectId } from "mongodb";
@@ -1234,6 +1450,258 @@ function resolveSortKeys(sortSpec) {
1234
1450
  return entries;
1235
1451
  }
1236
1452
 
1453
+ // src/populate/builder.ts
1454
+ var PopulateRefBuilder = class {
1455
+ /**
1456
+ * Declare a projection to apply when fetching the referenced documents.
1457
+ *
1458
+ * Supported: inclusion (`{ name: 1 }`), exclusion (`{ email: 0 }`), or
1459
+ * `_id` suppression (`{ name: 1, _id: 0 }`).
1460
+ *
1461
+ * @param projection - MongoDB-style inclusion or exclusion projection.
1462
+ * @returns A config object carrying the projection type for compile-time narrowing.
1463
+ *
1464
+ * @example
1465
+ * ```ts
1466
+ * (b) => b.project({ name: 1, email: 1 })
1467
+ * (b) => b.project({ password: 0 })
1468
+ * ```
1469
+ */
1470
+ project(projection) {
1471
+ return { projection };
1472
+ }
1473
+ };
1474
+
1475
+ // src/populate/execute.ts
1476
+ import { z as z3 } from "zod";
1477
+ function unwrapRefSchema(schema) {
1478
+ const def = schema._zod.def;
1479
+ if (def && typeof def === "object") {
1480
+ if ("innerType" in def && def.innerType instanceof z3.ZodType) {
1481
+ return unwrapRefSchema(def.innerType);
1482
+ }
1483
+ if ("element" in def && def.element instanceof z3.ZodType) {
1484
+ return unwrapRefSchema(def.element);
1485
+ }
1486
+ }
1487
+ return schema;
1488
+ }
1489
+ function resolveRefField(shape, fieldName, collectionName) {
1490
+ const fieldSchema = shape[fieldName];
1491
+ if (!fieldSchema) {
1492
+ throw new Error(
1493
+ `[zodmon] populate: field '${fieldName}' does not exist on collection '${collectionName}'.`
1494
+ );
1495
+ }
1496
+ const isArray = fieldSchema instanceof z3.ZodArray;
1497
+ const inner = unwrapRefSchema(fieldSchema);
1498
+ const ref = getRefMetadata(inner);
1499
+ if (!ref) {
1500
+ throw new Error(
1501
+ `[zodmon] populate: field '${fieldName}' has no .ref() metadata. Only fields declared with .ref(Collection) can be populated.`
1502
+ );
1503
+ }
1504
+ return { isArray, ref };
1505
+ }
1506
+ function resolvePopulateStep(definition, previousSteps, path, as, projection) {
1507
+ const dotIndex = path.indexOf(".");
1508
+ if (dotIndex === -1) {
1509
+ const shape = definition.shape;
1510
+ const { isArray: isArray2, ref: ref2 } = resolveRefField(shape, path, definition.name);
1511
+ return {
1512
+ originalPath: path,
1513
+ leafField: path,
1514
+ as,
1515
+ parentOutputPath: void 0,
1516
+ targetCollection: ref2.collection,
1517
+ isArray: isArray2,
1518
+ ...projection !== void 0 ? { projection } : {}
1519
+ };
1520
+ }
1521
+ const parentPath = path.slice(0, dotIndex);
1522
+ const leafField = path.slice(dotIndex + 1);
1523
+ const parentStep = previousSteps.find((s) => s.as === parentPath);
1524
+ if (!parentStep) {
1525
+ throw new Error(
1526
+ `[zodmon] populate: parent '${parentPath}' has not been populated. Populate '${parentPath}' before populating '${path}'.`
1527
+ );
1528
+ }
1529
+ const parentShape = parentStep.targetCollection.shape;
1530
+ const { isArray, ref } = resolveRefField(parentShape, leafField, parentStep.targetCollection.name);
1531
+ return {
1532
+ originalPath: path,
1533
+ leafField,
1534
+ as,
1535
+ parentOutputPath: parentPath,
1536
+ targetCollection: ref.collection,
1537
+ isArray,
1538
+ ...projection !== void 0 ? { projection } : {}
1539
+ };
1540
+ }
1541
+ function expandValue(value) {
1542
+ if (value == null) return [];
1543
+ if (Array.isArray(value)) {
1544
+ const result = [];
1545
+ for (const item of value) {
1546
+ if (item != null && typeof item === "object") {
1547
+ result.push(item);
1548
+ }
1549
+ }
1550
+ return result;
1551
+ }
1552
+ if (typeof value === "object") {
1553
+ return [value];
1554
+ }
1555
+ return [];
1556
+ }
1557
+ function getNestedTargets(doc, path) {
1558
+ const parts = path.split(".");
1559
+ let targets = [doc];
1560
+ for (const part of parts) {
1561
+ targets = targets.flatMap((target) => expandValue(target[part]));
1562
+ }
1563
+ return targets;
1564
+ }
1565
+ function addUniqueId(value, idSet, idValues) {
1566
+ const key = String(value);
1567
+ if (!idSet.has(key)) {
1568
+ idSet.add(key);
1569
+ idValues.push(value);
1570
+ }
1571
+ }
1572
+ function collectIds(targets, leafField) {
1573
+ const idSet = /* @__PURE__ */ new Set();
1574
+ const idValues = [];
1575
+ for (const target of targets) {
1576
+ const value = target[leafField];
1577
+ if (value == null) continue;
1578
+ if (Array.isArray(value)) {
1579
+ for (const id of value) {
1580
+ addUniqueId(id, idSet, idValues);
1581
+ }
1582
+ } else {
1583
+ addUniqueId(value, idSet, idValues);
1584
+ }
1585
+ }
1586
+ return idValues;
1587
+ }
1588
+ function resolvePopulatedValue(value, map) {
1589
+ if (value == null) return value;
1590
+ if (Array.isArray(value)) {
1591
+ return value.map((id) => map.get(String(id))).filter((d) => d != null);
1592
+ }
1593
+ return map.get(String(value)) ?? null;
1594
+ }
1595
+ function mergePopulated(targets, step, map) {
1596
+ for (const target of targets) {
1597
+ const value = target[step.leafField];
1598
+ const populated = resolvePopulatedValue(value, map);
1599
+ if (step.as !== step.leafField) {
1600
+ delete target[step.leafField];
1601
+ }
1602
+ target[step.as] = populated;
1603
+ }
1604
+ }
1605
+ async function executePopulate(documents, steps, getCollection) {
1606
+ for (const step of steps) {
1607
+ const targets = step.parentOutputPath ? documents.flatMap((doc) => getNestedTargets(doc, step.parentOutputPath)) : documents;
1608
+ const idValues = collectIds(targets, step.leafField);
1609
+ if (idValues.length === 0) continue;
1610
+ const col = getCollection(step.targetCollection.name);
1611
+ const findOptions = step.projection !== void 0 ? { projection: step.projection } : {};
1612
+ const fetched = await col.find({ _id: { $in: idValues } }, findOptions).toArray();
1613
+ const map = /* @__PURE__ */ new Map();
1614
+ for (const doc of fetched) {
1615
+ map.set(String(doc._id), doc);
1616
+ }
1617
+ mergePopulated(targets, step, map);
1618
+ }
1619
+ return documents;
1620
+ }
1621
+
1622
+ // src/populate/cursor.ts
1623
+ var PopulateCursor = class _PopulateCursor {
1624
+ cursor;
1625
+ definition;
1626
+ steps;
1627
+ nativeCollection;
1628
+ /** @internal */
1629
+ constructor(cursor, definition, steps, nativeCollection) {
1630
+ this.cursor = cursor;
1631
+ this.definition = definition;
1632
+ this.steps = steps;
1633
+ this.nativeCollection = nativeCollection;
1634
+ }
1635
+ // Implementation -- TypeScript cannot narrow overloaded generics in the
1636
+ // implementation body, so param types are widened and the return is cast.
1637
+ populate(field, asOrConfigure) {
1638
+ let alias;
1639
+ let projection;
1640
+ if (typeof asOrConfigure === "function") {
1641
+ alias = field.includes(".") ? field.split(".").pop() ?? field : field;
1642
+ const config = asOrConfigure(new PopulateRefBuilder());
1643
+ projection = config.projection;
1644
+ } else {
1645
+ alias = asOrConfigure ?? (field.includes(".") ? field.split(".").pop() ?? field : field);
1646
+ projection = void 0;
1647
+ }
1648
+ const step = resolvePopulateStep(this.definition, this.steps, field, alias, projection);
1649
+ const newSteps = [...this.steps, step];
1650
+ return new _PopulateCursor(this.cursor, this.definition, newSteps, this.nativeCollection);
1651
+ }
1652
+ /**
1653
+ * Execute the query and return all matching documents as a populated array.
1654
+ *
1655
+ * Fetches all documents from the underlying cursor, then applies populate
1656
+ * steps in order using batch `$in` queries (no N+1 problem).
1657
+ *
1658
+ * @returns Array of populated documents.
1659
+ *
1660
+ * @example
1661
+ * ```ts
1662
+ * const posts = await db.use(Posts)
1663
+ * .find({})
1664
+ * .populate('authorId', 'author')
1665
+ * .toArray()
1666
+ * ```
1667
+ */
1668
+ async toArray() {
1669
+ const docs = await this.cursor.toArray();
1670
+ if (this.steps.length === 0) return docs;
1671
+ const populated = await executePopulate(
1672
+ docs,
1673
+ this.steps,
1674
+ (name) => this.nativeCollection.db.collection(name)
1675
+ );
1676
+ return populated;
1677
+ }
1678
+ /**
1679
+ * Async iterator for streaming populated documents.
1680
+ *
1681
+ * Fetches all documents first (populate requires the full batch for
1682
+ * efficient `$in` queries), then yields results one at a time.
1683
+ *
1684
+ * @yields Populated documents one at a time.
1685
+ *
1686
+ * @example
1687
+ * ```ts
1688
+ * for await (const post of db.use(Posts).find({}).populate('authorId', 'author')) {
1689
+ * console.log(post.author.name)
1690
+ * }
1691
+ * ```
1692
+ */
1693
+ async *[Symbol.asyncIterator]() {
1694
+ const results = await this.toArray();
1695
+ for (const doc of results) {
1696
+ yield doc;
1697
+ }
1698
+ }
1699
+ };
1700
+ function createPopulateCursor(cursor, definition, steps) {
1701
+ const nativeCollection = cursor.nativeCollection;
1702
+ return new PopulateCursor(cursor, definition, steps, nativeCollection);
1703
+ }
1704
+
1237
1705
  // src/query/projection.ts
1238
1706
  function isIncludeValue(value) {
1239
1707
  return value === 1 || value === true;
@@ -1287,6 +1755,8 @@ var TypedFindCursor = class {
1287
1755
  /** @internal */
1288
1756
  cursor;
1289
1757
  /** @internal */
1758
+ definition;
1759
+ /** @internal */
1290
1760
  schema;
1291
1761
  /** @internal */
1292
1762
  collectionName;
@@ -1298,17 +1768,21 @@ var TypedFindCursor = class {
1298
1768
  // biome-ignore lint/suspicious/noExplicitAny: TypedFilter is not assignable to MongoDB's Filter; stored opaquely for paginate
1299
1769
  filter;
1300
1770
  /** @internal */
1771
+ session;
1772
+ /** @internal */
1301
1773
  sortSpec;
1302
1774
  /** @internal */
1303
1775
  projectedSchema;
1304
1776
  /** @internal */
1305
- constructor(cursor, definition, mode, nativeCollection, filter) {
1777
+ constructor(cursor, definition, mode, nativeCollection, filter, session) {
1306
1778
  this.cursor = cursor;
1779
+ this.definition = definition;
1307
1780
  this.schema = definition.schema;
1308
1781
  this.collectionName = definition.name;
1309
1782
  this.mode = mode;
1310
1783
  this.nativeCollection = nativeCollection;
1311
1784
  this.filter = filter;
1785
+ this.session = session;
1312
1786
  this.sortSpec = null;
1313
1787
  this.projectedSchema = null;
1314
1788
  }
@@ -1418,6 +1892,14 @@ var TypedFindCursor = class {
1418
1892
  );
1419
1893
  return this;
1420
1894
  }
1895
+ // Implementation — creates a PopulateCursor and delegates the first populate call.
1896
+ // No circular runtime dependency: populate/cursor.ts imports TypedFindCursor as a
1897
+ // *type* only (erased at runtime), so the runtime import flows one way:
1898
+ // query/cursor.ts → populate/cursor.ts.
1899
+ populate(field, asOrConfigure) {
1900
+ const popCursor = createPopulateCursor(this, this.definition, []);
1901
+ return popCursor.populate(field, asOrConfigure);
1902
+ }
1421
1903
  async paginate(opts) {
1422
1904
  const sortRecord = this.sortSpec ? this.sortSpec : null;
1423
1905
  const sortKeys2 = resolveSortKeys(sortRecord);
@@ -1434,8 +1916,11 @@ var TypedFindCursor = class {
1434
1916
  try {
1435
1917
  ;
1436
1918
  [total, raw2] = await Promise.all([
1437
- this.nativeCollection.countDocuments(this.filter),
1438
- this.nativeCollection.find(this.filter).sort(sort).skip((opts.page - 1) * opts.perPage).limit(opts.perPage).toArray()
1919
+ this.nativeCollection.countDocuments(
1920
+ this.filter,
1921
+ this.session ? { session: this.session } : {}
1922
+ ),
1923
+ this.nativeCollection.find(this.filter, this.session ? { session: this.session } : void 0).sort(sort).skip((opts.page - 1) * opts.perPage).limit(opts.perPage).toArray()
1439
1924
  ]);
1440
1925
  } catch (err) {
1441
1926
  wrapMongoError(err, this.collectionName);
@@ -1465,7 +1950,7 @@ var TypedFindCursor = class {
1465
1950
  const effectiveSort = isBackward ? Object.fromEntries(sortKeys2.map(([f, d]) => [f, d === 1 ? -1 : 1])) : sort;
1466
1951
  let raw2;
1467
1952
  try {
1468
- raw2 = await this.nativeCollection.find(combinedFilter).sort(effectiveSort).limit(opts.limit + 1).toArray();
1953
+ raw2 = await this.nativeCollection.find(combinedFilter, this.session ? { session: this.session } : void 0).sort(effectiveSort).limit(opts.limit + 1).toArray();
1469
1954
  } catch (err) {
1470
1955
  wrapMongoError(err, this.collectionName);
1471
1956
  }
@@ -1534,11 +2019,11 @@ var TypedFindCursor = class {
1534
2019
  if (this.mode === false || this.mode === "passthrough") {
1535
2020
  return raw2;
1536
2021
  }
1537
- const schema = this.projectedSchema ?? this.schema;
2022
+ const schema = this.projectedSchema ?? (this.mode === "strict" ? this.definition.strictSchema : this.schema);
1538
2023
  try {
1539
2024
  return schema.parse(raw2);
1540
2025
  } catch (err) {
1541
- if (err instanceof z3.ZodError) {
2026
+ if (err instanceof z4.ZodError) {
1542
2027
  throw new ZodmonValidationError(this.collectionName, err, raw2);
1543
2028
  }
1544
2029
  throw err;
@@ -1553,7 +2038,11 @@ async function findOne(handle, filter, options) {
1553
2038
  const findOptions = project ? { projection: project } : void 0;
1554
2039
  let raw2;
1555
2040
  try {
1556
- raw2 = await handle.native.findOne(filter, findOptions);
2041
+ raw2 = await handle.native.findOne(
2042
+ // biome-ignore lint/suspicious/noExplicitAny: TypedFilter intersection type is not directly assignable to MongoDB's Filter
2043
+ filter,
2044
+ handle.session ? { ...findOptions, session: handle.session } : findOptions
2045
+ );
1557
2046
  } catch (err) {
1558
2047
  wrapMongoError(err, handle.definition.name);
1559
2048
  }
@@ -1565,11 +2054,11 @@ async function findOne(handle, filter, options) {
1565
2054
  const schema = project ? deriveProjectedSchema(
1566
2055
  handle.definition.schema,
1567
2056
  project
1568
- ) : handle.definition.schema;
2057
+ ) : mode === "strict" ? handle.definition.strictSchema : handle.definition.schema;
1569
2058
  try {
1570
2059
  return schema.parse(raw2);
1571
2060
  } catch (err) {
1572
- if (err instanceof z4.ZodError) {
2061
+ if (err instanceof z5.ZodError) {
1573
2062
  throw new ZodmonValidationError(handle.definition.name, err, raw2);
1574
2063
  }
1575
2064
  throw err;
@@ -1584,10 +2073,21 @@ async function findOneOrThrow(handle, filter, options) {
1584
2073
  }
1585
2074
  function find(handle, filter, options) {
1586
2075
  checkUnindexedFields(handle.definition, filter);
1587
- const raw2 = handle.native.find(filter);
2076
+ const raw2 = handle.native.find(
2077
+ // biome-ignore lint/suspicious/noExplicitAny: TypedFilter intersection type is not directly assignable to MongoDB's Filter
2078
+ filter,
2079
+ handle.session ? { session: handle.session } : void 0
2080
+ );
1588
2081
  const cursor = raw2;
1589
2082
  const mode = options?.validate !== void 0 ? options.validate : handle.definition.options.validation;
1590
- const typedCursor = new TypedFindCursor(cursor, handle.definition, mode, handle.native, filter);
2083
+ const typedCursor = new TypedFindCursor(
2084
+ cursor,
2085
+ handle.definition,
2086
+ mode,
2087
+ handle.native,
2088
+ filter,
2089
+ handle.session
2090
+ );
1591
2091
  const project = options && "project" in options ? options.project : void 0;
1592
2092
  if (project) {
1593
2093
  return typedCursor.project(project);
@@ -1596,19 +2096,19 @@ function find(handle, filter, options) {
1596
2096
  }
1597
2097
 
1598
2098
  // src/crud/insert.ts
1599
- import { z as z5 } from "zod";
2099
+ import { z as z6 } from "zod";
1600
2100
  async function insertOne(handle, doc) {
1601
2101
  let parsed;
1602
2102
  try {
1603
2103
  parsed = handle.definition.schema.parse(doc);
1604
2104
  } catch (err) {
1605
- if (err instanceof z5.ZodError) {
2105
+ if (err instanceof z6.ZodError) {
1606
2106
  throw new ZodmonValidationError(handle.definition.name, err, doc);
1607
2107
  }
1608
2108
  throw err;
1609
2109
  }
1610
2110
  try {
1611
- await handle.native.insertOne(parsed);
2111
+ await handle.native.insertOne(parsed, handle.session ? { session: handle.session } : {});
1612
2112
  } catch (err) {
1613
2113
  wrapMongoError(err, handle.definition.name);
1614
2114
  }
@@ -1621,14 +2121,14 @@ async function insertMany(handle, docs) {
1621
2121
  try {
1622
2122
  parsed.push(handle.definition.schema.parse(doc));
1623
2123
  } catch (err) {
1624
- if (err instanceof z5.ZodError) {
2124
+ if (err instanceof z6.ZodError) {
1625
2125
  throw new ZodmonValidationError(handle.definition.name, err, doc);
1626
2126
  }
1627
2127
  throw err;
1628
2128
  }
1629
2129
  }
1630
2130
  try {
1631
- await handle.native.insertMany(parsed);
2131
+ await handle.native.insertMany(parsed, handle.session ? { session: handle.session } : {});
1632
2132
  } catch (err) {
1633
2133
  wrapMongoError(err, handle.definition.name);
1634
2134
  }
@@ -1636,17 +2136,29 @@ async function insertMany(handle, docs) {
1636
2136
  }
1637
2137
 
1638
2138
  // src/crud/update.ts
1639
- import { z as z6 } from "zod";
2139
+ import { z as z7 } from "zod";
1640
2140
  async function updateOne(handle, filter, update, options) {
1641
2141
  try {
1642
- return await handle.native.updateOne(filter, update, options);
2142
+ return await handle.native.updateOne(
2143
+ // biome-ignore lint/suspicious/noExplicitAny: TypedFilter intersection type is not directly assignable to MongoDB's Filter
2144
+ filter,
2145
+ // biome-ignore lint/suspicious/noExplicitAny: TypedUpdateFilter intersection type is not directly assignable to MongoDB's UpdateFilter
2146
+ update,
2147
+ handle.session ? { ...options, session: handle.session } : options
2148
+ );
1643
2149
  } catch (err) {
1644
2150
  wrapMongoError(err, handle.definition.name);
1645
2151
  }
1646
2152
  }
1647
2153
  async function updateMany(handle, filter, update, options) {
1648
2154
  try {
1649
- return await handle.native.updateMany(filter, update, options);
2155
+ return await handle.native.updateMany(
2156
+ // biome-ignore lint/suspicious/noExplicitAny: TypedFilter intersection type is not directly assignable to MongoDB's Filter
2157
+ filter,
2158
+ // biome-ignore lint/suspicious/noExplicitAny: TypedUpdateFilter intersection type is not directly assignable to MongoDB's UpdateFilter
2159
+ update,
2160
+ handle.session ? { ...options, session: handle.session } : options
2161
+ );
1650
2162
  } catch (err) {
1651
2163
  wrapMongoError(err, handle.definition.name);
1652
2164
  }
@@ -1659,6 +2171,9 @@ async function findOneAndUpdate(handle, filter, update, options) {
1659
2171
  if (options?.upsert !== void 0) {
1660
2172
  driverOptions["upsert"] = options.upsert;
1661
2173
  }
2174
+ if (handle.session) {
2175
+ driverOptions["session"] = handle.session;
2176
+ }
1662
2177
  let result;
1663
2178
  try {
1664
2179
  result = await handle.native.findOneAndUpdate(
@@ -1678,24 +2193,165 @@ async function findOneAndUpdate(handle, filter, update, options) {
1678
2193
  return result;
1679
2194
  }
1680
2195
  try {
1681
- return handle.definition.schema.parse(result);
2196
+ const schema = mode === "strict" ? handle.definition.strictSchema : handle.definition.schema;
2197
+ return schema.parse(result);
1682
2198
  } catch (err) {
1683
- if (err instanceof z6.ZodError) {
2199
+ if (err instanceof z7.ZodError) {
1684
2200
  throw new ZodmonValidationError(handle.definition.name, err, result);
1685
2201
  }
1686
2202
  throw err;
1687
2203
  }
1688
2204
  }
1689
2205
 
2206
+ // src/populate/query.ts
2207
+ var PopulateOneQuery = class _PopulateOneQuery {
2208
+ handle;
2209
+ filter;
2210
+ options;
2211
+ steps;
2212
+ constructor(handle, filter, options, steps = []) {
2213
+ this.handle = handle;
2214
+ this.filter = filter;
2215
+ this.options = options;
2216
+ this.steps = steps;
2217
+ }
2218
+ // Implementation — TypeScript cannot narrow overloaded generics in the
2219
+ // implementation body, so param types are widened and the return is cast.
2220
+ populate(field, asOrConfigure) {
2221
+ let alias;
2222
+ let projection;
2223
+ if (typeof asOrConfigure === "function") {
2224
+ alias = field.includes(".") ? field.split(".").pop() ?? field : field;
2225
+ const config = asOrConfigure(new PopulateRefBuilder());
2226
+ projection = config.projection;
2227
+ } else {
2228
+ alias = asOrConfigure ?? (field.includes(".") ? field.split(".").pop() ?? field : field);
2229
+ projection = void 0;
2230
+ }
2231
+ const step = resolvePopulateStep(this.handle.definition, this.steps, field, alias, projection);
2232
+ const newSteps = [...this.steps, step];
2233
+ return new _PopulateOneQuery(this.handle, this.filter, this.options, newSteps);
2234
+ }
2235
+ /**
2236
+ * Attach fulfillment and rejection handlers to the query promise.
2237
+ *
2238
+ * Executes the base findOne query and applies populate steps if any.
2239
+ * Returns `null` when no document matches the filter.
2240
+ */
2241
+ // biome-ignore lint/suspicious/noThenProperty: PromiseLike requires a then method
2242
+ then(onfulfilled, onrejected) {
2243
+ const promise = this.execute();
2244
+ return promise.then(onfulfilled, onrejected);
2245
+ }
2246
+ async execute() {
2247
+ const doc = await findOne(this.handle, this.filter, this.options);
2248
+ if (!doc) return null;
2249
+ if (this.steps.length === 0) return doc;
2250
+ const populated = await executePopulate(
2251
+ [doc],
2252
+ this.steps,
2253
+ (name) => this.handle.native.db.collection(name)
2254
+ );
2255
+ return populated[0] ?? null;
2256
+ }
2257
+ };
2258
+ var PopulateOneOrThrowQuery = class _PopulateOneOrThrowQuery {
2259
+ handle;
2260
+ filter;
2261
+ options;
2262
+ steps;
2263
+ constructor(handle, filter, options, steps = []) {
2264
+ this.handle = handle;
2265
+ this.filter = filter;
2266
+ this.options = options;
2267
+ this.steps = steps;
2268
+ }
2269
+ // Implementation — see PopulateOneQuery for reasoning on casts.
2270
+ populate(field, asOrConfigure) {
2271
+ let alias;
2272
+ let projection;
2273
+ if (typeof asOrConfigure === "function") {
2274
+ alias = field.includes(".") ? field.split(".").pop() ?? field : field;
2275
+ const config = asOrConfigure(new PopulateRefBuilder());
2276
+ projection = config.projection;
2277
+ } else {
2278
+ alias = asOrConfigure ?? (field.includes(".") ? field.split(".").pop() ?? field : field);
2279
+ projection = void 0;
2280
+ }
2281
+ const step = resolvePopulateStep(this.handle.definition, this.steps, field, alias, projection);
2282
+ const newSteps = [...this.steps, step];
2283
+ return new _PopulateOneOrThrowQuery(this.handle, this.filter, this.options, newSteps);
2284
+ }
2285
+ /**
2286
+ * Attach fulfillment and rejection handlers to the query promise.
2287
+ *
2288
+ * Executes the base findOneOrThrow query and applies populate steps if any.
2289
+ * Throws {@link ZodmonNotFoundError} when no document matches.
2290
+ */
2291
+ // biome-ignore lint/suspicious/noThenProperty: PromiseLike requires a then method
2292
+ then(onfulfilled, onrejected) {
2293
+ const promise = this.execute();
2294
+ return promise.then(onfulfilled, onrejected);
2295
+ }
2296
+ async execute() {
2297
+ const doc = await findOne(this.handle, this.filter, this.options);
2298
+ if (!doc) {
2299
+ throw new ZodmonNotFoundError(this.handle.definition.name, this.filter);
2300
+ }
2301
+ if (this.steps.length === 0) return doc;
2302
+ const populated = await executePopulate(
2303
+ [doc],
2304
+ this.steps,
2305
+ (name) => this.handle.native.db.collection(name)
2306
+ );
2307
+ const result = populated[0];
2308
+ if (!result) {
2309
+ throw new ZodmonNotFoundError(this.handle.definition.name, this.filter);
2310
+ }
2311
+ return result;
2312
+ }
2313
+ };
2314
+
1690
2315
  // src/client/handle.ts
1691
- var CollectionHandle = class {
2316
+ var CollectionHandle = class _CollectionHandle {
1692
2317
  /** The collection definition containing schema, name, and index metadata. */
1693
2318
  definition;
1694
2319
  /** The underlying MongoDB driver collection, typed to the inferred document type. */
1695
2320
  native;
1696
- constructor(definition, native) {
2321
+ /**
2322
+ * The MongoDB client session bound to this handle, if any.
2323
+ *
2324
+ * When set, all CRUD and aggregation operations performed through this
2325
+ * handle will include the session in their options, enabling transactional
2326
+ * reads and writes. Undefined when no session is bound.
2327
+ */
2328
+ session;
2329
+ constructor(definition, native, session) {
1697
2330
  this.definition = definition;
1698
2331
  this.native = native;
2332
+ this.session = session;
2333
+ }
2334
+ /**
2335
+ * Create a new handle bound to the given MongoDB client session.
2336
+ *
2337
+ * Returns a new {@link CollectionHandle} that shares the same collection
2338
+ * definition and native driver collection, but passes `session` to every
2339
+ * CRUD and aggregation operation. The original handle is not modified.
2340
+ *
2341
+ * @param session - The MongoDB `ClientSession` to bind.
2342
+ * @returns A new handle with the session attached.
2343
+ *
2344
+ * @example
2345
+ * ```ts
2346
+ * const users = db.use(Users)
2347
+ * await db.client.withSession(async (session) => {
2348
+ * const bound = users.withSession(session)
2349
+ * await bound.insertOne({ name: 'Ada' }) // uses session
2350
+ * })
2351
+ * ```
2352
+ */
2353
+ withSession(session) {
2354
+ return new _CollectionHandle(this.definition, this.native, session);
1699
2355
  }
1700
2356
  /**
1701
2357
  * Insert a single document into the collection.
@@ -1741,11 +2397,17 @@ var CollectionHandle = class {
1741
2397
  async insertMany(docs) {
1742
2398
  return await insertMany(this, docs);
1743
2399
  }
1744
- async findOne(filter, options) {
1745
- return await findOne(this, filter, options);
2400
+ findOne(filter, options) {
2401
+ if (options && "project" in options) {
2402
+ return findOne(this, filter, options);
2403
+ }
2404
+ return new PopulateOneQuery(this, filter, options);
1746
2405
  }
1747
- async findOneOrThrow(filter, options) {
1748
- return await findOneOrThrow(this, filter, options);
2406
+ findOneOrThrow(filter, options) {
2407
+ if (options && "project" in options) {
2408
+ return findOneOrThrow(this, filter, options);
2409
+ }
2410
+ return new PopulateOneOrThrowQuery(this, filter, options);
1749
2411
  }
1750
2412
  find(filter, options) {
1751
2413
  return find(this, filter, options);
@@ -1995,12 +2657,51 @@ var Database = class {
1995
2657
  return results;
1996
2658
  }
1997
2659
  /**
1998
- * Execute a function within a MongoDB transaction with auto-commit/rollback.
2660
+ * Execute a function within a MongoDB transaction.
2661
+ *
2662
+ * Starts a client session and runs the callback inside
2663
+ * `session.withTransaction()`. The driver handles commit on success,
2664
+ * abort on error, and automatic retries for transient transaction errors.
2665
+ *
2666
+ * The return value of `fn` is forwarded as the return value of this method.
2667
+ *
2668
+ * @param fn - Async callback receiving a {@link TransactionContext}.
2669
+ * @returns The value returned by `fn`.
2670
+ *
2671
+ * @example
2672
+ * ```ts
2673
+ * const user = await db.transaction(async (tx) => {
2674
+ * const txUsers = tx.use(users)
2675
+ * return await txUsers.insertOne({ name: 'Ada' })
2676
+ * })
2677
+ * ```
1999
2678
  *
2000
- * Stub — full implementation in TASK-106.
2679
+ * @example
2680
+ * ```ts
2681
+ * // Rollback on error
2682
+ * try {
2683
+ * await db.transaction(async (tx) => {
2684
+ * const txUsers = tx.use(users)
2685
+ * await txUsers.insertOne({ name: 'Ada' })
2686
+ * throw new Error('abort!')
2687
+ * })
2688
+ * } catch (err) {
2689
+ * // insert was rolled back, err is the original error
2690
+ * }
2691
+ * ```
2001
2692
  */
2002
- transaction(_fn) {
2003
- throw new Error("Not implemented");
2693
+ async transaction(fn) {
2694
+ const session = this._client.startSession();
2695
+ try {
2696
+ let result;
2697
+ await session.withTransaction(async () => {
2698
+ const tx = new TransactionContext(session);
2699
+ result = await fn(tx);
2700
+ });
2701
+ return result;
2702
+ } finally {
2703
+ await session.endSession();
2704
+ }
2004
2705
  }
2005
2706
  /**
2006
2707
  * Close the underlying `MongoClient` connection. Safe to call even if
@@ -2033,10 +2734,10 @@ function createClient(uri, dbNameOrOptions, maybeOptions) {
2033
2734
 
2034
2735
  // src/collection/collection.ts
2035
2736
  import { ObjectId as ObjectId3 } from "mongodb";
2036
- import { z as z9 } from "zod";
2737
+ import { z as z10 } from "zod";
2037
2738
 
2038
2739
  // src/schema/extensions.ts
2039
- import { z as z7 } from "zod";
2740
+ import { z as z8 } from "zod";
2040
2741
  var indexMetadata = /* @__PURE__ */ new WeakMap();
2041
2742
  function getIndexMetadata(schema) {
2042
2743
  if (typeof schema !== "object" || schema === null) return void 0;
@@ -2044,7 +2745,7 @@ function getIndexMetadata(schema) {
2044
2745
  }
2045
2746
  var GUARD = /* @__PURE__ */ Symbol.for("zodmon_extensions");
2046
2747
  function installExtensions() {
2047
- const proto = z7.ZodType.prototype;
2748
+ const proto = z8.ZodType.prototype;
2048
2749
  if (GUARD in proto) return;
2049
2750
  Object.defineProperty(proto, "index", {
2050
2751
  /**
@@ -2143,10 +2844,10 @@ installExtensions();
2143
2844
 
2144
2845
  // src/schema/object-id.ts
2145
2846
  import { ObjectId as ObjectId2 } from "mongodb";
2146
- import { z as z8 } from "zod";
2847
+ import { z as z9 } from "zod";
2147
2848
  var OBJECT_ID_HEX = /^[a-f\d]{24}$/i;
2148
2849
  function objectId() {
2149
- return z8.custom((val) => {
2850
+ return z9.custom((val) => {
2150
2851
  if (val instanceof ObjectId2) return true;
2151
2852
  return typeof val === "string" && OBJECT_ID_HEX.test(val);
2152
2853
  }, "Invalid ObjectId").transform((val) => val instanceof ObjectId2 ? val : ObjectId2.createFromHexString(val));
@@ -2165,7 +2866,8 @@ function extractFieldIndexes(shape) {
2165
2866
  }
2166
2867
  function collection(name, shape, options) {
2167
2868
  const resolvedShape = "_id" in shape ? shape : { _id: objectId().default(() => new ObjectId3()), ...shape };
2168
- const schema = z9.object(resolvedShape);
2869
+ const schema = z10.object(resolvedShape);
2870
+ const strictSchema = schema.strict();
2169
2871
  const fieldIndexes = extractFieldIndexes(shape);
2170
2872
  const { indexes: compoundIndexes, validation, ...rest } = options ?? {};
2171
2873
  return {
@@ -2175,6 +2877,7 @@ function collection(name, shape, options) {
2175
2877
  // not assignable to ZodObject<ResolvedShape<TShape>>. The cast is safe because
2176
2878
  // the runtime shape is correct — only the readonly modifier differs.
2177
2879
  schema,
2880
+ strictSchema,
2178
2881
  shape,
2179
2882
  fieldIndexes,
2180
2883
  // Safe cast: compoundIndexes is TIndexes at runtime (or an empty array when
@@ -2313,6 +3016,11 @@ export {
2313
3016
  CollectionHandle,
2314
3017
  Database,
2315
3018
  IndexBuilder,
3019
+ PopulateCursor,
3020
+ PopulateOneOrThrowQuery,
3021
+ PopulateOneQuery,
3022
+ PopulateRefBuilder,
3023
+ TransactionContext,
2316
3024
  TypedFindCursor,
2317
3025
  ZodmonAuthError,
2318
3026
  ZodmonBulkWriteError,
@@ -2332,9 +3040,11 @@ export {
2332
3040
  createAccumulatorBuilder,
2333
3041
  createClient,
2334
3042
  createExpressionBuilder,
3043
+ createPopulateCursor,
2335
3044
  deleteMany,
2336
3045
  deleteOne,
2337
3046
  deriveProjectedSchema,
3047
+ executePopulate,
2338
3048
  extractComparableOptions,
2339
3049
  extractDbName,
2340
3050
  extractFieldIndexes,
@@ -2354,10 +3064,12 @@ export {
2354
3064
  objectId,
2355
3065
  oid,
2356
3066
  raw,
3067
+ resolvePopulateStep,
2357
3068
  serializeIndexKey,
2358
3069
  syncIndexes,
2359
3070
  toCompoundIndexSpec,
2360
3071
  toFieldIndexSpec,
3072
+ unwrapRefSchema,
2361
3073
  updateMany,
2362
3074
  updateOne,
2363
3075
  wrapMongoError