@zodmon/core 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ObjectId, Document, DeleteResult, FindCursor, Collection, UpdateResult, MongoClientOptions } from 'mongodb';
1
+ import { ObjectId, Document, DeleteResult, Collection, FindCursor, ClientSession, UpdateResult, MongoClientOptions } from 'mongodb';
2
2
  import { ZodPipe, ZodCustom, ZodTransform, z, ZodDefault } from 'zod';
3
3
 
4
4
  /**
@@ -260,6 +260,8 @@ type InferInsert<TDef extends {
260
260
  type CollectionDefinition<TName extends string = string, TShape extends z.core.$ZodShape = z.core.$ZodShape, TIndexes extends readonly CompoundIndexDefinition<Extract<keyof TShape, string>>[] = readonly CompoundIndexDefinition<Extract<keyof TShape, string>>[]> = {
261
261
  readonly name: TName;
262
262
  readonly schema: z.ZodObject<ResolvedShape<TShape>>;
263
+ /** Strict validation variant that throws on unknown fields. */
264
+ readonly strictSchema: z.ZodObject<ResolvedShape<TShape>>;
263
265
  readonly shape: TShape;
264
266
  readonly fieldIndexes: FieldIndexDefinition[];
265
267
  readonly compoundIndexes: TIndexes;
@@ -319,6 +321,40 @@ type ExtractNames<T extends readonly {
319
321
  type Prettify<T> = {
320
322
  [K in keyof T]: T[K];
321
323
  } & {};
324
+ /**
325
+ * Generates all valid dot-separated field paths for a document type.
326
+ *
327
+ * Recursively traverses nested objects and unwraps arrays to match
328
+ * MongoDB's implicit array traversal semantics. Both top-level keys
329
+ * and nested paths are included in the resulting union.
330
+ *
331
+ * @example
332
+ * ```ts
333
+ * type Doc = { address: { city: string }; tags: Array<{ label: string }> }
334
+ * type Paths = DotPath<Doc>
335
+ * // ^? 'address' | 'address.city' | 'tags' | 'tags.label'
336
+ * ```
337
+ */
338
+ type DotPath<T> = T extends ReadonlyArray<infer E> ? DotPath<E> : T extends Record<string, unknown> ? {
339
+ [K in keyof T & string]: T[K] extends ReadonlyArray<infer E> ? E extends Record<string, unknown> ? K | `${K}.${DotPath<E>}` : K : T[K] extends Record<string, unknown> ? K | `${K}.${DotPath<T[K]>}` : K;
340
+ }[keyof T & string] : never;
341
+ /**
342
+ * Resolves the value type at a dot-separated path within a document type.
343
+ *
344
+ * Splits the path on `.` and recursively descends into nested types.
345
+ * Unwraps arrays at intermediate levels to match MongoDB's implicit
346
+ * array traversal. Returns `never` for invalid paths.
347
+ *
348
+ * @example
349
+ * ```ts
350
+ * type Doc = { address: { city: string }; tags: Array<{ label: string }> }
351
+ * type City = DotPathValue<Doc, 'address.city'>
352
+ * // ^? string
353
+ * type Label = DotPathValue<Doc, 'tags.label'>
354
+ * // ^? string
355
+ * ```
356
+ */
357
+ type DotPathValue<T, P extends string> = P extends `${infer K}.${infer Rest}` ? K extends keyof T ? T[K] extends ReadonlyArray<infer E> ? DotPathValue<E, Rest> : DotPathValue<T[K], Rest> : never : P extends keyof T ? T[P] : never;
322
358
 
323
359
  /**
324
360
  * Type-level marker that carries the target collection type through the
@@ -397,6 +433,48 @@ declare module 'zod' {
397
433
  * ```
398
434
  */
399
435
  declare function getRefMetadata(schema: unknown): RefMetadata | undefined;
436
+ /**
437
+ * Peel Zod wrappers (ZodOptional, ZodNullable, ZodDefault, ZodArray)
438
+ * to reach the inner schema type. Used to detect RefMarker on wrapped
439
+ * fields like `objectId().ref(X).optional()` or `z.array(objectId().ref(X))`.
440
+ *
441
+ * Bounded by wrapper depth (1-3 layers) — no circular risk.
442
+ *
443
+ * @example
444
+ * ```ts
445
+ * // ZodOptional<ZodObjectId & RefMarker<typeof Users>> → ZodObjectId & RefMarker<typeof Users>
446
+ * type Inner = UnwrapRef<typeof schema>
447
+ * ```
448
+ */
449
+ type UnwrapRef<T> = T extends z.ZodOptional<infer U> ? UnwrapRef<U> : T extends z.ZodNullable<infer U> ? UnwrapRef<U> : T extends z.ZodDefault<infer U> ? UnwrapRef<U> : T extends z.ZodArray<infer E> ? UnwrapRef<E> : T;
450
+ /**
451
+ * Extract field names that carry `.ref()` metadata from a collection definition.
452
+ *
453
+ * Unwraps ZodArray, ZodOptional, ZodNullable, ZodDefault to detect refs on
454
+ * inner schemas. Works for bare refs, array refs, and optional refs.
455
+ *
456
+ * @example
457
+ * ```ts
458
+ * type PostRefs = RefFields<typeof Posts>
459
+ * // ^? 'authorId' | 'categoryIds' | 'reviewerId'
460
+ * ```
461
+ */
462
+ type RefFields<TDef extends AnyCollection> = {
463
+ [K in keyof TDef['shape'] & string]: UnwrapRef<TDef['shape'][K]> extends RefMarker ? K : never;
464
+ }[keyof TDef['shape'] & string];
465
+ /**
466
+ * Given a collection definition and a ref-bearing field key, extract
467
+ * the target collection type from the `RefMarker<TCollection>` phantom.
468
+ *
469
+ * Unwraps Zod wrappers to reach the inner RefMarker.
470
+ *
471
+ * @example
472
+ * ```ts
473
+ * type Target = ExtractRefCollection<typeof Posts, 'authorId'>
474
+ * // ^? typeof Users
475
+ * ```
476
+ */
477
+ type ExtractRefCollection<TDef extends AnyCollection, K extends RefFields<TDef>> = UnwrapRef<TDef['shape'][K]> extends RefMarker<infer TCol> ? TCol : never;
400
478
 
401
479
  /**
402
480
  * Branded wrapper for accumulator expressions used inside `groupBy`.
@@ -448,21 +526,25 @@ type InferAccumulators<T extends Record<string, Accumulator>> = {
448
526
  [K in keyof T]: InferAccumulator<T[K]>;
449
527
  };
450
528
  /**
451
- * Field reference string prefixed with `$`, constrained to keys of `T`.
529
+ * Field reference string prefixed with `$`, constrained to dot-paths of `T`.
530
+ *
531
+ * Supports both top-level fields (`$price`) and nested dot-paths
532
+ * (`$address.city`, `$address.geo.lat`).
452
533
  *
453
534
  * @example
454
535
  * ```ts
455
- * type OrderRef = FieldRef<{ price: number; qty: number }>
456
- * // ^? '$price' | '$qty'
536
+ * type OrderRef = FieldRef<{ price: number; address: { city: string } }>
537
+ * // ^? '$price' | '$address' | '$address.city'
457
538
  * ```
458
539
  */
459
- type FieldRef<T> = `$${keyof T & string}`;
540
+ type FieldRef<T> = `$${DotPath<T>}`;
460
541
  /**
461
542
  * Typed accumulator factory passed to the `groupBy` callback.
462
543
  *
463
- * Each method constrains field names to `keyof T & string`, providing
464
- * IDE autocomplete and compile-time validation. Return types resolve
465
- * to the actual field type (`T[K]`), not `unknown`.
544
+ * Each method constrains field names to `DotPath<T>`, providing
545
+ * IDE autocomplete and compile-time validation for both top-level
546
+ * and nested dot-path fields. Return types resolve to the actual
547
+ * field type via `DotPathValue<T, F>`, not `unknown`.
466
548
  *
467
549
  * @typeParam T - The current pipeline output document type.
468
550
  *
@@ -470,10 +552,10 @@ type FieldRef<T> = `$${keyof T & string}`;
470
552
  * ```ts
471
553
  * users.aggregate()
472
554
  * .groupBy('role', acc => ({
473
- * minSalary: acc.min('salary'), // Accumulator<number>
474
- * firstName: acc.first('name'), // Accumulator<string>
475
- * allNames: acc.push('name'), // Accumulator<string[]>
476
- * count: acc.count(), // Accumulator<number>
555
+ * minSalary: acc.min('salary'), // Accumulator<number>
556
+ * firstName: acc.first('name'), // Accumulator<string>
557
+ * allCities: acc.push('address.city'), // Accumulator<string[]>
558
+ * count: acc.count(), // Accumulator<number>
477
559
  * }))
478
560
  * ```
479
561
  */
@@ -481,64 +563,109 @@ type AccumulatorBuilder<T> = {
481
563
  /** Count documents in each group. Always returns `Accumulator<number>`. */
482
564
  count(): Accumulator<number>;
483
565
  /** Sum a numeric field. Always returns `Accumulator<number>`. */
484
- sum<K extends keyof T & string>(field: K): Accumulator<number>;
566
+ sum(field: DotPath<T>): Accumulator<number>;
485
567
  /** Sum a literal number per document. Always returns `Accumulator<number>`. */
486
568
  sum(value: number): Accumulator<number>;
487
569
  /** Average a numeric field. Always returns `Accumulator<number>`. */
488
- avg<K extends keyof T & string>(field: K): Accumulator<number>;
489
- /** Minimum value of a field. Returns `Accumulator<T[K]>`. */
490
- min<K extends keyof T & string>(field: K): Accumulator<T[K]>;
491
- /** Maximum value of a field. Returns `Accumulator<T[K]>`. */
492
- max<K extends keyof T & string>(field: K): Accumulator<T[K]>;
493
- /** First value in each group (by document order). Returns `Accumulator<T[K]>`. */
494
- first<K extends keyof T & string>(field: K): Accumulator<T[K]>;
495
- /** Last value in each group (by document order). Returns `Accumulator<T[K]>`. */
496
- last<K extends keyof T & string>(field: K): Accumulator<T[K]>;
497
- /** Collect values into an array (with duplicates). Returns `Accumulator<T[K][]>`. */
498
- push<K extends keyof T & string>(field: K): Accumulator<T[K][]>;
499
- /** Collect unique values into an array. Returns `Accumulator<T[K][]>`. */
500
- addToSet<K extends keyof T & string>(field: K): Accumulator<T[K][]>;
570
+ avg(field: DotPath<T>): Accumulator<number>;
571
+ /** Minimum value of a field. Returns `Accumulator<DotPathValue<T, F>>`. */
572
+ min<F extends DotPath<T>>(field: F): Accumulator<DotPathValue<T, F>>;
573
+ /** Maximum value of a field. Returns `Accumulator<DotPathValue<T, F>>`. */
574
+ max<F extends DotPath<T>>(field: F): Accumulator<DotPathValue<T, F>>;
575
+ /** First value in each group (by document order). Returns `Accumulator<DotPathValue<T, F>>`. */
576
+ first<F extends DotPath<T>>(field: F): Accumulator<DotPathValue<T, F>>;
577
+ /** Last value in each group (by document order). Returns `Accumulator<DotPathValue<T, F>>`. */
578
+ last<F extends DotPath<T>>(field: F): Accumulator<DotPathValue<T, F>>;
579
+ /** Collect values into an array (with duplicates). Returns `Accumulator<DotPathValue<T, F>[]>`. */
580
+ push<F extends DotPath<T>>(field: F): Accumulator<DotPathValue<T, F>[]>;
581
+ /** Collect unique values into an array. Returns `Accumulator<DotPathValue<T, F>[]>`. */
582
+ addToSet<F extends DotPath<T>>(field: F): Accumulator<DotPathValue<T, F>[]>;
501
583
  };
502
584
  /**
503
585
  * Resolves the document field type from a `$`-prefixed field reference.
504
586
  *
587
+ * Supports both top-level refs (`$price`) and dot-path refs
588
+ * (`$address.city`), resolving through nested objects and arrays.
589
+ *
505
590
  * @example
506
591
  * ```ts
507
- * type Order = { price: number; name: string }
592
+ * type Order = { price: number; address: { city: string } }
508
593
  * type PriceType = FieldRefType<Order, '$price'>
509
594
  * // ^? number
595
+ * type CityType = FieldRefType<Order, '$address.city'>
596
+ * // ^? string
510
597
  * ```
511
598
  */
512
- type FieldRefType<T, F extends string> = F extends `$${infer K}` ? K extends keyof T ? T[K] : never : never;
599
+ type FieldRefType<T, F extends string> = F extends `$${infer K}` ? DotPathValue<T, K> extends never ? never : DotPathValue<T, K> : never;
513
600
  /**
514
601
  * Output shape of a single-field `groupBy` stage.
515
602
  *
516
603
  * The `_id` field holds the grouped-by field's value, and the rest of the
517
- * shape comes from the inferred accumulator types.
604
+ * shape comes from the inferred accumulator types. Supports dot-path keys
605
+ * for grouping by nested fields.
518
606
  *
519
607
  * @example
520
608
  * ```ts
521
609
  * type Result = GroupByResult<Order, 'status', { count: Accumulator<number> }>
522
610
  * // ^? { _id: string; count: number }
611
+ *
612
+ * type Nested = GroupByResult<Order, 'address.city', { count: Accumulator<number> }>
613
+ * // ^? { _id: string; count: number }
523
614
  * ```
524
615
  */
525
- type GroupByResult<T, K extends keyof T, TAccum extends Record<string, Accumulator>> = Prettify<{
526
- _id: T[K];
616
+ type GroupByResult<T, K extends DotPath<T>, TAccum extends Record<string, Accumulator>> = Prettify<{
617
+ _id: DotPathValue<T, K & string>;
527
618
  } & InferAccumulators<TAccum>>;
619
+ /**
620
+ * Replace dots with underscores in a string type.
621
+ *
622
+ * Used to sanitize dot-path keys for compound `groupBy` `_id` sub-documents,
623
+ * since MongoDB forbids dots in field names.
624
+ *
625
+ * @example
626
+ * ```ts
627
+ * type R = ReplaceDots<'address.city'>
628
+ * // ^? 'address_city'
629
+ * ```
630
+ */
631
+ type ReplaceDots<S extends string> = S extends `${infer A}.${infer B}` ? `${A}_${ReplaceDots<B>}` : S;
528
632
  /**
529
633
  * Output shape of a compound (multi-field) `groupBy` stage.
530
634
  *
531
635
  * The `_id` field is an object with one property per grouped field,
532
636
  * and the rest of the shape comes from the inferred accumulator types.
637
+ * Dot-path keys are sanitized (dots replaced with underscores) because
638
+ * MongoDB forbids dots in field names.
533
639
  *
534
640
  * @example
535
641
  * ```ts
536
642
  * type Result = GroupByCompoundResult<Order, 'status' | 'region', { count: Accumulator<number> }>
537
- * // ^? { _id: Pick<Order, 'status' | 'region'>; count: number }
643
+ * // ^? { _id: { status: string; region: string }; count: number }
644
+ *
645
+ * type Nested = GroupByCompoundResult<Order, 'name' | 'address.city', { count: Accumulator<number> }>
646
+ * // ^? { _id: { name: string; address_city: string }; count: number }
647
+ * ```
648
+ */
649
+ type GroupByCompoundResult<T, K extends DotPath<T>, TAccum extends Record<string, Accumulator>> = Prettify<{
650
+ _id: Prettify<{
651
+ [P in K & string as ReplaceDots<P>]: DotPathValue<T, P>;
652
+ }>;
653
+ } & InferAccumulators<TAccum>>;
654
+ /**
655
+ * Output shape of a null-key `groupBy` stage (global aggregation).
656
+ *
657
+ * When `groupBy(null, ...)` is used, MongoDB collapses all documents
658
+ * into a single group with `_id: null`. The rest of the shape comes
659
+ * from the inferred accumulator types.
660
+ *
661
+ * @example
662
+ * ```ts
663
+ * type Result = GroupByNullResult<{ total: Accumulator<number> }>
664
+ * // ^? { _id: null; total: number }
538
665
  * ```
539
666
  */
540
- type GroupByCompoundResult<T, K extends keyof T, TAccum extends Record<string, Accumulator>> = Prettify<{
541
- _id: Prettify<Pick<T, K>>;
667
+ type GroupByNullResult<TAccum extends Record<string, Accumulator>> = Prettify<{
668
+ _id: null;
542
669
  } & InferAccumulators<TAccum>>;
543
670
  /**
544
671
  * Output shape after unwinding an array field.
@@ -591,29 +718,6 @@ type NarrowFromFilter<T, F> = {
591
718
  $nin: ReadonlyArray<infer V extends T[K]>;
592
719
  } ? Exclude<T[K], V> : T[K] : T[K];
593
720
  };
594
- /**
595
- * Extract field keys from a collection definition whose schema fields
596
- * carry `RefMarker` (i.e. fields that have `.ref()` metadata).
597
- *
598
- * @example
599
- * ```ts
600
- * type EmpRefs = RefFields<typeof Employees> // → 'departmentId'
601
- * ```
602
- */
603
- type RefFields<TDef extends AnyCollection> = {
604
- [K in keyof TDef['shape'] & string]: TDef['shape'][K] extends RefMarker ? K : never;
605
- }[keyof TDef['shape'] & string];
606
- /**
607
- * Given a collection definition and a ref-bearing field key, extract
608
- * the target collection type from the `RefMarker<TCollection>` phantom.
609
- *
610
- * @example
611
- * ```ts
612
- * type Target = ExtractRefCollection<typeof Employees, 'departmentId'>
613
- * // → typeof Departments
614
- * ```
615
- */
616
- type ExtractRefCollection<TDef extends AnyCollection, K extends RefFields<TDef>> = TDef['shape'][K] extends RefMarker<infer TCol> ? TCol : never;
617
721
  /**
618
722
  * Extract the collection name string literal from a collection definition.
619
723
  *
@@ -670,12 +774,44 @@ type InferExpression<T> = T extends Expression<infer R> ? R : unknown;
670
774
  type InferAddedFields<T extends Record<string, unknown>> = {
671
775
  [K in keyof T]: InferExpression<T[K]>;
672
776
  };
777
+ /**
778
+ * Accepts either a dot-path field reference or a pre-built `Expression<R>`.
779
+ *
780
+ * Used throughout `ExpressionBuilder` to allow method inputs to be either a
781
+ * field name string (auto-prefixed with `$` at runtime) or the result of a
782
+ * previous expression call (embedded directly into the pipeline stage).
783
+ *
784
+ * @typeParam T - The current pipeline document type.
785
+ * @typeParam R - The runtime value type produced by this input.
786
+ *
787
+ * @example
788
+ * ```ts
789
+ * // Field path (common case)
790
+ * expr.round('salary', 2)
791
+ *
792
+ * // Composed expression
793
+ * expr.round(expr.multiply('salary', 0.1), 2)
794
+ * ```
795
+ */
796
+ type FieldOrExpr<T, R = unknown> = DotPath<T> | Expression<R>;
797
+ /**
798
+ * Extracts the element type from a readonly or mutable array type.
799
+ *
800
+ * @example
801
+ * ```ts
802
+ * type Item = ArrayElement<string[]> // → string
803
+ * type Num = ArrayElement<number[]> // → number
804
+ * type N = ArrayElement<string> // → never
805
+ * ```
806
+ */
807
+ type ArrayElement<T> = T extends ReadonlyArray<infer E> ? E : never;
673
808
  /**
674
809
  * Typed expression factory passed to the `addFields` callback.
675
810
  *
676
- * Each method constrains field names to `keyof T & string`, providing
677
- * IDE autocomplete and compile-time validation. Return types resolve
678
- * to `Expression<R>` where `R` is the MongoDB operator's output type.
811
+ * Each method accepts either a `DotPath<T>` field name string (auto-prefixed
812
+ * with `$` at runtime) or the result of a previous expression call
813
+ * (`Expression<R>`, embedded directly). This enables arbitrarily deep
814
+ * expression composition without `.raw()`.
679
815
  *
680
816
  * @typeParam T - The current pipeline output document type.
681
817
  *
@@ -683,65 +819,181 @@ type InferAddedFields<T extends Record<string, unknown>> = {
683
819
  * ```ts
684
820
  * employees.aggregate()
685
821
  * .addFields(expr => ({
686
- * hireYear: expr.year('hiredAt'), // Expression<number>
687
- * fullName: expr.concat('name', ' '), // Expression<string>
688
- * isHighPay: expr.gte('salary', 100_000), // Expression<boolean>
822
+ * hireYear: expr.year('hiredAt'),
823
+ * bonus: expr.round(expr.multiply('salary', 0.1), 2),
824
+ * profitMargin: expr.cond(
825
+ * expr.eq('revenue', 0),
826
+ * 0,
827
+ * expr.round(expr.divide(expr.subtract('revenue', 'cost'), 'revenue'), 4),
828
+ * ),
689
829
  * }))
690
830
  * ```
691
831
  */
692
832
  type ExpressionBuilder<T> = {
693
- /** Add a numeric field and a value. Returns `Expression<number>`. */
694
- add<K extends keyof T & string>(field: K, value: number | (keyof T & string)): Expression<number>;
695
- /** Subtract a value from a numeric field. Returns `Expression<number>`. */
696
- subtract<K extends keyof T & string>(field: K, value: number | (keyof T & string)): Expression<number>;
697
- /** Multiply a numeric field by a value. Returns `Expression<number>`. */
698
- multiply<K extends keyof T & string>(field: K, value: number | (keyof T & string)): Expression<number>;
699
- /** Divide a numeric field by a value. Returns `Expression<number>`. */
700
- divide<K extends keyof T & string>(field: K, value: number | (keyof T & string)): Expression<number>;
701
- /** Modulo of a numeric field. Returns `Expression<number>`. */
702
- mod<K extends keyof T & string>(field: K, value: number | (keyof T & string)): Expression<number>;
703
- /** Absolute value of a numeric field. Returns `Expression<number>`. */
704
- abs<K extends keyof T & string>(field: K): Expression<number>;
705
- /** Ceiling of a numeric field. Returns `Expression<number>`. */
706
- ceil<K extends keyof T & string>(field: K): Expression<number>;
707
- /** Floor of a numeric field. Returns `Expression<number>`. */
708
- floor<K extends keyof T & string>(field: K): Expression<number>;
709
- /** Round a numeric field to N decimal places. Returns `Expression<number>`. */
710
- round<K extends keyof T & string>(field: K, place?: number): Expression<number>;
711
- /** Concatenate field values and literal strings. Returns `Expression<string>`. */
712
- concat(...parts: Array<(keyof T & string) | string>): Expression<string>;
713
- /** Convert a string field to lowercase. Returns `Expression<string>`. */
714
- toLower<K extends keyof T & string>(field: K): Expression<string>;
715
- /** Convert a string field to uppercase. Returns `Expression<string>`. */
716
- toUpper<K extends keyof T & string>(field: K): Expression<string>;
717
- /** Trim whitespace from a string field. Returns `Expression<string>`. */
718
- trim<K extends keyof T & string>(field: K): Expression<string>;
719
- /** Extract a substring from a string field. Returns `Expression<string>`. */
720
- substr<K extends keyof T & string>(field: K, start: number, length: number): Expression<string>;
721
- /** Test equality of a field against a value. Returns `Expression<boolean>`. */
722
- eq<K extends keyof T & string>(field: K, value: T[K]): Expression<boolean>;
723
- /** Test if a field is greater than a value. Returns `Expression<boolean>`. */
724
- gt<K extends keyof T & string>(field: K, value: T[K]): Expression<boolean>;
725
- /** Test if a field is greater than or equal to a value. Returns `Expression<boolean>`. */
726
- gte<K extends keyof T & string>(field: K, value: T[K]): Expression<boolean>;
727
- /** Test if a field is less than a value. Returns `Expression<boolean>`. */
728
- lt<K extends keyof T & string>(field: K, value: T[K]): Expression<boolean>;
729
- /** Test if a field is less than or equal to a value. Returns `Expression<boolean>`. */
730
- lte<K extends keyof T & string>(field: K, value: T[K]): Expression<boolean>;
731
- /** Test if a field is not equal to a value. Returns `Expression<boolean>`. */
732
- ne<K extends keyof T & string>(field: K, value: T[K]): Expression<boolean>;
733
- /** Extract year from a date field. Returns `Expression<number>`. */
734
- year<K extends keyof T & string>(field: K): Expression<number>;
735
- /** Extract month (1-12) from a date field. Returns `Expression<number>`. */
736
- month<K extends keyof T & string>(field: K): Expression<number>;
737
- /** Extract day of month (1-31) from a date field. Returns `Expression<number>`. */
738
- dayOfMonth<K extends keyof T & string>(field: K): Expression<number>;
739
- /** Count elements in an array field. Returns `Expression<number>`. */
740
- size<K extends keyof T & string>(field: K): Expression<number>;
741
- /** Conditional expression. Returns `Expression<TThen | TElse>`. */
742
- cond<TThen, TElse>(condition: Expression<boolean>, thenValue: TThen, elseValue: TElse): Expression<TThen | TElse>;
743
- /** Replace null/missing field with a default. Returns `Expression<NonNullable<T[K]> | TDefault>`. */
744
- ifNull<K extends keyof T & string, TDefault>(field: K, fallback: TDefault): Expression<NonNullable<T[K]> | TDefault>;
833
+ /** Add two numeric values. Returns `Expression<number>`. */
834
+ add(a: FieldOrExpr<T, number>, b: FieldOrExpr<T, number> | number): Expression<number>;
835
+ /** Subtract `b` from `a`. Returns `Expression<number>`. */
836
+ subtract(a: FieldOrExpr<T, number>, b: FieldOrExpr<T, number> | number): Expression<number>;
837
+ /** Multiply two numeric values. Returns `Expression<number>`. */
838
+ multiply(a: FieldOrExpr<T, number>, b: FieldOrExpr<T, number> | number): Expression<number>;
839
+ /** Divide `a` by `b`. Returns `Expression<number>`. */
840
+ divide(a: FieldOrExpr<T, number>, b: FieldOrExpr<T, number> | number): Expression<number>;
841
+ /** Modulo of `a` divided by `b`. Returns `Expression<number>`. */
842
+ mod(a: FieldOrExpr<T, number>, b: FieldOrExpr<T, number> | number): Expression<number>;
843
+ /** Absolute value of a numeric field or expression. Returns `Expression<number>`. */
844
+ abs(field: FieldOrExpr<T, number>): Expression<number>;
845
+ /** Ceiling (round up) of a numeric field or expression. Returns `Expression<number>`. */
846
+ ceil(field: FieldOrExpr<T, number>): Expression<number>;
847
+ /** Floor (round down) of a numeric field or expression. Returns `Expression<number>`. */
848
+ floor(field: FieldOrExpr<T, number>): Expression<number>;
849
+ /** Round to `place` decimal places (default 0). Returns `Expression<number>`. */
850
+ round(field: FieldOrExpr<T, number>, place?: number): Expression<number>;
851
+ /** Concatenate field values, literal strings, and string expressions. Returns `Expression<string>`. */
852
+ concat(...parts: Array<DotPath<T> | string | Expression<string>>): Expression<string>;
853
+ /** Convert a string field or expression to lowercase. Returns `Expression<string>`. */
854
+ toLower(field: FieldOrExpr<T, string>): Expression<string>;
855
+ /** Convert a string field or expression to uppercase. Returns `Expression<string>`. */
856
+ toUpper(field: FieldOrExpr<T, string>): Expression<string>;
857
+ /** Trim whitespace from a string field or expression. Returns `Expression<string>`. */
858
+ trim(field: FieldOrExpr<T, string>): Expression<string>;
859
+ /** Extract a byte-based substring. Returns `Expression<string>`. */
860
+ substr(field: FieldOrExpr<T, string>, start: number, length: number): Expression<string>;
861
+ /** Convert any value (number, date, ObjectId, etc.) to its string representation. Returns `Expression<string>`. */
862
+ toString(field: FieldOrExpr<T, unknown>): Expression<string>;
863
+ /** Test equality of a field against a value (type-safe value). Returns `Expression<boolean>`. */
864
+ eq<F extends DotPath<T>>(field: F, value: DotPathValue<T, F>): Expression<boolean>;
865
+ /** Test equality of a field against a field reference expression (field-vs-field). Returns `Expression<boolean>`. */
866
+ eq(field: DotPath<T>, value: Expression<unknown>): Expression<boolean>;
867
+ /** Test equality with a composed expression as the left-hand side. Returns `Expression<boolean>`. */
868
+ eq(field: Expression<unknown>, value: unknown): Expression<boolean>;
869
+ /** Test if a field is greater than a value (type-safe). Returns `Expression<boolean>`. */
870
+ gt<F extends DotPath<T>>(field: F, value: DotPathValue<T, F>): Expression<boolean>;
871
+ /** Test if a field is greater than a field reference expression (field-vs-field). Returns `Expression<boolean>`. */
872
+ gt(field: DotPath<T>, value: Expression<unknown>): Expression<boolean>;
873
+ /** Test if a composed expression is greater than a value. Returns `Expression<boolean>`. */
874
+ gt(field: Expression<unknown>, value: unknown): Expression<boolean>;
875
+ /** Test if a field is greater than or equal to a value (type-safe). Returns `Expression<boolean>`. */
876
+ gte<F extends DotPath<T>>(field: F, value: DotPathValue<T, F>): Expression<boolean>;
877
+ /** Test if a field is greater than or equal to a field reference expression (field-vs-field). Returns `Expression<boolean>`. */
878
+ gte(field: DotPath<T>, value: Expression<unknown>): Expression<boolean>;
879
+ /** Test if a composed expression is greater than or equal to a value. Returns `Expression<boolean>`. */
880
+ gte(field: Expression<unknown>, value: unknown): Expression<boolean>;
881
+ /** Test if a field is less than a value (type-safe). Returns `Expression<boolean>`. */
882
+ lt<F extends DotPath<T>>(field: F, value: DotPathValue<T, F>): Expression<boolean>;
883
+ /** Test if a field is less than a field reference expression (field-vs-field). Returns `Expression<boolean>`. */
884
+ lt(field: DotPath<T>, value: Expression<unknown>): Expression<boolean>;
885
+ /** Test if a composed expression is less than a value. Returns `Expression<boolean>`. */
886
+ lt(field: Expression<unknown>, value: unknown): Expression<boolean>;
887
+ /** Test if a field is less than or equal to a value (type-safe). Returns `Expression<boolean>`. */
888
+ lte<F extends DotPath<T>>(field: F, value: DotPathValue<T, F>): Expression<boolean>;
889
+ /** Test if a field is less than or equal to a field reference expression (field-vs-field). Returns `Expression<boolean>`. */
890
+ lte(field: DotPath<T>, value: Expression<unknown>): Expression<boolean>;
891
+ /** Test if a composed expression is less than or equal to a value. Returns `Expression<boolean>`. */
892
+ lte(field: Expression<unknown>, value: unknown): Expression<boolean>;
893
+ /** Test if a field is not equal to a value (type-safe). Returns `Expression<boolean>`. */
894
+ ne<F extends DotPath<T>>(field: F, value: DotPathValue<T, F>): Expression<boolean>;
895
+ /** Test if a field is not equal to a field reference expression (field-vs-field). Returns `Expression<boolean>`. */
896
+ ne(field: DotPath<T>, value: Expression<unknown>): Expression<boolean>;
897
+ /** Test if a composed expression is not equal to a value. Returns `Expression<boolean>`. */
898
+ ne(field: Expression<unknown>, value: unknown): Expression<boolean>;
899
+ /** Extract year from a date field or expression. Returns `Expression<number>`. */
900
+ year(field: FieldOrExpr<T, Date>): Expression<number>;
901
+ /** Extract month (1-12) from a date field or expression. Returns `Expression<number>`. */
902
+ month(field: FieldOrExpr<T, Date>): Expression<number>;
903
+ /** Extract day of month (1-31) from a date field or expression. Returns `Expression<number>`. */
904
+ dayOfMonth(field: FieldOrExpr<T, Date>): Expression<number>;
905
+ /** Extract day of week (1=Sunday … 7=Saturday) from a date field or expression. Returns `Expression<number>`. */
906
+ dayOfWeek(field: FieldOrExpr<T, Date>): Expression<number>;
907
+ /**
908
+ * Format a date field or expression as a string.
909
+ *
910
+ * Uses MongoDB format specifiers: `%Y` year, `%m` month (01-12), `%d` day (01-31), etc.
911
+ *
912
+ * @example
913
+ * ```ts
914
+ * expr.dateToString('createdAt', '%Y-%m') // Expression<string>
915
+ * ```
916
+ */
917
+ dateToString(field: FieldOrExpr<T, Date>, format: string): Expression<string>;
918
+ /** Current timestamp at query start (`$$NOW`). Returns `Expression<Date>`. */
919
+ now(): Expression<Date>;
920
+ /** Count elements in an array field or expression. Returns `Expression<number>`. */
921
+ size(field: FieldOrExpr<T, unknown[]>): Expression<number>;
922
+ /**
923
+ * Check if a value is present in an array.
924
+ *
925
+ * Named `inArray` to distinguish from the filter-level `$in` operator.
926
+ * The second argument may be a field/expression producing an array, or a literal array.
927
+ *
928
+ * @example
929
+ * ```ts
930
+ * expr.inArray(expr.dayOfWeek('createdAt'), [1, 7]) // Expression<boolean>
931
+ * ```
932
+ */
933
+ inArray(value: FieldOrExpr<T, unknown>, array: FieldOrExpr<T, unknown[]> | ReadonlyArray<unknown>): Expression<boolean>;
934
+ /**
935
+ * Return the element at `index` from an array field (zero-based).
936
+ *
937
+ * When called with a typed field path, the element type is inferred from the schema.
938
+ * When called with a composed expression, the element type is `unknown`.
939
+ *
940
+ * @example
941
+ * ```ts
942
+ * // items: Item[] in schema → Expression<Item>
943
+ * expr.arrayElemAt('items', 0)
944
+ * ```
945
+ */
946
+ arrayElemAt<F extends DotPath<T>>(field: F, index: number): Expression<ArrayElement<DotPathValue<T, F>>>;
947
+ /** Return the element at `index` from an array expression. Returns `Expression<unknown>`. */
948
+ arrayElemAt(field: Expression<unknown[]>, index: number): Expression<unknown>;
949
+ /**
950
+ * Conditional expression: if `condition` is true return `thenValue`, else return `elseValue`.
951
+ *
952
+ * Both `thenValue` and `elseValue` accept literal values or composed `Expression<R>` results.
953
+ * Returns `Expression<TThen | TElse>`.
954
+ *
955
+ * @example
956
+ * ```ts
957
+ * expr.cond(expr.eq('revenue', 0), 0, expr.round(expr.divide('profit', 'revenue'), 4))
958
+ * ```
959
+ */
960
+ cond<TThen, TElse>(condition: Expression<boolean>, thenValue: TThen | Expression<TThen>, elseValue: TElse | Expression<TElse>): Expression<TThen | TElse>;
961
+ /** Replace null/missing field with a default. Returns `Expression<NonNullable<DotPathValue<T, F>> | TDefault>`. */
962
+ ifNull<F extends DotPath<T>, TDefault>(field: F, fallback: TDefault): Expression<NonNullable<DotPathValue<T, F>> | TDefault>;
963
+ /** Replace null/missing expression result with a default. Returns `Expression<NonNullable<R> | TDefault>`. */
964
+ ifNull<R, TDefault>(field: Expression<R>, fallback: TDefault): Expression<NonNullable<R> | TDefault>;
965
+ /**
966
+ * Multi-branch conditional. Evaluates `branches` in order and returns the first matching `then` value,
967
+ * or `fallback` if no branch matches.
968
+ *
969
+ * TypeScript infers `TThen` as the union of all `then` value types and the fallback type.
970
+ *
971
+ * @example
972
+ * ```ts
973
+ * expr.switch([
974
+ * { case: expr.gte('spend', 10_000), then: 'VIP' },
975
+ * { case: expr.gte('spend', 5_000), then: 'Gold' },
976
+ * ], 'Bronze')
977
+ * // → Expression<'VIP' | 'Gold' | 'Bronze'>
978
+ * ```
979
+ */
980
+ switch<TThen>(branches: ReadonlyArray<{
981
+ case: Expression<boolean>;
982
+ then: TThen | Expression<TThen>;
983
+ }>, fallback: TThen | Expression<TThen>): Expression<TThen>;
984
+ /**
985
+ * Create a typed field reference for use as a comparison operand.
986
+ *
987
+ * Use when the right-hand side of a comparison should be a document
988
+ * field rather than a literal value (field-vs-field comparison).
989
+ *
990
+ * @example
991
+ * ```ts
992
+ * // Compare two fields in the same document
993
+ * .match({}, (expr) => expr.gt('totalAmount', expr.field('refundedAmount')))
994
+ * ```
995
+ */
996
+ field<F extends DotPath<T>>(name: F): Expression<DotPathValue<T, F>>;
745
997
  };
746
998
 
747
999
  /**
@@ -792,139 +1044,169 @@ declare const $avg: (field: `$${string}`) => Accumulator<number>;
792
1044
  /**
793
1045
  * Returns the minimum value across documents in each group.
794
1046
  *
795
- * For full type safety, prefer the callback builder (`acc.min('price')`).
796
- * The standalone form accepts an explicit result type `R` for manual typing.
1047
+ * Three tiers of type safety:
1048
+ * - **Tier 1 (document type):** `$min<Doc>('$address.city')` resolves via `FieldRefType`.
1049
+ * - **Tier 2 (explicit scalar):** `$min<number>('$price')` — manual result type.
1050
+ * - **Tier 3 (untyped):** `$min('$price')` — defaults to `unknown`.
797
1051
  *
798
- * @typeParam R - The expected result type (defaults to `unknown`).
799
- * @param field - A `$field` reference.
800
- * @returns An `Accumulator<R>`.
1052
+ * For best type safety, prefer the callback builder (`acc.min('price')`).
1053
+ *
1054
+ * @param field - A `$field` reference (supports dot-paths with document type param).
1055
+ * @returns An `Accumulator` with the resolved result type.
801
1056
  *
802
1057
  * @example
803
1058
  * ```ts
1059
+ * // Tier 1 — document type with dot-path
1060
+ * $min<Doc>('$address.city') // Accumulator<string>
1061
+ *
804
1062
  * // Tier 2 — explicit result type
805
- * const pipeline = orders.aggregate()
806
- * .groupBy('category', { cheapest: $min<number>('$price') })
1063
+ * $min<number>('$price') // Accumulator<number>
807
1064
  *
808
- * // Tier 1callback builder (recommended)
809
- * orders.aggregate()
810
- * .groupBy('category', acc => ({ cheapest: acc.min('price') }))
1065
+ * // Tier 3untyped
1066
+ * $min('$price') // Accumulator<unknown>
811
1067
  * ```
812
1068
  */
813
- declare const $min: <R = unknown>(field: `$${string}`) => Accumulator<R>;
1069
+ declare function $min<T extends Record<string, unknown>>(field: FieldRef<T>): Accumulator<FieldRefType<T, FieldRef<T>>>;
1070
+ declare function $min<R = unknown>(field: `$${string}`): Accumulator<R>;
814
1071
  /**
815
1072
  * Returns the maximum value across documents in each group.
816
1073
  *
817
- * For full type safety, prefer the callback builder (`acc.max('price')`).
818
- * The standalone form accepts an explicit result type `R` for manual typing.
1074
+ * Three tiers of type safety:
1075
+ * - **Tier 1 (document type):** `$max<Doc>('$address.city')` resolves via `FieldRefType`.
1076
+ * - **Tier 2 (explicit scalar):** `$max<number>('$price')` — manual result type.
1077
+ * - **Tier 3 (untyped):** `$max('$price')` — defaults to `unknown`.
1078
+ *
1079
+ * For best type safety, prefer the callback builder (`acc.max('price')`).
819
1080
  *
820
- * @typeParam R - The expected result type (defaults to `unknown`).
821
- * @param field - A `$field` reference.
822
- * @returns An `Accumulator<R>`.
1081
+ * @param field - A `$field` reference (supports dot-paths with document type param).
1082
+ * @returns An `Accumulator` with the resolved result type.
823
1083
  *
824
1084
  * @example
825
1085
  * ```ts
1086
+ * // Tier 1 — document type with dot-path
1087
+ * $max<Doc>('$tags.label') // Accumulator<string>
1088
+ *
826
1089
  * // Tier 2 — explicit result type
827
- * const pipeline = orders.aggregate()
828
- * .groupBy('category', { priciest: $max<number>('$price') })
1090
+ * $max<number>('$price') // Accumulator<number>
829
1091
  *
830
- * // Tier 1callback builder (recommended)
831
- * orders.aggregate()
832
- * .groupBy('category', acc => ({ priciest: acc.max('price') }))
1092
+ * // Tier 3untyped
1093
+ * $max('$price') // Accumulator<unknown>
833
1094
  * ```
834
1095
  */
835
- declare const $max: <R = unknown>(field: `$${string}`) => Accumulator<R>;
1096
+ declare function $max<T extends Record<string, unknown>>(field: FieldRef<T>): Accumulator<FieldRefType<T, FieldRef<T>>>;
1097
+ declare function $max<R = unknown>(field: `$${string}`): Accumulator<R>;
836
1098
  /**
837
1099
  * Returns the first value in each group according to the document order.
838
1100
  *
839
- * For full type safety, prefer the callback builder (`acc.first('name')`).
840
- * The standalone form accepts an explicit result type `R` for manual typing.
1101
+ * Three tiers of type safety:
1102
+ * - **Tier 1 (document type):** `$first<Doc>('$address.city')` resolves via `FieldRefType`.
1103
+ * - **Tier 2 (explicit scalar):** `$first<Date>('$createdAt')` — manual result type.
1104
+ * - **Tier 3 (untyped):** `$first('$name')` — defaults to `unknown`.
841
1105
  *
842
- * @typeParam R - The expected result type (defaults to `unknown`).
843
- * @param field - A `$field` reference.
844
- * @returns An `Accumulator<R>`.
1106
+ * For best type safety, prefer the callback builder (`acc.first('name')`).
1107
+ *
1108
+ * @param field - A `$field` reference (supports dot-paths with document type param).
1109
+ * @returns An `Accumulator` with the resolved result type.
845
1110
  *
846
1111
  * @example
847
1112
  * ```ts
1113
+ * // Tier 1 — document type with dot-path
1114
+ * $first<Doc>('$address.city') // Accumulator<string>
1115
+ *
848
1116
  * // Tier 2 — explicit result type
849
- * const pipeline = orders.aggregate()
850
- * .sort({ createdAt: 1 })
851
- * .groupBy('status', { earliest: $first<Date>('$createdAt') })
1117
+ * $first<Date>('$createdAt') // Accumulator<Date>
852
1118
  *
853
- * // Tier 1callback builder (recommended)
854
- * orders.aggregate()
855
- * .sort({ createdAt: 1 })
856
- * .groupBy('status', acc => ({ earliest: acc.first('createdAt') }))
1119
+ * // Tier 3untyped
1120
+ * $first('$name') // Accumulator<unknown>
857
1121
  * ```
858
1122
  */
859
- declare const $first: <R = unknown>(field: `$${string}`) => Accumulator<R>;
1123
+ declare function $first<T extends Record<string, unknown>>(field: FieldRef<T>): Accumulator<FieldRefType<T, FieldRef<T>>>;
1124
+ declare function $first<R = unknown>(field: `$${string}`): Accumulator<R>;
860
1125
  /**
861
1126
  * Returns the last value in each group according to the document order.
862
1127
  *
863
- * For full type safety, prefer the callback builder (`acc.last('name')`).
864
- * The standalone form accepts an explicit result type `R` for manual typing.
1128
+ * Three tiers of type safety:
1129
+ * - **Tier 1 (document type):** `$last<Doc>('$address.city')` resolves via `FieldRefType`.
1130
+ * - **Tier 2 (explicit scalar):** `$last<Date>('$createdAt')` — manual result type.
1131
+ * - **Tier 3 (untyped):** `$last('$name')` — defaults to `unknown`.
1132
+ *
1133
+ * For best type safety, prefer the callback builder (`acc.last('name')`).
865
1134
  *
866
- * @typeParam R - The expected result type (defaults to `unknown`).
867
- * @param field - A `$field` reference.
868
- * @returns An `Accumulator<R>`.
1135
+ * @param field - A `$field` reference (supports dot-paths with document type param).
1136
+ * @returns An `Accumulator` with the resolved result type.
869
1137
  *
870
1138
  * @example
871
1139
  * ```ts
1140
+ * // Tier 1 — document type with dot-path
1141
+ * $last<Doc>('$address.city') // Accumulator<string>
1142
+ *
872
1143
  * // Tier 2 — explicit result type
873
- * const pipeline = orders.aggregate()
874
- * .sort({ createdAt: -1 })
875
- * .groupBy('status', { latest: $last<Date>('$createdAt') })
1144
+ * $last<Date>('$createdAt') // Accumulator<Date>
876
1145
  *
877
- * // Tier 1callback builder (recommended)
878
- * orders.aggregate()
879
- * .sort({ createdAt: -1 })
880
- * .groupBy('status', acc => ({ latest: acc.last('createdAt') }))
1146
+ * // Tier 3untyped
1147
+ * $last('$name') // Accumulator<unknown>
881
1148
  * ```
882
1149
  */
883
- declare const $last: <R = unknown>(field: `$${string}`) => Accumulator<R>;
1150
+ declare function $last<T extends Record<string, unknown>>(field: FieldRef<T>): Accumulator<FieldRefType<T, FieldRef<T>>>;
1151
+ declare function $last<R = unknown>(field: `$${string}`): Accumulator<R>;
884
1152
  /**
885
1153
  * Pushes values into an array for each group.
886
1154
  *
887
1155
  * May contain duplicates. Use `$addToSet` for unique values.
888
- * For full type safety, prefer the callback builder (`acc.push('name')`).
889
1156
  *
890
- * @typeParam R - The element type (defaults to `unknown`).
891
- * @param field - A `$field` reference.
892
- * @returns An `Accumulator<R[]>`.
1157
+ * Three tiers of type safety:
1158
+ * - **Tier 1 (document type):** `$push<Doc>('$address.city')` — resolves via `FieldRefType`.
1159
+ * - **Tier 2 (explicit scalar):** `$push<string>('$name')` — manual element type.
1160
+ * - **Tier 3 (untyped):** `$push('$name')` — defaults to `unknown[]`.
1161
+ *
1162
+ * For best type safety, prefer the callback builder (`acc.push('name')`).
1163
+ *
1164
+ * @param field - A `$field` reference (supports dot-paths with document type param).
1165
+ * @returns An `Accumulator` with the resolved array result type.
893
1166
  *
894
1167
  * @example
895
1168
  * ```ts
1169
+ * // Tier 1 — document type with dot-path
1170
+ * $push<Doc>('$address.city') // Accumulator<string[]>
1171
+ *
896
1172
  * // Tier 2 — explicit element type
897
- * const pipeline = orders.aggregate()
898
- * .groupBy('status', { items: $push<string>('$name') })
1173
+ * $push<string>('$name') // Accumulator<string[]>
899
1174
  *
900
- * // Tier 1callback builder (recommended)
901
- * orders.aggregate()
902
- * .groupBy('status', acc => ({ items: acc.push('name') }))
1175
+ * // Tier 3untyped
1176
+ * $push('$name') // Accumulator<unknown[]>
903
1177
  * ```
904
1178
  */
905
- declare const $push: <R = unknown>(field: `$${string}`) => Accumulator<R[]>;
1179
+ declare function $push<T extends Record<string, unknown>>(field: FieldRef<T>): Accumulator<FieldRefType<T, FieldRef<T>>[]>;
1180
+ declare function $push<R = unknown>(field: `$${string}`): Accumulator<R[]>;
906
1181
  /**
907
1182
  * Collects unique values into an array for each group (set semantics).
908
1183
  *
909
1184
  * Like `$push` but deduplicates.
910
- * For full type safety, prefer the callback builder (`acc.addToSet('tag')`).
911
1185
  *
912
- * @typeParam R - The element type (defaults to `unknown`).
913
- * @param field - A `$field` reference.
914
- * @returns An `Accumulator<R[]>`.
1186
+ * Three tiers of type safety:
1187
+ * - **Tier 1 (document type):** `$addToSet<Doc>('$address.zip')` — resolves via `FieldRefType`.
1188
+ * - **Tier 2 (explicit scalar):** `$addToSet<string>('$tag')` — manual element type.
1189
+ * - **Tier 3 (untyped):** `$addToSet('$tag')` — defaults to `unknown[]`.
1190
+ *
1191
+ * For best type safety, prefer the callback builder (`acc.addToSet('tag')`).
1192
+ *
1193
+ * @param field - A `$field` reference (supports dot-paths with document type param).
1194
+ * @returns An `Accumulator` with the resolved array result type.
915
1195
  *
916
1196
  * @example
917
1197
  * ```ts
1198
+ * // Tier 1 — document type with dot-path
1199
+ * $addToSet<Doc>('$address.zip') // Accumulator<string[]>
1200
+ *
918
1201
  * // Tier 2 — explicit element type
919
- * const pipeline = orders.aggregate()
920
- * .groupBy('category', { uniqueTags: $addToSet<string>('$tag') })
1202
+ * $addToSet<string>('$tag') // Accumulator<string[]>
921
1203
  *
922
- * // Tier 1callback builder (recommended)
923
- * orders.aggregate()
924
- * .groupBy('category', acc => ({ uniqueTags: acc.addToSet('tag') }))
1204
+ * // Tier 3untyped
1205
+ * $addToSet('$tag') // Accumulator<unknown[]>
925
1206
  * ```
926
1207
  */
927
- declare const $addToSet: <R = unknown>(field: `$${string}`) => Accumulator<R[]>;
1208
+ declare function $addToSet<T extends Record<string, unknown>>(field: FieldRef<T>): Accumulator<FieldRefType<T, FieldRef<T>>[]>;
1209
+ declare function $addToSet<R = unknown>(field: `$${string}`): Accumulator<R[]>;
928
1210
  /**
929
1211
  * Create a typed accumulator builder for use inside `groupBy` callbacks.
930
1212
  *
@@ -947,19 +1229,20 @@ declare function createAccumulatorBuilder<T>(): AccumulatorBuilder<T>;
947
1229
  /**
948
1230
  * Create a typed expression builder for use inside `addFields` callbacks.
949
1231
  *
950
- * The builder adds the `$` prefix to field names automatically and returns
951
- * properly typed `Expression<T>` values. Primarily used internally by
952
- * `AggregatePipeline.addFields` — most users interact with the builder via
953
- * the callback parameter rather than calling this directly.
1232
+ * The builder accepts either a field name string (auto-prefixed with `$`) or a
1233
+ * pre-built `Expression<R>` from another builder method, enabling composition.
1234
+ * Primarily used internally by `AggregatePipeline.addFields` — most users
1235
+ * interact with the builder via the callback parameter rather than calling
1236
+ * this directly.
954
1237
  *
955
1238
  * @typeParam T - The current pipeline output document type.
956
1239
  * @returns An `ExpressionBuilder<T>` with methods for each MongoDB expression operator.
957
1240
  *
958
1241
  * @example
959
1242
  * ```ts
960
- * const expr = createExpressionBuilder<{ salary: number; name: string; hiredAt: Date }>()
961
- * expr.year('hiredAt') // { __expr: true, value: { $year: '$hiredAt' } }
962
- * expr.multiply('salary', 0.9) // { __expr: true, value: { $multiply: ['$salary', 0.9] } }
1243
+ * const expr = createExpressionBuilder<{ salary: number; hiredAt: Date }>()
1244
+ * expr.year('hiredAt') // { __expr: true, value: { $year: '$hiredAt' } }
1245
+ * expr.round(expr.multiply('salary', 0.1), 2) // { __expr: true, value: { $round: [{ $multiply: ['$salary', 0.1] }, 2] } }
963
1246
  * ```
964
1247
  */
965
1248
  declare function createExpressionBuilder<T>(): ExpressionBuilder<T>;
@@ -1005,40 +1288,21 @@ type ComparisonOperators<V> = {
1005
1288
  } & (V extends string ? {
1006
1289
  $regex?: RegExp | string;
1007
1290
  } : unknown);
1008
- /** Depth counter for limiting dot-notation recursion. Index = current depth, value = next depth. */
1009
- type Prev = [never, 0, 1, 2];
1010
- /**
1011
- * Generates a union of all valid dot-separated paths for nested object fields in `T`.
1012
- *
1013
- * Recursion is limited to 3 levels deep to prevent TypeScript compilation performance issues.
1014
- * Only plain object fields are traversed — arrays, `Date`, `RegExp`, and `ObjectId` are
1015
- * treated as leaf nodes and do not produce sub-paths.
1016
- *
1017
- * @example
1018
- * ```ts
1019
- * type User = { address: { city: string; geo: { lat: number; lng: number } } }
1020
- *
1021
- * // DotPaths<User> = 'address.city' | 'address.geo' | 'address.geo.lat' | 'address.geo.lng'
1022
- * ```
1023
- */
1024
- type DotPaths<T, Depth extends number = 3> = Depth extends 0 ? never : {
1025
- [K in keyof T & string]: NonNullable<T[K]> extends ReadonlyArray<unknown> | Date | RegExp | ObjectId ? never : NonNullable<T[K]> extends Record<string, unknown> ? `${K}.${keyof NonNullable<T[K]> & string}` | `${K}.${DotPaths<NonNullable<T[K]>, Prev[Depth]>}` : never;
1026
- }[keyof T & string];
1027
1291
  /**
1028
- * Resolves the value type at a dot-separated path `P` within type `T`.
1292
+ * Dot-separated paths for nested fields, excluding top-level keys.
1029
1293
  *
1030
- * Splits `P` on the first `.` and recursively descends into `T`'s nested types.
1031
- * Returns `never` if the path is invalid.
1294
+ * Filters out `keyof T & string` because top-level fields are already
1295
+ * handled by slot (A) of {@link TypedFilter}. Keeping them separate
1296
+ * preserves the intersection structure for lazy TypeScript evaluation.
1032
1297
  *
1033
1298
  * @example
1034
1299
  * ```ts
1035
- * type User = { address: { city: string; geo: { lat: number } } }
1036
- *
1037
- * // DotPathType<User, 'address.city'> = string
1038
- * // DotPathType<User, 'address.geo.lat'> = number
1300
+ * type User = { name: string; address: { city: string } }
1301
+ * type Paths = DotSubPaths<User>
1302
+ * // ^? 'address.city'
1039
1303
  * ```
1040
1304
  */
1041
- type DotPathType<T, P extends string> = P extends `${infer K}.${infer Rest}` ? K extends keyof T ? Rest extends keyof NonNullable<T[K]> ? NonNullable<T[K]>[Rest] : DotPathType<NonNullable<T[K]>, Rest> : never : P extends keyof T ? T[P] : never;
1305
+ type DotSubPaths<T> = Exclude<DotPath<T>, keyof T & string>;
1042
1306
  /**
1043
1307
  * Strict type-safe MongoDB filter query type.
1044
1308
  *
@@ -1049,7 +1313,7 @@ type DotPathType<T, P extends string> = P extends `${infer K}.${infer Rest}` ? K
1049
1313
  * Supports three forms of filter expressions:
1050
1314
  * - **Direct field values** (implicit `$eq`): `{ name: 'Alice' }`
1051
1315
  * - **Comparison operators**: `{ age: { $gt: 25 } }` or `{ age: $gt(25) }`
1052
- * - **Dot notation** for nested fields up to 3 levels: `{ 'address.city': 'NYC' }`
1316
+ * - **Dot notation** including nested and array-traversed fields: `{ 'address.city': 'NYC' }`
1053
1317
  *
1054
1318
  * Logical operators `$and`, `$or`, and `$nor` accept arrays of `TypedFilter<T>`
1055
1319
  * for composing complex queries.
@@ -1078,7 +1342,7 @@ type DotPathType<T, P extends string> = P extends `${infer K}.${infer Rest}` ? K
1078
1342
  type TypedFilter<T> = {
1079
1343
  [K in keyof T]?: T[K] | ComparisonOperators<T[K]>;
1080
1344
  } & {
1081
- [P in DotPaths<T>]?: DotPathType<T, P> | ComparisonOperators<DotPathType<T, P>>;
1345
+ [P in DotSubPaths<T>]?: DotPathValue<T, P> | ComparisonOperators<DotPathValue<T, P>>;
1082
1346
  } & {
1083
1347
  /** Joins clauses with a logical AND. Matches documents that satisfy all filters. */
1084
1348
  $and?: TypedFilter<T>[];
@@ -1239,72 +1503,602 @@ type CursorPage<TDoc> = {
1239
1503
  endCursor: string | null;
1240
1504
  };
1241
1505
 
1506
+ type IncludeValue = 1 | true;
1507
+ type ExcludeValue = 0 | false;
1242
1508
  /**
1243
- * Type-safe sort specification for a document type.
1509
+ * Inclusion projection maps document fields to `1 | true`.
1244
1510
  *
1245
- * Constrains sort keys to top-level fields of `T` with direction `1` (ascending)
1246
- * or `-1` (descending). Dot-path sorts deferred to v1.0.
1511
+ * Allows `_id: 0 | false` to suppress the `_id` field from the result.
1512
+ * All other fields only accept inclusion values (`1` or `true`).
1247
1513
  *
1248
1514
  * @example
1249
1515
  * ```ts
1250
- * const sort: TypedSort<User> = { name: 1, createdAt: -1 }
1516
+ * type UserInclusion = InclusionProjection<User>
1517
+ * const proj: UserInclusion = { name: 1, email: 1 }
1518
+ * const withoutId: UserInclusion = { name: 1, _id: 0 }
1251
1519
  * ```
1252
1520
  */
1253
- type TypedSort<T> = Partial<Record<keyof T & string, 1 | -1>>;
1521
+ type InclusionProjection<T> = {
1522
+ [K in keyof T & string]?: K extends '_id' ? IncludeValue | ExcludeValue : IncludeValue;
1523
+ };
1254
1524
  /**
1255
- * Type-safe cursor wrapping MongoDB's `FindCursor`.
1525
+ * Exclusion projection maps document fields to `0 | false`.
1256
1526
  *
1257
- * Provides chainable query modifiers (`sort`, `skip`, `limit`, `hint`) that return
1258
- * `this` for fluent chaining, and terminal methods (`toArray`,
1259
- * `[Symbol.asyncIterator]`) that validate each document against the
1260
- * collection's Zod schema before returning.
1527
+ * Removes the specified fields from the result, keeping all others.
1261
1528
  *
1262
- * Created by {@link find} — do not construct directly.
1529
+ * @example
1530
+ * ```ts
1531
+ * type UserExclusion = ExclusionProjection<User>
1532
+ * const proj: UserExclusion = { email: 0, age: 0 }
1533
+ * ```
1534
+ */
1535
+ type ExclusionProjection<T> = {
1536
+ [K in keyof T & string]?: ExcludeValue;
1537
+ };
1538
+ /**
1539
+ * Union of inclusion or exclusion projection for a document type `T`.
1263
1540
  *
1264
- * @typeParam TDef - The collection definition type, used to infer the document type.
1265
- * @typeParam TIndexNames - Union of declared index names accepted by `.hint()`.
1541
+ * MongoDB projections must be either all-inclusion or all-exclusion (with the
1542
+ * exception of `_id`, which can always be excluded). This type enforces that
1543
+ * constraint at the type level.
1266
1544
  *
1267
1545
  * @example
1268
1546
  * ```ts
1269
- * const docs = await find(users, { role: 'admin' })
1270
- * .sort({ name: 1 })
1271
- * .limit(10)
1272
- * .toArray()
1547
+ * type UserProjection = TypedProjection<User>
1548
+ * const include: UserProjection = { name: 1, email: 1 }
1549
+ * const exclude: UserProjection = { email: 0, age: 0 }
1273
1550
  * ```
1274
1551
  */
1275
- declare class TypedFindCursor<TDef extends AnyCollection, TIndexNames extends string = string> {
1276
- /** @internal */
1277
- private cursor;
1278
- /** @internal */
1279
- private schema;
1280
- /** @internal */
1281
- private collectionName;
1282
- /** @internal */
1283
- private mode;
1284
- /** @internal */
1285
- private readonly nativeCollection;
1286
- /** @internal */
1287
- private readonly filter;
1288
- /** @internal */
1289
- private sortSpec;
1290
- /** @internal */
1291
- constructor(cursor: FindCursor<InferDocument<TDef>>, definition: TDef, mode: ValidationMode | false, nativeCollection: Collection<InferDocument<TDef>>, filter: any);
1552
+ type TypedProjection<T> = InclusionProjection<T> | ExclusionProjection<T>;
1553
+ /** True if any key in `P` (excluding `_id`) has value `1 | true`. */
1554
+ type IsInclusion<P> = true extends {
1555
+ [K in keyof P]: K extends '_id' ? never : P[K] extends IncludeValue ? true : never;
1556
+ }[keyof P] ? true : false;
1557
+ /** Keys of `P` whose value is `1 | true` (excluding `_id`). */
1558
+ type IncludedKeys<P> = {
1559
+ [K in keyof P]: K extends '_id' ? never : P[K] extends IncludeValue ? K : never;
1560
+ }[keyof P];
1561
+ /** Keys of `P` whose value is `0 | false`. */
1562
+ type ExcludedKeys<P> = {
1563
+ [K in keyof P]: P[K] extends ExcludeValue ? K : never;
1564
+ }[keyof P];
1565
+ /** True if `P` has `_id` set to `0 | false`. */
1566
+ type IsIdSuppressed<P> = '_id' extends keyof P ? P['_id'] extends ExcludeValue ? true : false : false;
1567
+ /**
1568
+ * Computes the result type after applying a projection `P` to document type `T`.
1569
+ *
1570
+ * For inclusion projections (`{ name: 1 }`), returns only the included fields
1571
+ * plus `_id` (unless `_id: 0` is specified). For exclusion projections
1572
+ * (`{ email: 0 }`), returns all fields except the excluded ones.
1573
+ *
1574
+ * @example
1575
+ * ```ts
1576
+ * type User = { _id: ObjectId; name: string; email: string; age: number }
1577
+ *
1578
+ * // Inclusion: picks name + _id
1579
+ * type A = ProjectionResult<User, { name: 1 }>
1580
+ * // ^? { _id: ObjectId; name: string }
1581
+ *
1582
+ * // Inclusion with _id suppressed
1583
+ * type B = ProjectionResult<User, { name: 1; _id: 0 }>
1584
+ * // ^? { name: string }
1585
+ *
1586
+ * // Exclusion: drops email
1587
+ * type C = ProjectionResult<User, { email: 0 }>
1588
+ * // ^? { _id: ObjectId; name: string; age: number }
1589
+ * ```
1590
+ */
1591
+ type ProjectionResult<T, P> = IsInclusion<P> extends true ? IsIdSuppressed<P> extends true ? Prettify<Pick<T, IncludedKeys<P> & keyof T>> : Prettify<Pick<T, (IncludedKeys<P> | '_id') & keyof T>> : IsIdSuppressed<P> extends true ? Prettify<Omit<T, ExcludedKeys<P>>> : Prettify<Omit<T, Exclude<ExcludedKeys<P>, '_id'>>>;
1592
+ /**
1593
+ * Returns `true` if the projection is an inclusion projection.
1594
+ *
1595
+ * A projection is considered inclusion when any key other than `_id` has
1596
+ * a value of `1` or `true`. This mirrors the MongoDB rule: you cannot mix
1597
+ * inclusion and exclusion (except for `_id`, which may always be suppressed).
1598
+ *
1599
+ * @example
1600
+ * ```ts
1601
+ * isInclusionProjection({ name: 1 }) // true
1602
+ * isInclusionProjection({ name: 1, _id: 0 }) // true
1603
+ * isInclusionProjection({ email: 0 }) // false
1604
+ * ```
1605
+ */
1606
+ declare function isInclusionProjection(projection: Record<string, 0 | 1 | boolean>): boolean;
1607
+ /**
1608
+ * Derives a scoped Zod schema by applying a MongoDB-style projection.
1609
+ *
1610
+ * For inclusion projections (`{ name: 1, email: 1 }`), returns a schema with
1611
+ * only the specified fields plus `_id` (unless `_id: 0`). For exclusion
1612
+ * projections (`{ email: 0 }`), returns a schema with the specified fields
1613
+ * removed.
1614
+ *
1615
+ * Keys not present in the original schema are silently ignored.
1616
+ *
1617
+ * @example
1618
+ * ```ts
1619
+ * const userSchema = z.object({
1620
+ * _id: z.string(),
1621
+ * name: z.string(),
1622
+ * email: z.string(),
1623
+ * age: z.number(),
1624
+ * })
1625
+ *
1626
+ * // Inclusion: picks name + email + _id
1627
+ * const projected = deriveProjectedSchema(userSchema, { name: 1, email: 1 })
1628
+ *
1629
+ * // Exclusion: drops email
1630
+ * const excluded = deriveProjectedSchema(userSchema, { email: 0 })
1631
+ * ```
1632
+ */
1633
+ declare function deriveProjectedSchema(schema: z.ZodObject<z.core.$ZodShape>, projection: Record<string, 0 | 1 | boolean>): z.ZodObject<z.core.$ZodShape>;
1634
+
1635
+ /**
1636
+ * Typed return value from {@link PopulateRefBuilder.project}.
1637
+ *
1638
+ * Carries the projection type `P` for compile-time narrowing in populate calls.
1639
+ * Callers never construct this directly — it is returned by the builder and
1640
+ * consumed by the `.populate()` overload to narrow the result type.
1641
+ *
1642
+ * @example
1643
+ * ```ts
1644
+ * // P is inferred as { name: 1; email: 1 } — post.authorId is narrowed accordingly
1645
+ * const post = await posts.findOne({}).populate('authorId', (b) => b.project({ name: 1, email: 1 }))
1646
+ * ```
1647
+ */
1648
+ type PopulateProjectionConfig<P> = {
1649
+ readonly projection: P;
1650
+ };
1651
+ /**
1652
+ * Fluent builder passed to populate projection callbacks.
1653
+ *
1654
+ * Call `.project()` to specify which fields to fetch from the referenced collection.
1655
+ * The projection type `P` flows back to the `.populate()` overload to narrow the
1656
+ * result type at compile time.
1657
+ *
1658
+ * @typeParam TDoc - The referenced document type (e.g. `InferDocument<typeof Users>`).
1659
+ *
1660
+ * @example
1661
+ * ```ts
1662
+ * posts.findOne({}).populate('authorId', (b) => b.project({ name: 1, email: 1 }))
1663
+ * // post.authorId is narrowed to { _id: ObjectId; name: string; email: string }
1664
+ * ```
1665
+ */
1666
+ declare class PopulateRefBuilder<TDoc> {
1292
1667
  /**
1293
- * Set the sort order for the query.
1668
+ * Declare a projection to apply when fetching the referenced documents.
1294
1669
  *
1295
- * Only top-level document fields are accepted as sort keys.
1296
- * Values must be `1` (ascending) or `-1` (descending).
1670
+ * Supported: inclusion (`{ name: 1 }`), exclusion (`{ email: 0 }`), or
1671
+ * `_id` suppression (`{ name: 1, _id: 0 }`).
1297
1672
  *
1298
- * @param spec - Sort specification mapping field names to sort direction.
1299
- * @returns `this` for chaining.
1673
+ * @param projection - MongoDB-style inclusion or exclusion projection.
1674
+ * @returns A config object carrying the projection type for compile-time narrowing.
1300
1675
  *
1301
1676
  * @example
1302
1677
  * ```ts
1303
- * find(users, {}).sort({ name: 1, age: -1 }).toArray()
1678
+ * (b) => b.project({ name: 1, email: 1 })
1679
+ * (b) => b.project({ password: 0 })
1304
1680
  * ```
1305
1681
  */
1306
- sort(spec: TypedSort<InferDocument<TDef>>): this;
1307
- /**
1682
+ project<P extends TypedProjection<TDoc>>(projection: P): PopulateProjectionConfig<P>;
1683
+ }
1684
+
1685
+ /**
1686
+ * Resolve the populated type for a ref field, preserving wrappers.
1687
+ *
1688
+ * Transforms the field type from ID to document based on the Zod wrapper:
1689
+ * - `ObjectId` → `InferDocument<Target>`
1690
+ * - `ObjectId[]` → `InferDocument<Target>[]`
1691
+ * - `ObjectId | undefined` → `InferDocument<Target> | undefined`
1692
+ * - `ObjectId | null` → `InferDocument<Target> | null`
1693
+ *
1694
+ * @example
1695
+ * ```ts
1696
+ * type Author = PopulateField<typeof Posts, 'authorId'>
1697
+ * // ^? { _id: ObjectId; name: string; email: string; companyId: ObjectId }
1698
+ *
1699
+ * type Cats = PopulateField<typeof Posts, 'categoryIds'>
1700
+ * // ^? { _id: ObjectId; title: string }[]
1701
+ * ```
1702
+ */
1703
+ type PopulateField<TDef extends AnyCollection, K extends RefFields<TDef>> = TDef['shape'][K] extends z.ZodArray<infer _E> ? InferDocument<ExtractRefCollection<TDef, K>>[] : TDef['shape'][K] extends z.ZodOptional<infer _U> ? InferDocument<ExtractRefCollection<TDef, K>> | undefined : TDef['shape'][K] extends z.ZodNullable<infer _U> ? InferDocument<ExtractRefCollection<TDef, K>> | null : InferDocument<ExtractRefCollection<TDef, K>>;
1704
+ /**
1705
+ * Document type with specified ref fields replaced by their populated types.
1706
+ *
1707
+ * @example
1708
+ * ```ts
1709
+ * type PostWithAuthor = Populated<typeof Posts, 'authorId'>
1710
+ * // ^? { _id: ObjectId; title: string; authorId: User; categoryIds: ObjectId[]; ... }
1711
+ *
1712
+ * type PostFull = Populated<typeof Posts, 'authorId' | 'categoryIds'>
1713
+ * // ^? { ...; authorId: User; categoryIds: Category[]; ... }
1714
+ * ```
1715
+ */
1716
+ type Populated<TDef extends AnyCollection, K extends RefFields<TDef>> = Prettify<Omit<InferDocument<TDef>, K> & {
1717
+ [P in K]: PopulateField<TDef, P>;
1718
+ }>;
1719
+ /**
1720
+ * Apply a single populate step to the output type.
1721
+ *
1722
+ * Without rename: replaces the field type in-place.
1723
+ * With rename: removes the old key and adds the new key with the populated type.
1724
+ *
1725
+ * @example
1726
+ * ```ts
1727
+ * // No rename: authorId stays, type changes to User
1728
+ * type A = ApplyPopulate<PostDoc, typeof Posts, 'authorId'>
1729
+ *
1730
+ * // Rename: authorId removed, author added with User type
1731
+ * type B = ApplyPopulate<PostDoc, typeof Posts, 'authorId', 'author'>
1732
+ * ```
1733
+ */
1734
+ type ApplyPopulate<T, TDef extends AnyCollection, K extends RefFields<TDef>, TAs extends string = K> = Prettify<Omit<T, K> & {
1735
+ [P in TAs]: PopulateField<TDef, K>;
1736
+ }>;
1737
+ /**
1738
+ * Resolve the projected populated type for a ref field, preserving wrappers.
1739
+ *
1740
+ * Like {@link PopulateField} but applies `ProjectionResult<T, P>` to narrow
1741
+ * the document type to only the projected fields.
1742
+ *
1743
+ * - `ObjectId` → `ProjectionResult<InferDocument<Target>, P>`
1744
+ * - `ObjectId[]` → `ProjectionResult<InferDocument<Target>, P>[]`
1745
+ * - `ObjectId | undefined` → `ProjectionResult<InferDocument<Target>, P> | undefined`
1746
+ * - `ObjectId | null` → `ProjectionResult<InferDocument<Target>, P> | null`
1747
+ *
1748
+ * @example
1749
+ * ```ts
1750
+ * type Author = PopulateFieldProjected<typeof Posts, 'authorId', { name: 1; email: 1 }>
1751
+ * // ^? { _id: ObjectId; name: string; email: string }
1752
+ * ```
1753
+ */
1754
+ type PopulateFieldProjected<TDef extends AnyCollection, K extends RefFields<TDef>, P> = TDef['shape'][K] extends z.ZodArray<infer _E> ? ProjectionResult<InferDocument<ExtractRefCollection<TDef, K>>, P>[] : TDef['shape'][K] extends z.ZodOptional<infer _U> ? ProjectionResult<InferDocument<ExtractRefCollection<TDef, K>>, P> | undefined : TDef['shape'][K] extends z.ZodNullable<infer _U> ? ProjectionResult<InferDocument<ExtractRefCollection<TDef, K>>, P> | null : ProjectionResult<InferDocument<ExtractRefCollection<TDef, K>>, P>;
1755
+ /**
1756
+ * Apply a projected populate step to the output type.
1757
+ *
1758
+ * Like {@link ApplyPopulate} but uses {@link PopulateFieldProjected} to narrow
1759
+ * the result to only the projected fields. Use `TAs` to rename the output field.
1760
+ *
1761
+ * @example
1762
+ * ```ts
1763
+ * type Result = ApplyPopulateProjected<PostDoc, typeof Posts, 'authorId', { name: 1; email: 1 }>
1764
+ * // ^? { ...; authorId: { _id: ObjectId; name: string; email: string } }
1765
+ * ```
1766
+ */
1767
+ type ApplyPopulateProjected<T, TDef extends AnyCollection, K extends RefFields<TDef>, P, TAs extends string = K> = Prettify<Omit<T, K> & {
1768
+ [F in TAs]: PopulateFieldProjected<TDef, K, P>;
1769
+ }>;
1770
+ /**
1771
+ * Apply a populate at an arbitrary nesting depth.
1772
+ *
1773
+ * Splits the dot-path and recursively navigates into the output type.
1774
+ * If a parent field is an array, the element type is transformed.
1775
+ *
1776
+ * @example
1777
+ * ```ts
1778
+ * // Nested: author.companyId → author.company
1779
+ * type Result = DeepPopulate<PostWithAuthor, 'author.companyId', 'companyId', 'company', Company>
1780
+ * ```
1781
+ */
1782
+ type DeepPopulate<T, Path extends string, LeafField extends string, TAs extends string, PopType> = Path extends `${infer Parent}.${infer Rest}` ? Parent extends keyof T ? T[Parent] extends (infer E)[] ? Prettify<Omit<T, Parent> & {
1783
+ [P in Parent]: DeepPopulate<E, Rest, LeafField, TAs, PopType>[];
1784
+ }> : Prettify<Omit<T, Parent> & {
1785
+ [P in Parent]: DeepPopulate<T[Parent], Rest, LeafField, TAs, PopType>;
1786
+ }> : never : Prettify<Omit<T, LeafField> & {
1787
+ [P in TAs]: PopType;
1788
+ }>;
1789
+ /**
1790
+ * Valid nested populate paths given the current TPopMap.
1791
+ *
1792
+ * Produces a union of `parentPath.refField` strings where `parentPath`
1793
+ * is a key in TPopMap and `refField` is a ref-bearing field in that
1794
+ * parent's collection definition.
1795
+ *
1796
+ * @example
1797
+ * ```ts
1798
+ * // If TPopMap = { author: typeof Users }, produces:
1799
+ * // 'author.companyId' | 'author.<other ref fields>'
1800
+ * type Paths = NestedRefPath<{ author: typeof Users }>
1801
+ * ```
1802
+ */
1803
+ type NestedRefPath<TPopMap extends Record<string, AnyCollection>> = {
1804
+ [Path in keyof TPopMap & string]: `${Path}.${RefFields<TPopMap[Path]>}`;
1805
+ }[keyof TPopMap & string];
1806
+ /**
1807
+ * Runtime data for a single populate step.
1808
+ *
1809
+ * Built by each `.populate()` call and consumed by `executePopulate()`.
1810
+ */
1811
+ type PopulateStep = {
1812
+ /** The original field path from the root collection. */
1813
+ readonly originalPath: string;
1814
+ /** The leaf field name in the target shape. */
1815
+ readonly leafField: string;
1816
+ /** The output field name (renamed or original). */
1817
+ readonly as: string;
1818
+ /** Parent path in the output document for nesting, or undefined for top-level. */
1819
+ readonly parentOutputPath: string | undefined;
1820
+ /** The target collection definition resolved from RefMarker metadata. */
1821
+ readonly targetCollection: AnyCollection;
1822
+ /** Whether the field is an array ref. */
1823
+ readonly isArray: boolean;
1824
+ /** MongoDB projection spec to pass to the `$in` query. Omit to fetch the full document. */
1825
+ readonly projection?: Record<string, 0 | 1 | boolean>;
1826
+ };
1827
+
1828
+ /**
1829
+ * Extract the parent segment from a dot-separated path.
1830
+ *
1831
+ * @example
1832
+ * ```ts
1833
+ * type P = ExtractParent<'author.companyId'> // 'author'
1834
+ * ```
1835
+ */
1836
+ type ExtractParent$1<T extends string> = T extends `${infer P}.${string}` ? P : never;
1837
+ /**
1838
+ * Extract the leaf segment from a dot-separated path.
1839
+ *
1840
+ * @example
1841
+ * ```ts
1842
+ * type L = ExtractLeaf<'author.companyId'> // 'companyId'
1843
+ * ```
1844
+ */
1845
+ type ExtractLeaf$1<T extends string> = T extends `${string}.${infer L}` ? L : never;
1846
+ /**
1847
+ * Cursor with chained populate steps, wrapping a {@link TypedFindCursor}.
1848
+ *
1849
+ * Created by calling `.populate()` on a `TypedFindCursor`. Exposes only
1850
+ * `.populate()` (for additional fields) and terminal methods (`toArray`,
1851
+ * async iteration). Cursor modifiers (`sort`, `skip`, `limit`) are not
1852
+ * available -- they must be called before `.populate()`.
1853
+ *
1854
+ * @typeParam TDef - The root collection definition type.
1855
+ * @typeParam TOutput - The current output document type after populate transforms.
1856
+ * @typeParam TPopMap - Map of populated alias names to their collection definitions,
1857
+ * used to resolve nested populate paths.
1858
+ *
1859
+ * @example
1860
+ * ```ts
1861
+ * const posts = await db.use(Posts)
1862
+ * .find({ published: true })
1863
+ * .sort({ createdAt: -1 })
1864
+ * .limit(10)
1865
+ * .populate('authorId', 'author')
1866
+ * .populate('categoryIds')
1867
+ * .toArray()
1868
+ * ```
1869
+ */
1870
+ declare class PopulateCursor<TDef extends AnyCollection, TOutput, TPopMap extends Record<string, AnyCollection> = Record<string, never>> {
1871
+ private readonly cursor;
1872
+ private readonly definition;
1873
+ private readonly steps;
1874
+ private readonly nativeCollection;
1875
+ /** @internal */
1876
+ constructor(cursor: TypedFindCursor<TDef>, definition: TDef, steps: readonly PopulateStep[], nativeCollection: Collection<InferDocument<TDef>>);
1877
+ /**
1878
+ * Populate a top-level ref field, keeping the original field name.
1879
+ *
1880
+ * @param field - A ref-bearing field name on the root collection.
1881
+ * @returns A new cursor with the field type replaced by the referenced document.
1882
+ *
1883
+ * @example
1884
+ * ```ts
1885
+ * const posts = await db.use(Posts)
1886
+ * .find({})
1887
+ * .populate('authorId')
1888
+ * .toArray()
1889
+ * // posts[0].authorId is now a User document instead of ObjectId
1890
+ * ```
1891
+ */
1892
+ populate<K extends RefFields<TDef>>(field: K): PopulateCursor<TDef, ApplyPopulate<TOutput, TDef, K>, TPopMap & {
1893
+ [P in K]: ExtractRefCollection<TDef, K>;
1894
+ }>;
1895
+ /**
1896
+ * Populate a top-level ref field with a projection, keeping the original field name.
1897
+ *
1898
+ * @param field - A ref-bearing field name on the root collection.
1899
+ * @param configure - Callback receiving a builder; call `.project()` to set the projection.
1900
+ * @returns A new cursor with the field type narrowed to only the projected fields.
1901
+ *
1902
+ * @example
1903
+ * ```ts
1904
+ * const posts = await db.use(Posts)
1905
+ * .find({})
1906
+ * .populate('authorId', (b) => b.project({ name: 1, email: 1 }))
1907
+ * .toArray()
1908
+ * // posts[0].authorId is { _id: ObjectId; name: string; email: string }
1909
+ * ```
1910
+ */
1911
+ populate<K extends RefFields<TDef>, P>(field: K, configure: (b: PopulateRefBuilder<InferDocument<ExtractRefCollection<TDef, K>>>) => PopulateProjectionConfig<P>): PopulateCursor<TDef, ApplyPopulateProjected<TOutput, TDef, K, P>, TPopMap & {
1912
+ [F in K]: ExtractRefCollection<TDef, K>;
1913
+ }>;
1914
+ /**
1915
+ * Populate a top-level ref field and rename it in the output.
1916
+ *
1917
+ * @param field - A ref-bearing field name on the root collection.
1918
+ * @param as - The new field name in the output document.
1919
+ * @returns A new cursor with the old field removed and the new field added.
1920
+ *
1921
+ * @example
1922
+ * ```ts
1923
+ * const posts = await db.use(Posts)
1924
+ * .find({})
1925
+ * .populate('authorId', 'author')
1926
+ * .toArray()
1927
+ * // posts[0].author is a User document; posts[0].authorId no longer exists
1928
+ * ```
1929
+ */
1930
+ populate<K extends RefFields<TDef>, TAs extends string>(field: K, as: TAs): PopulateCursor<TDef, ApplyPopulate<TOutput, TDef, K, TAs>, TPopMap & {
1931
+ [P in TAs]: ExtractRefCollection<TDef, K>;
1932
+ }>;
1933
+ /**
1934
+ * Populate a nested ref field, keeping the leaf field name.
1935
+ *
1936
+ * The parent path must have been populated in a previous `.populate()` call.
1937
+ *
1938
+ * @param path - A dot-separated path like `'author.companyId'`.
1939
+ * @returns A new cursor with the nested field type replaced.
1940
+ *
1941
+ * @example
1942
+ * ```ts
1943
+ * const posts = await db.use(Posts)
1944
+ * .find({})
1945
+ * .populate('authorId', 'author')
1946
+ * .populate('author.companyId')
1947
+ * .toArray()
1948
+ * // posts[0].author.companyId is now a Company document
1949
+ * ```
1950
+ */
1951
+ populate<TPath extends NestedRefPath<TPopMap>>(path: TPath): PopulateCursor<TDef, DeepPopulate<TOutput, `${ExtractParent$1<TPath>}.${ExtractLeaf$1<TPath>}`, ExtractLeaf$1<TPath>, ExtractLeaf$1<TPath>, PopulateField<TPopMap[ExtractParent$1<TPath>], ExtractLeaf$1<TPath> & RefFields<TPopMap[ExtractParent$1<TPath>]>>>, TPopMap & {
1952
+ [P in `${ExtractParent$1<TPath>}.${ExtractLeaf$1<TPath>}`]: ExtractRefCollection<TPopMap[ExtractParent$1<TPath>], ExtractLeaf$1<TPath> & RefFields<TPopMap[ExtractParent$1<TPath>]>>;
1953
+ }>;
1954
+ /**
1955
+ * Populate a nested ref field and rename the leaf in the output.
1956
+ *
1957
+ * The parent path must have been populated in a previous `.populate()` call.
1958
+ *
1959
+ * @param path - A dot-separated path like `'author.companyId'`.
1960
+ * @param as - The new name for the leaf field in the output.
1961
+ * @returns A new cursor with the nested field renamed and typed.
1962
+ *
1963
+ * @example
1964
+ * ```ts
1965
+ * const posts = await db.use(Posts)
1966
+ * .find({})
1967
+ * .populate('authorId', 'author')
1968
+ * .populate('author.companyId', 'company')
1969
+ * .toArray()
1970
+ * // posts[0].author.company is a Company document
1971
+ * ```
1972
+ */
1973
+ populate<TPath extends NestedRefPath<TPopMap>, TAs extends string>(path: TPath, as: TAs): PopulateCursor<TDef, DeepPopulate<TOutput, `${ExtractParent$1<TPath>}.${TAs}`, ExtractLeaf$1<TPath>, TAs, PopulateField<TPopMap[ExtractParent$1<TPath>], ExtractLeaf$1<TPath> & RefFields<TPopMap[ExtractParent$1<TPath>]>>>, TPopMap & {
1974
+ [P in `${ExtractParent$1<TPath>}.${TAs}`]: ExtractRefCollection<TPopMap[ExtractParent$1<TPath>], ExtractLeaf$1<TPath> & RefFields<TPopMap[ExtractParent$1<TPath>]>>;
1975
+ }>;
1976
+ /**
1977
+ * Execute the query and return all matching documents as a populated array.
1978
+ *
1979
+ * Fetches all documents from the underlying cursor, then applies populate
1980
+ * steps in order using batch `$in` queries (no N+1 problem).
1981
+ *
1982
+ * @returns Array of populated documents.
1983
+ *
1984
+ * @example
1985
+ * ```ts
1986
+ * const posts = await db.use(Posts)
1987
+ * .find({})
1988
+ * .populate('authorId', 'author')
1989
+ * .toArray()
1990
+ * ```
1991
+ */
1992
+ toArray(): Promise<TOutput[]>;
1993
+ /**
1994
+ * Async iterator for streaming populated documents.
1995
+ *
1996
+ * Fetches all documents first (populate requires the full batch for
1997
+ * efficient `$in` queries), then yields results one at a time.
1998
+ *
1999
+ * @yields Populated documents one at a time.
2000
+ *
2001
+ * @example
2002
+ * ```ts
2003
+ * for await (const post of db.use(Posts).find({}).populate('authorId', 'author')) {
2004
+ * console.log(post.author.name)
2005
+ * }
2006
+ * ```
2007
+ */
2008
+ [Symbol.asyncIterator](): AsyncGenerator<TOutput>;
2009
+ }
2010
+ /**
2011
+ * Create a new {@link PopulateCursor} from a {@link TypedFindCursor}.
2012
+ *
2013
+ * This factory function exists to break the circular import between
2014
+ * `query/cursor.ts` and `populate/cursor.ts`. The `TypedFindCursor.populate()`
2015
+ * method uses a lazy `require()` to call this function at runtime.
2016
+ *
2017
+ * @param cursor - The typed find cursor to wrap.
2018
+ * @param definition - The collection definition.
2019
+ * @param steps - Initial populate steps (typically empty).
2020
+ * @returns A new PopulateCursor instance.
2021
+ *
2022
+ * @example
2023
+ * ```ts
2024
+ * const popCursor = createPopulateCursor(typedCursor, Posts, [])
2025
+ * ```
2026
+ */
2027
+ declare function createPopulateCursor<TDef extends AnyCollection>(cursor: TypedFindCursor<TDef>, definition: TDef, steps: readonly PopulateStep[]): PopulateCursor<TDef, InferDocument<TDef>>;
2028
+
2029
+ /**
2030
+ * Type-safe sort specification for a document type.
2031
+ *
2032
+ * Constrains sort keys to top-level fields of `T` with direction `1` (ascending)
2033
+ * or `-1` (descending). Dot-path sorts deferred to v1.0.
2034
+ *
2035
+ * @example
2036
+ * ```ts
2037
+ * const sort: TypedSort<User> = { name: 1, createdAt: -1 }
2038
+ * ```
2039
+ */
2040
+ type TypedSort<T> = Partial<Record<keyof T & string, 1 | -1>>;
2041
+ /**
2042
+ * Type-safe cursor wrapping MongoDB's `FindCursor`.
2043
+ *
2044
+ * Provides chainable query modifiers (`sort`, `skip`, `limit`, `hint`) that return
2045
+ * `this` for fluent chaining, and terminal methods (`toArray`,
2046
+ * `[Symbol.asyncIterator]`) that validate each document against the
2047
+ * collection's Zod schema before returning.
2048
+ *
2049
+ * Created by {@link find} — do not construct directly.
2050
+ *
2051
+ * @typeParam TDef - The collection definition type, used to infer the document type.
2052
+ * @typeParam TIndexNames - Union of declared index names accepted by `.hint()`.
2053
+ * @typeParam TOutput - The output document type, narrowed by `.project()`.
2054
+ *
2055
+ * @example
2056
+ * ```ts
2057
+ * const docs = await find(users, { role: 'admin' })
2058
+ * .sort({ name: 1 })
2059
+ * .limit(10)
2060
+ * .toArray()
2061
+ * ```
2062
+ */
2063
+ declare class TypedFindCursor<TDef extends AnyCollection, TIndexNames extends string = string, TOutput = InferDocument<TDef>> {
2064
+ /** @internal */
2065
+ private cursor;
2066
+ /** @internal */
2067
+ readonly definition: TDef;
2068
+ /** @internal */
2069
+ private schema;
2070
+ /** @internal */
2071
+ private collectionName;
2072
+ /** @internal */
2073
+ private mode;
2074
+ /** @internal */
2075
+ readonly nativeCollection: Collection<InferDocument<TDef>>;
2076
+ /** @internal */
2077
+ private readonly filter;
2078
+ /** @internal */
2079
+ private readonly session;
2080
+ /** @internal */
2081
+ private sortSpec;
2082
+ /** @internal */
2083
+ private projectedSchema;
2084
+ /** @internal */
2085
+ constructor(cursor: FindCursor<InferDocument<TDef>>, definition: TDef, mode: ValidationMode | false, nativeCollection: Collection<InferDocument<TDef>>, filter: any, session?: ClientSession);
2086
+ /**
2087
+ * Set the sort order for the query.
2088
+ *
2089
+ * Only top-level document fields are accepted as sort keys.
2090
+ * Values must be `1` (ascending) or `-1` (descending).
2091
+ *
2092
+ * @param spec - Sort specification mapping field names to sort direction.
2093
+ * @returns `this` for chaining.
2094
+ *
2095
+ * @example
2096
+ * ```ts
2097
+ * find(users, {}).sort({ name: 1, age: -1 }).toArray()
2098
+ * ```
2099
+ */
2100
+ sort(spec: TypedSort<InferDocument<TDef>>): this;
2101
+ /**
1308
2102
  * Skip the first `n` documents in the result set.
1309
2103
  *
1310
2104
  * @param n - Number of documents to skip.
@@ -1348,6 +2142,100 @@ declare class TypedFindCursor<TDef extends AnyCollection, TIndexNames extends st
1348
2142
  * ```
1349
2143
  */
1350
2144
  hint(indexName: TIndexNames): this;
2145
+ /**
2146
+ * Apply a projection to narrow the returned fields.
2147
+ *
2148
+ * Inclusion projections (`{ name: 1 }`) return only the specified fields
2149
+ * plus `_id` (unless `_id: 0`). Exclusion projections (`{ email: 0 }`)
2150
+ * return all fields except those excluded.
2151
+ *
2152
+ * The cursor's output type is narrowed at compile time. A derived Zod
2153
+ * schema is built for runtime validation of the projected fields.
2154
+ *
2155
+ * Projects from the original document type, not from a previous projection.
2156
+ * Calling `.project()` twice overrides the previous projection.
2157
+ *
2158
+ * @param spec - Type-safe projection document.
2159
+ * @returns A new cursor with the narrowed output type.
2160
+ *
2161
+ * @example
2162
+ * ```ts
2163
+ * const names = await find(users, {})
2164
+ * .project({ name: 1 })
2165
+ * .sort({ name: 1 })
2166
+ * .toArray()
2167
+ * // names[0].name ✓
2168
+ * // names[0].email TS error
2169
+ * ```
2170
+ */
2171
+ project<P extends TypedProjection<InferDocument<TDef>>>(spec: P): TypedFindCursor<TDef, TIndexNames, Prettify<ProjectionResult<InferDocument<TDef>, P>>>;
2172
+ /**
2173
+ * Populate a top-level ref field, keeping the original field name.
2174
+ *
2175
+ * Transitions to a {@link PopulateCursor} that only exposes `.populate()`
2176
+ * and terminal methods. Cursor modifiers (`sort`, `skip`, `limit`) must
2177
+ * be called before `.populate()`.
2178
+ *
2179
+ * @param field - A ref-bearing field name on the root collection.
2180
+ * @returns A populate cursor with the field type replaced by the referenced document.
2181
+ *
2182
+ * @example
2183
+ * ```ts
2184
+ * const posts = await db.use(Posts)
2185
+ * .find({})
2186
+ * .populate('authorId')
2187
+ * .toArray()
2188
+ * ```
2189
+ */
2190
+ populate<K extends RefFields<TDef>>(field: K): PopulateCursor<TDef, ApplyPopulate<TOutput, TDef, K>, {
2191
+ [P in K]: ExtractRefCollection<TDef, K>;
2192
+ }>;
2193
+ /**
2194
+ * Populate a top-level ref field with a projection, keeping the original field name.
2195
+ *
2196
+ * Transitions to a {@link PopulateCursor}. Cursor modifiers (`sort`, `skip`, `limit`)
2197
+ * must be called before `.populate()`.
2198
+ *
2199
+ * @param field - A ref-bearing field name on the root collection.
2200
+ * @param configure - Callback receiving a builder; call `.project()` to set the projection.
2201
+ * @returns A populate cursor with the field type narrowed to only the projected fields.
2202
+ *
2203
+ * @example
2204
+ * ```ts
2205
+ * const posts = await db.use(Posts)
2206
+ * .find({})
2207
+ * .populate('authorId', (b) => b.project({ name: 1, email: 1 }))
2208
+ * .toArray()
2209
+ * // posts[0].authorId is { _id: ObjectId; name: string; email: string }
2210
+ * ```
2211
+ */
2212
+ populate<K extends RefFields<TDef>, P>(field: K, configure: (b: PopulateRefBuilder<InferDocument<ExtractRefCollection<TDef, K>>>) => PopulateProjectionConfig<P>): PopulateCursor<TDef, ApplyPopulateProjected<TOutput, TDef, K, P>, {
2213
+ [F in K]: ExtractRefCollection<TDef, K>;
2214
+ }>;
2215
+ /**
2216
+ * Populate a top-level ref field and rename it in the output.
2217
+ *
2218
+ * Transitions to a {@link PopulateCursor} that only exposes `.populate()`
2219
+ * and terminal methods. Cursor modifiers (`sort`, `skip`, `limit`) must
2220
+ * be called before `.populate()`.
2221
+ *
2222
+ * @param field - A ref-bearing field name on the root collection.
2223
+ * @param as - The new field name in the output document.
2224
+ * @returns A populate cursor with the old field removed and the new field added.
2225
+ *
2226
+ * @example
2227
+ * ```ts
2228
+ * const posts = await db.use(Posts)
2229
+ * .find({})
2230
+ * .sort({ createdAt: -1 })
2231
+ * .limit(10)
2232
+ * .populate('authorId', 'author')
2233
+ * .toArray()
2234
+ * ```
2235
+ */
2236
+ populate<K extends RefFields<TDef>, TAs extends string>(field: K, as: TAs): PopulateCursor<TDef, ApplyPopulate<TOutput, TDef, K, TAs>, {
2237
+ [P in TAs]: ExtractRefCollection<TDef, K>;
2238
+ }>;
1351
2239
  /**
1352
2240
  * Execute the query with offset-based pagination, returning a page of documents
1353
2241
  * with total count and navigation metadata.
@@ -1367,7 +2255,7 @@ declare class TypedFindCursor<TDef extends AnyCollection, TIndexNames extends st
1367
2255
  * console.log(page.total, page.totalPages, page.hasNext)
1368
2256
  * ```
1369
2257
  */
1370
- paginate(opts: OffsetPaginateOptions): Promise<OffsetPage<InferDocument<TDef>>>;
2258
+ paginate(opts: OffsetPaginateOptions): Promise<OffsetPage<TOutput>>;
1371
2259
  /**
1372
2260
  * Execute the query with cursor-based pagination, returning a page of documents
1373
2261
  * with opaque cursors for forward/backward navigation.
@@ -1388,7 +2276,7 @@ declare class TypedFindCursor<TDef extends AnyCollection, TIndexNames extends st
1388
2276
  * .paginate({ cursor: first.endCursor, limit: 10 })
1389
2277
  * ```
1390
2278
  */
1391
- paginate(opts: CursorPaginateOptions): Promise<CursorPage<InferDocument<TDef>>>;
2279
+ paginate(opts: CursorPaginateOptions): Promise<CursorPage<TOutput>>;
1392
2280
  /** @internal Offset pagination implementation. */
1393
2281
  private offsetPaginate;
1394
2282
  /** @internal Cursor pagination implementation. */
@@ -1407,7 +2295,7 @@ declare class TypedFindCursor<TDef extends AnyCollection, TIndexNames extends st
1407
2295
  * const admins = await find(users, { role: 'admin' }).toArray()
1408
2296
  * ```
1409
2297
  */
1410
- toArray(): Promise<InferDocument<TDef>[]>;
2298
+ toArray(): Promise<TOutput[]>;
1411
2299
  /**
1412
2300
  * Async iterator for streaming documents one at a time.
1413
2301
  *
@@ -1424,17 +2312,33 @@ declare class TypedFindCursor<TDef extends AnyCollection, TIndexNames extends st
1424
2312
  * }
1425
2313
  * ```
1426
2314
  */
1427
- [Symbol.asyncIterator](): AsyncGenerator<InferDocument<TDef>>;
2315
+ [Symbol.asyncIterator](): AsyncGenerator<TOutput>;
1428
2316
  /** @internal Validate a single raw document against the schema. */
1429
2317
  private validateDoc;
1430
2318
  }
1431
2319
 
1432
2320
  /**
1433
- * Options for {@link findOne} and {@link findOneOrThrow}.
2321
+ * Options for {@link findOne} and {@link findOneOrThrow} without projection.
1434
2322
  */
1435
2323
  type FindOneOptions = {
1436
- /** MongoDB projection include (`1`) or exclude (`0`) fields. Typed projections deferred to v1.0. */
1437
- project?: Record<string, 0 | 1>;
2324
+ /** Override the collection-level validation mode, or `false` to skip validation entirely. */
2325
+ validate?: ValidationMode | false;
2326
+ };
2327
+ /**
2328
+ * Options for projected {@link findOne} and {@link findOneOrThrow} queries.
2329
+ *
2330
+ * @typeParam T - The document type.
2331
+ * @typeParam P - The projection document type.
2332
+ *
2333
+ * @example
2334
+ * ```ts
2335
+ * const user = await findOne(users, { name: 'Ada' }, { project: { name: 1 } })
2336
+ * // ^? { _id: ObjectId; name: string } | null
2337
+ * ```
2338
+ */
2339
+ type FindOneProjectionOptions<T, P extends TypedProjection<T>> = {
2340
+ /** Type-safe MongoDB projection — include (`1 | true`) or exclude (`0 | false`) fields. */
2341
+ project: P;
1438
2342
  /** Override the collection-level validation mode, or `false` to skip validation entirely. */
1439
2343
  validate?: ValidationMode | false;
1440
2344
  };
@@ -1447,7 +2351,7 @@ type FindOneOptions = {
1447
2351
  *
1448
2352
  * @param handle - The collection handle to query.
1449
2353
  * @param filter - Type-safe filter to match documents.
1450
- * @param options - Optional projection and validation overrides.
2354
+ * @param options - Optional validation overrides.
1451
2355
  * @returns The matched document, or `null` if no document matches.
1452
2356
  * @throws {ZodmonValidationError} When the fetched document fails schema validation in strict mode.
1453
2357
  *
@@ -1458,6 +2362,26 @@ type FindOneOptions = {
1458
2362
  * ```
1459
2363
  */
1460
2364
  declare function findOne<TDef extends AnyCollection>(handle: CollectionHandle<TDef>, filter: TypedFilter<InferDocument<TDef>>, options?: FindOneOptions): Promise<InferDocument<TDef> | null>;
2365
+ /**
2366
+ * Find a single document matching the filter with a type-safe projection.
2367
+ *
2368
+ * Returns only the fields specified by the projection. The return type is
2369
+ * narrowed to reflect which fields are included or excluded.
2370
+ *
2371
+ * @param handle - The collection handle to query.
2372
+ * @param filter - Type-safe filter to match documents.
2373
+ * @param options - Projection and optional validation overrides.
2374
+ * @returns The projected document, or `null` if no document matches.
2375
+ * @throws {ZodmonValidationError} When the fetched document fails schema validation in strict mode.
2376
+ *
2377
+ * @example
2378
+ * ```ts
2379
+ * const user = await findOne(users, { name: 'Ada' }, { project: { name: 1 } })
2380
+ * if (user) console.log(user.name) // typed as string
2381
+ * // user.role would be a type error — not in the projection
2382
+ * ```
2383
+ */
2384
+ declare function findOne<TDef extends AnyCollection, P extends TypedProjection<InferDocument<TDef>>>(handle: CollectionHandle<TDef>, filter: TypedFilter<InferDocument<TDef>>, options: FindOneProjectionOptions<InferDocument<TDef>, P>): Promise<Prettify<ProjectionResult<InferDocument<TDef>, P>> | null>;
1461
2385
  /**
1462
2386
  * Find a single document matching the filter, or throw if none exists.
1463
2387
  *
@@ -1466,7 +2390,7 @@ declare function findOne<TDef extends AnyCollection>(handle: CollectionHandle<TD
1466
2390
  *
1467
2391
  * @param handle - The collection handle to query.
1468
2392
  * @param filter - Type-safe filter to match documents.
1469
- * @param options - Optional projection and validation overrides.
2393
+ * @param options - Optional validation overrides.
1470
2394
  * @returns The matched document (never null).
1471
2395
  * @throws {ZodmonNotFoundError} When no document matches the filter.
1472
2396
  * @throws {ZodmonValidationError} When the fetched document fails schema validation in strict mode.
@@ -1478,6 +2402,28 @@ declare function findOne<TDef extends AnyCollection>(handle: CollectionHandle<TD
1478
2402
  * ```
1479
2403
  */
1480
2404
  declare function findOneOrThrow<TDef extends AnyCollection>(handle: CollectionHandle<TDef>, filter: TypedFilter<InferDocument<TDef>>, options?: FindOneOptions): Promise<InferDocument<TDef>>;
2405
+ /**
2406
+ * Find a single document matching the filter with a type-safe projection, or throw if none exists.
2407
+ *
2408
+ * Returns only the fields specified by the projection. The return type is
2409
+ * narrowed to reflect which fields are included or excluded. Throws
2410
+ * {@link ZodmonNotFoundError} instead of returning `null`.
2411
+ *
2412
+ * @param handle - The collection handle to query.
2413
+ * @param filter - Type-safe filter to match documents.
2414
+ * @param options - Projection and optional validation overrides.
2415
+ * @returns The projected document (never null).
2416
+ * @throws {ZodmonNotFoundError} When no document matches the filter.
2417
+ * @throws {ZodmonValidationError} When the fetched document fails schema validation in strict mode.
2418
+ *
2419
+ * @example
2420
+ * ```ts
2421
+ * const user = await findOneOrThrow(users, { name: 'Ada' }, { project: { name: 1 } })
2422
+ * console.log(user.name) // typed as string
2423
+ * // user.role would be a type error — not in the projection
2424
+ * ```
2425
+ */
2426
+ declare function findOneOrThrow<TDef extends AnyCollection, P extends TypedProjection<InferDocument<TDef>>>(handle: CollectionHandle<TDef>, filter: TypedFilter<InferDocument<TDef>>, options: FindOneProjectionOptions<InferDocument<TDef>, P>): Promise<Prettify<ProjectionResult<InferDocument<TDef>, P>>>;
1481
2427
  /**
1482
2428
  * Options for {@link find}.
1483
2429
  */
@@ -1485,6 +2431,25 @@ type FindOptions = {
1485
2431
  /** Override the collection-level validation mode, or `false` to skip validation entirely. */
1486
2432
  validate?: ValidationMode | false;
1487
2433
  };
2434
+ /**
2435
+ * Options for projected {@link find} queries.
2436
+ *
2437
+ * @typeParam T - The document type.
2438
+ * @typeParam P - The projection document type.
2439
+ *
2440
+ * @example
2441
+ * ```ts
2442
+ * const admins = await find(users, { role: 'admin' }, { project: { name: 1 } })
2443
+ * .toArray()
2444
+ * // ^? Array<{ _id: ObjectId; name: string }>
2445
+ * ```
2446
+ */
2447
+ type FindProjectionOptions<T, P extends TypedProjection<T>> = {
2448
+ /** Type-safe MongoDB projection — include (`1 | true`) or exclude (`0 | false`) fields. */
2449
+ project: P;
2450
+ /** Override the collection-level validation mode, or `false` to skip validation entirely. */
2451
+ validate?: ValidationMode | false;
2452
+ };
1488
2453
  /**
1489
2454
  * Find all documents matching the filter, returning a chainable typed cursor.
1490
2455
  *
@@ -1516,17 +2481,29 @@ type FindOptions = {
1516
2481
  * ```
1517
2482
  */
1518
2483
  declare function find<TDef extends AnyCollection>(handle: CollectionHandle<TDef>, filter: TypedFilter<InferDocument<TDef>>, options?: FindOptions): TypedFindCursor<TDef, IndexNames<TDef>>;
1519
-
1520
2484
  /**
1521
- * Extracts the element type from an array type.
2485
+ * Find all documents matching the filter with a type-safe projection, returning a chainable typed cursor.
2486
+ *
2487
+ * The cursor is lazy — no query is executed until a terminal method
2488
+ * (`toArray`, `for await`) is called. The return type is narrowed to
2489
+ * reflect which fields are included or excluded by the projection.
2490
+ *
2491
+ * @param handle - The collection handle to query.
2492
+ * @param filter - Type-safe filter to match documents.
2493
+ * @param options - Projection and optional validation overrides.
2494
+ * @returns A typed cursor whose output type matches the projection.
1522
2495
  *
1523
2496
  * @example
1524
2497
  * ```ts
1525
- * type E = ArrayElement<string[]> // string
1526
- * type N = ArrayElement<number[]> // number
2498
+ * const names = await find(users, { role: 'admin' }, { project: { name: 1 } })
2499
+ * .sort({ name: 1 })
2500
+ * .toArray()
2501
+ * // names[0].name — string
2502
+ * // names[0].role — type error, not in projection
1527
2503
  * ```
1528
2504
  */
1529
- type ArrayElement<T> = T extends ReadonlyArray<infer E> ? E : never;
2505
+ declare function find<TDef extends AnyCollection, P extends TypedProjection<InferDocument<TDef>>>(handle: CollectionHandle<TDef>, filter: TypedFilter<InferDocument<TDef>>, options: FindProjectionOptions<InferDocument<TDef>, P>): TypedFindCursor<TDef, IndexNames<TDef>, Prettify<ProjectionResult<InferDocument<TDef>, P>>>;
2506
+
1530
2507
  /**
1531
2508
  * Fields valid for `$set`, `$setOnInsert`, `$min`, and `$max` operators.
1532
2509
  *
@@ -1541,7 +2518,7 @@ type ArrayElement<T> = T extends ReadonlyArray<infer E> ? E : never;
1541
2518
  type SetFields<T> = {
1542
2519
  [K in keyof T]?: T[K];
1543
2520
  } & {
1544
- [P in DotPaths<T>]?: DotPathType<T, P>;
2521
+ [P in DotSubPaths<T>]?: DotPathValue<T, P>;
1545
2522
  };
1546
2523
  /**
1547
2524
  * Fields valid for the `$inc` operator — only number-typed fields.
@@ -1556,7 +2533,7 @@ type SetFields<T> = {
1556
2533
  type IncFields<T> = {
1557
2534
  [K in keyof T as NonNullable<T[K]> extends number ? K : never]?: number;
1558
2535
  } & {
1559
- [P in DotPaths<T> as DotPathType<T, P> extends number ? P : never]?: number;
2536
+ [P in DotSubPaths<T> as DotPathValue<T, P> extends number ? P : never]?: number;
1560
2537
  };
1561
2538
  /**
1562
2539
  * Modifiers for the `$push` operator.
@@ -1847,59 +2824,310 @@ type SyncIndexesOptions = {
1847
2824
  * options). When `false` (the default), orphaned and stale indexes are
1848
2825
  * reported but left untouched.
1849
2826
  */
1850
- dropOrphaned?: boolean;
1851
- };
1852
- /**
1853
- * Describes an index whose key matches a desired index but whose options differ.
1854
- *
1855
- * Returned in {@link SyncIndexesResult.stale} so the caller can decide whether
1856
- * to manually reconcile or re-run with `dropOrphaned: true`.
1857
- *
1858
- * @example
1859
- * ```ts
1860
- * const result = await users.syncIndexes()
1861
- * for (const s of result.stale) {
1862
- * console.log(`${s.name}: key=${JSON.stringify(s.key)}`)
1863
- * console.log(` existing=${JSON.stringify(s.existing)}`)
1864
- * console.log(` desired=${JSON.stringify(s.desired)}`)
1865
- * }
1866
- * ```
1867
- */
1868
- type StaleIndex = {
1869
- /** The MongoDB index name (e.g. `'email_1'`). */
1870
- name: string;
1871
- /** The index key spec (e.g. `{ email: 1 }`). */
1872
- key: Record<string, 1 | -1 | 'text'>;
1873
- /** The relevant options currently set on the existing index. */
1874
- existing: Record<string, unknown>;
1875
- /** The options the schema declares for this index. */
1876
- desired: Record<string, unknown>;
1877
- };
2827
+ dropOrphaned?: boolean;
2828
+ };
2829
+ /**
2830
+ * Describes an index whose key matches a desired index but whose options differ.
2831
+ *
2832
+ * Returned in {@link SyncIndexesResult.stale} so the caller can decide whether
2833
+ * to manually reconcile or re-run with `dropOrphaned: true`.
2834
+ *
2835
+ * @example
2836
+ * ```ts
2837
+ * const result = await users.syncIndexes()
2838
+ * for (const s of result.stale) {
2839
+ * console.log(`${s.name}: key=${JSON.stringify(s.key)}`)
2840
+ * console.log(` existing=${JSON.stringify(s.existing)}`)
2841
+ * console.log(` desired=${JSON.stringify(s.desired)}`)
2842
+ * }
2843
+ * ```
2844
+ */
2845
+ type StaleIndex = {
2846
+ /** The MongoDB index name (e.g. `'email_1'`). */
2847
+ name: string;
2848
+ /** The index key spec (e.g. `{ email: 1 }`). */
2849
+ key: Record<string, 1 | -1 | 'text'>;
2850
+ /** The relevant options currently set on the existing index. */
2851
+ existing: Record<string, unknown>;
2852
+ /** The options the schema declares for this index. */
2853
+ desired: Record<string, unknown>;
2854
+ };
2855
+ /**
2856
+ * The result of a {@link syncIndexes} call.
2857
+ *
2858
+ * Every array contains index names. `stale` contains full details so the
2859
+ * caller can inspect the mismatch.
2860
+ *
2861
+ * @example
2862
+ * ```ts
2863
+ * const result = await users.syncIndexes()
2864
+ * console.log('created:', result.created)
2865
+ * console.log('dropped:', result.dropped)
2866
+ * console.log('skipped:', result.skipped)
2867
+ * console.log('stale:', result.stale.map(s => s.name))
2868
+ * ```
2869
+ */
2870
+ type SyncIndexesResult = {
2871
+ /** Names of indexes that were created (or would be created in dryRun mode). */
2872
+ created: string[];
2873
+ /** Names of indexes that were dropped (or would be dropped in dryRun mode). */
2874
+ dropped: string[];
2875
+ /** Names of indexes that already existed with matching options — no action taken. */
2876
+ skipped: string[];
2877
+ /** Indexes whose key matches a desired spec but whose options differ. */
2878
+ stale: StaleIndex[];
2879
+ };
2880
+
2881
+ /**
2882
+ * Extract the parent segment from a dot-separated path.
2883
+ *
2884
+ * @example
2885
+ * ```ts
2886
+ * type P = ExtractParent<'author.companyId'> // 'author'
2887
+ * ```
2888
+ */
2889
+ type ExtractParent<T extends string> = T extends `${infer P}.${string}` ? P : never;
2890
+ /**
2891
+ * Extract the leaf segment from a dot-separated path.
2892
+ *
2893
+ * @example
2894
+ * ```ts
2895
+ * type L = ExtractLeaf<'author.companyId'> // 'companyId'
2896
+ * ```
2897
+ */
2898
+ type ExtractLeaf<T extends string> = T extends `${string}.${infer L}` ? L : never;
2899
+ /**
2900
+ * Fluent populate builder for findOne queries.
2901
+ *
2902
+ * Implements `PromiseLike` so it can be awaited directly without `.populate()`.
2903
+ * Each `.populate()` call returns a new builder with updated generics.
2904
+ *
2905
+ * @example
2906
+ * ```ts
2907
+ * // Without populate (backward compatible)
2908
+ * const post = await posts.findOne({ title: 'Hello' })
2909
+ *
2910
+ * // With populate
2911
+ * const post = await posts.findOne({ title: 'Hello' })
2912
+ * .populate('authorId', 'author')
2913
+ * .populate('author.companyId', 'company')
2914
+ * ```
2915
+ */
2916
+ declare class PopulateOneQuery<TDef extends AnyCollection, TOutput, TPopMap extends Record<string, AnyCollection> = Record<string, never>> implements PromiseLike<TOutput | null> {
2917
+ private readonly handle;
2918
+ private readonly filter;
2919
+ private readonly options;
2920
+ private readonly steps;
2921
+ constructor(handle: CollectionHandle<TDef>, filter: TypedFilter<InferDocument<TDef>>, options: FindOneOptions | undefined, steps?: readonly PopulateStep[]);
2922
+ /**
2923
+ * Populate a top-level ref field, keeping the original field name.
2924
+ *
2925
+ * @param field - A ref-bearing field name on the root collection.
2926
+ * @returns A new builder with the field type replaced by the referenced document.
2927
+ *
2928
+ * @example
2929
+ * ```ts
2930
+ * const post = await posts.findOne({ title: 'Hello' })
2931
+ * .populate('authorId')
2932
+ * // post.authorId is now a User document instead of ObjectId
2933
+ * ```
2934
+ */
2935
+ populate<K extends RefFields<TDef>>(field: K): PopulateOneQuery<TDef, ApplyPopulate<TOutput, TDef, K>, TPopMap & {
2936
+ [P in K]: ExtractRefCollection<TDef, K>;
2937
+ }>;
2938
+ /**
2939
+ * Populate a top-level ref field with a projection, keeping the original field name.
2940
+ *
2941
+ * @param field - A ref-bearing field name on the root collection.
2942
+ * @param configure - Callback receiving a builder; call `.project()` to set the projection.
2943
+ * @returns A new builder with the field type narrowed to only the projected fields.
2944
+ *
2945
+ * @example
2946
+ * ```ts
2947
+ * const post = await posts.findOne({ title: 'Hello' })
2948
+ * .populate('authorId', (b) => b.project({ name: 1, email: 1 }))
2949
+ * // post.authorId is { _id: ObjectId; name: string; email: string }
2950
+ * // post.authorId.age → TS error
2951
+ * ```
2952
+ */
2953
+ populate<K extends RefFields<TDef>, P>(field: K, configure: (b: PopulateRefBuilder<InferDocument<ExtractRefCollection<TDef, K>>>) => PopulateProjectionConfig<P>): PopulateOneQuery<TDef, ApplyPopulateProjected<TOutput, TDef, K, P>, TPopMap & {
2954
+ [F in K]: ExtractRefCollection<TDef, K>;
2955
+ }>;
2956
+ /**
2957
+ * Populate a top-level ref field and rename it in the output.
2958
+ *
2959
+ * @param field - A ref-bearing field name on the root collection.
2960
+ * @param as - The new field name in the output document.
2961
+ * @returns A new builder with the old field removed and the new field added.
2962
+ *
2963
+ * @example
2964
+ * ```ts
2965
+ * const post = await posts.findOne({ title: 'Hello' })
2966
+ * .populate('authorId', 'author')
2967
+ * // post.author is a User document; post.authorId no longer exists
2968
+ * ```
2969
+ */
2970
+ populate<K extends RefFields<TDef>, TAs extends string>(field: K, as: TAs): PopulateOneQuery<TDef, ApplyPopulate<TOutput, TDef, K, TAs>, TPopMap & {
2971
+ [P in TAs]: ExtractRefCollection<TDef, K>;
2972
+ }>;
2973
+ /**
2974
+ * Populate a nested ref field, keeping the leaf field name.
2975
+ *
2976
+ * The parent path must have been populated in a previous `.populate()` call.
2977
+ *
2978
+ * @param path - A dot-separated path like `'author.companyId'`.
2979
+ * @returns A new builder with the nested field type replaced.
2980
+ *
2981
+ * @example
2982
+ * ```ts
2983
+ * const post = await posts.findOne({ title: 'Hello' })
2984
+ * .populate('authorId', 'author')
2985
+ * .populate('author.companyId')
2986
+ * // post.author.companyId is now a Company document
2987
+ * ```
2988
+ */
2989
+ populate<TPath extends NestedRefPath<TPopMap>>(path: TPath): PopulateOneQuery<TDef, DeepPopulate<TOutput, `${ExtractParent<TPath>}.${ExtractLeaf<TPath>}`, ExtractLeaf<TPath>, ExtractLeaf<TPath>, PopulateField<TPopMap[ExtractParent<TPath>], ExtractLeaf<TPath> & RefFields<TPopMap[ExtractParent<TPath>]>>>, TPopMap & {
2990
+ [P in `${ExtractParent<TPath>}.${ExtractLeaf<TPath>}`]: ExtractRefCollection<TPopMap[ExtractParent<TPath>], ExtractLeaf<TPath> & RefFields<TPopMap[ExtractParent<TPath>]>>;
2991
+ }>;
2992
+ /**
2993
+ * Populate a nested ref field and rename the leaf in the output.
2994
+ *
2995
+ * The parent path must have been populated in a previous `.populate()` call.
2996
+ *
2997
+ * @param path - A dot-separated path like `'author.companyId'`.
2998
+ * @param as - The new name for the leaf field in the output.
2999
+ * @returns A new builder with the nested field renamed and typed.
3000
+ *
3001
+ * @example
3002
+ * ```ts
3003
+ * const post = await posts.findOne({ title: 'Hello' })
3004
+ * .populate('authorId', 'author')
3005
+ * .populate('author.companyId', 'company')
3006
+ * // post.author.company is a Company document
3007
+ * ```
3008
+ */
3009
+ populate<TPath extends NestedRefPath<TPopMap>, TAs extends string>(path: TPath, as: TAs): PopulateOneQuery<TDef, DeepPopulate<TOutput, `${ExtractParent<TPath>}.${TAs}`, ExtractLeaf<TPath>, TAs, PopulateField<TPopMap[ExtractParent<TPath>], ExtractLeaf<TPath> & RefFields<TPopMap[ExtractParent<TPath>]>>>, TPopMap & {
3010
+ [P in `${ExtractParent<TPath>}.${TAs}`]: ExtractRefCollection<TPopMap[ExtractParent<TPath>], ExtractLeaf<TPath> & RefFields<TPopMap[ExtractParent<TPath>]>>;
3011
+ }>;
3012
+ /**
3013
+ * Attach fulfillment and rejection handlers to the query promise.
3014
+ *
3015
+ * Executes the base findOne query and applies populate steps if any.
3016
+ * Returns `null` when no document matches the filter.
3017
+ */
3018
+ then<TResult1 = TOutput | null, TResult2 = never>(onfulfilled?: ((value: TOutput | null) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null): Promise<TResult1 | TResult2>;
3019
+ private execute;
3020
+ }
1878
3021
  /**
1879
- * The result of a {@link syncIndexes} call.
3022
+ * Fluent populate builder for findOneOrThrow queries.
1880
3023
  *
1881
- * Every array contains index names. `stale` contains full details so the
1882
- * caller can inspect the mismatch.
3024
+ * Identical to {@link PopulateOneQuery} but resolves to `TOutput` (never null).
3025
+ * Throws {@link ZodmonNotFoundError} when no document matches the filter.
1883
3026
  *
1884
3027
  * @example
1885
3028
  * ```ts
1886
- * const result = await users.syncIndexes()
1887
- * console.log('created:', result.created)
1888
- * console.log('dropped:', result.dropped)
1889
- * console.log('skipped:', result.skipped)
1890
- * console.log('stale:', result.stale.map(s => s.name))
3029
+ * const post = await posts.findOneOrThrow({ title: 'Hello' })
3030
+ * .populate('authorId', 'author')
3031
+ * // Guaranteed non-null; throws if not found
1891
3032
  * ```
1892
3033
  */
1893
- type SyncIndexesResult = {
1894
- /** Names of indexes that were created (or would be created in dryRun mode). */
1895
- created: string[];
1896
- /** Names of indexes that were dropped (or would be dropped in dryRun mode). */
1897
- dropped: string[];
1898
- /** Names of indexes that already existed with matching options — no action taken. */
1899
- skipped: string[];
1900
- /** Indexes whose key matches a desired spec but whose options differ. */
1901
- stale: StaleIndex[];
1902
- };
3034
+ declare class PopulateOneOrThrowQuery<TDef extends AnyCollection, TOutput, TPopMap extends Record<string, AnyCollection> = Record<string, never>> implements PromiseLike<TOutput> {
3035
+ private readonly handle;
3036
+ private readonly filter;
3037
+ private readonly options;
3038
+ private readonly steps;
3039
+ constructor(handle: CollectionHandle<TDef>, filter: TypedFilter<InferDocument<TDef>>, options: FindOneOptions | undefined, steps?: readonly PopulateStep[]);
3040
+ /**
3041
+ * Populate a top-level ref field, keeping the original field name.
3042
+ *
3043
+ * @param field - A ref-bearing field name on the root collection.
3044
+ * @returns A new builder with the field type replaced by the referenced document.
3045
+ *
3046
+ * @example
3047
+ * ```ts
3048
+ * const post = await posts.findOneOrThrow({ title: 'Hello' })
3049
+ * .populate('authorId')
3050
+ * ```
3051
+ */
3052
+ populate<K extends RefFields<TDef>>(field: K): PopulateOneOrThrowQuery<TDef, ApplyPopulate<TOutput, TDef, K>, TPopMap & {
3053
+ [P in K]: ExtractRefCollection<TDef, K>;
3054
+ }>;
3055
+ /**
3056
+ * Populate a top-level ref field with a projection, keeping the original field name.
3057
+ *
3058
+ * @param field - A ref-bearing field name on the root collection.
3059
+ * @param configure - Callback receiving a builder; call `.project()` to set the projection.
3060
+ * @returns A new builder with the field type narrowed to only the projected fields.
3061
+ *
3062
+ * @example
3063
+ * ```ts
3064
+ * const post = await posts.findOneOrThrow({ title: 'Hello' })
3065
+ * .populate('authorId', (b) => b.project({ name: 1, email: 1 }))
3066
+ * // post.authorId is { _id: ObjectId; name: string; email: string }
3067
+ * // post.authorId.age → TS error
3068
+ * ```
3069
+ */
3070
+ populate<K extends RefFields<TDef>, P>(field: K, configure: (b: PopulateRefBuilder<InferDocument<ExtractRefCollection<TDef, K>>>) => PopulateProjectionConfig<P>): PopulateOneOrThrowQuery<TDef, ApplyPopulateProjected<TOutput, TDef, K, P>, TPopMap & {
3071
+ [F in K]: ExtractRefCollection<TDef, K>;
3072
+ }>;
3073
+ /**
3074
+ * Populate a top-level ref field and rename it in the output.
3075
+ *
3076
+ * @param field - A ref-bearing field name on the root collection.
3077
+ * @param as - The new field name in the output document.
3078
+ * @returns A new builder with the old field removed and the new field added.
3079
+ *
3080
+ * @example
3081
+ * ```ts
3082
+ * const post = await posts.findOneOrThrow({ title: 'Hello' })
3083
+ * .populate('authorId', 'author')
3084
+ * ```
3085
+ */
3086
+ populate<K extends RefFields<TDef>, TAs extends string>(field: K, as: TAs): PopulateOneOrThrowQuery<TDef, ApplyPopulate<TOutput, TDef, K, TAs>, TPopMap & {
3087
+ [P in TAs]: ExtractRefCollection<TDef, K>;
3088
+ }>;
3089
+ /**
3090
+ * Populate a nested ref field, keeping the leaf field name.
3091
+ *
3092
+ * @param path - A dot-separated path like `'author.companyId'`.
3093
+ * @returns A new builder with the nested field type replaced.
3094
+ *
3095
+ * @example
3096
+ * ```ts
3097
+ * const post = await posts.findOneOrThrow({ title: 'Hello' })
3098
+ * .populate('authorId', 'author')
3099
+ * .populate('author.companyId')
3100
+ * ```
3101
+ */
3102
+ populate<TPath extends NestedRefPath<TPopMap>>(path: TPath): PopulateOneOrThrowQuery<TDef, DeepPopulate<TOutput, `${ExtractParent<TPath>}.${ExtractLeaf<TPath>}`, ExtractLeaf<TPath>, ExtractLeaf<TPath>, PopulateField<TPopMap[ExtractParent<TPath>], ExtractLeaf<TPath> & RefFields<TPopMap[ExtractParent<TPath>]>>>, TPopMap & {
3103
+ [P in `${ExtractParent<TPath>}.${ExtractLeaf<TPath>}`]: ExtractRefCollection<TPopMap[ExtractParent<TPath>], ExtractLeaf<TPath> & RefFields<TPopMap[ExtractParent<TPath>]>>;
3104
+ }>;
3105
+ /**
3106
+ * Populate a nested ref field and rename the leaf in the output.
3107
+ *
3108
+ * @param path - A dot-separated path like `'author.companyId'`.
3109
+ * @param as - The new name for the leaf field in the output.
3110
+ * @returns A new builder with the nested field renamed and typed.
3111
+ *
3112
+ * @example
3113
+ * ```ts
3114
+ * const post = await posts.findOneOrThrow({ title: 'Hello' })
3115
+ * .populate('authorId', 'author')
3116
+ * .populate('author.companyId', 'company')
3117
+ * ```
3118
+ */
3119
+ populate<TPath extends NestedRefPath<TPopMap>, TAs extends string>(path: TPath, as: TAs): PopulateOneOrThrowQuery<TDef, DeepPopulate<TOutput, `${ExtractParent<TPath>}.${TAs}`, ExtractLeaf<TPath>, TAs, PopulateField<TPopMap[ExtractParent<TPath>], ExtractLeaf<TPath> & RefFields<TPopMap[ExtractParent<TPath>]>>>, TPopMap & {
3120
+ [P in `${ExtractParent<TPath>}.${TAs}`]: ExtractRefCollection<TPopMap[ExtractParent<TPath>], ExtractLeaf<TPath> & RefFields<TPopMap[ExtractParent<TPath>]>>;
3121
+ }>;
3122
+ /**
3123
+ * Attach fulfillment and rejection handlers to the query promise.
3124
+ *
3125
+ * Executes the base findOneOrThrow query and applies populate steps if any.
3126
+ * Throws {@link ZodmonNotFoundError} when no document matches.
3127
+ */
3128
+ then<TResult1 = TOutput, TResult2 = never>(onfulfilled?: ((value: TOutput) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null): Promise<TResult1 | TResult2>;
3129
+ private execute;
3130
+ }
1903
3131
 
1904
3132
  /**
1905
3133
  * Typed wrapper around a MongoDB driver `Collection`.
@@ -1916,7 +3144,35 @@ declare class CollectionHandle<TDef extends AnyCollection = AnyCollection> {
1916
3144
  readonly definition: TDef;
1917
3145
  /** The underlying MongoDB driver collection, typed to the inferred document type. */
1918
3146
  readonly native: Collection<InferDocument<TDef>>;
1919
- constructor(definition: TDef, native: Collection<InferDocument<TDef>>);
3147
+ /**
3148
+ * The MongoDB client session bound to this handle, if any.
3149
+ *
3150
+ * When set, all CRUD and aggregation operations performed through this
3151
+ * handle will include the session in their options, enabling transactional
3152
+ * reads and writes. Undefined when no session is bound.
3153
+ */
3154
+ readonly session: ClientSession | undefined;
3155
+ constructor(definition: TDef, native: Collection<InferDocument<TDef>>, session?: ClientSession);
3156
+ /**
3157
+ * Create a new handle bound to the given MongoDB client session.
3158
+ *
3159
+ * Returns a new {@link CollectionHandle} that shares the same collection
3160
+ * definition and native driver collection, but passes `session` to every
3161
+ * CRUD and aggregation operation. The original handle is not modified.
3162
+ *
3163
+ * @param session - The MongoDB `ClientSession` to bind.
3164
+ * @returns A new handle with the session attached.
3165
+ *
3166
+ * @example
3167
+ * ```ts
3168
+ * const users = db.use(Users)
3169
+ * await db.client.withSession(async (session) => {
3170
+ * const bound = users.withSession(session)
3171
+ * await bound.insertOne({ name: 'Ada' }) // uses session
3172
+ * })
3173
+ * ```
3174
+ */
3175
+ withSession(session: ClientSession): CollectionHandle<TDef>;
1920
3176
  /**
1921
3177
  * Insert a single document into the collection.
1922
3178
  *
@@ -1960,13 +3216,12 @@ declare class CollectionHandle<TDef extends AnyCollection = AnyCollection> {
1960
3216
  /**
1961
3217
  * Find a single document matching the filter.
1962
3218
  *
1963
- * Queries MongoDB, then validates the fetched document against the collection's
1964
- * Zod schema. Validation mode is resolved from the per-query option, falling
1965
- * back to the collection-level default (which defaults to `'strict'`).
3219
+ * Returns a {@link PopulateOneQuery} that can be awaited directly or chained
3220
+ * with `.populate()` calls to resolve foreign key references.
1966
3221
  *
1967
3222
  * @param filter - Type-safe filter to match documents.
1968
- * @param options - Optional projection and validation overrides.
1969
- * @returns The matched document, or `null` if no document matches.
3223
+ * @param options - Optional validation overrides.
3224
+ * @returns A populate builder that resolves to the matched document, or `null`.
1970
3225
  * @throws {ZodmonValidationError} When the fetched document fails schema validation in strict mode.
1971
3226
  *
1972
3227
  * @example
@@ -1975,17 +3230,44 @@ declare class CollectionHandle<TDef extends AnyCollection = AnyCollection> {
1975
3230
  * const user = await users.findOne({ name: 'Ada' })
1976
3231
  * if (user) console.log(user.role)
1977
3232
  * ```
3233
+ *
3234
+ * @example
3235
+ * ```ts
3236
+ * const post = await posts.findOne({ title: 'Hello' })
3237
+ * .populate('authorId', 'author')
3238
+ * ```
3239
+ */
3240
+ findOne(filter: TypedFilter<InferDocument<TDef>>, options?: FindOneOptions): PopulateOneQuery<TDef, InferDocument<TDef>>;
3241
+ /**
3242
+ * Find a single document matching the filter with a type-safe projection.
3243
+ *
3244
+ * Returns only the fields specified by the projection. The return type is
3245
+ * narrowed to reflect which fields are included or excluded.
3246
+ * Projected queries do not support `.populate()`.
3247
+ *
3248
+ * @param filter - Type-safe filter to match documents.
3249
+ * @param options - Projection and optional validation overrides.
3250
+ * @returns The projected document, or `null` if no document matches.
3251
+ * @throws {ZodmonValidationError} When the fetched document fails schema validation in strict mode.
3252
+ *
3253
+ * @example
3254
+ * ```ts
3255
+ * const users = db.use(Users)
3256
+ * const user = await users.findOne({ name: 'Ada' }, { project: { name: 1 } })
3257
+ * if (user) console.log(user.name) // typed as string
3258
+ * ```
1978
3259
  */
1979
- findOne(filter: TypedFilter<InferDocument<TDef>>, options?: FindOneOptions): Promise<InferDocument<TDef> | null>;
3260
+ findOne<P extends TypedProjection<InferDocument<TDef>>>(filter: TypedFilter<InferDocument<TDef>>, options: FindOneProjectionOptions<InferDocument<TDef>, P>): Promise<Prettify<ProjectionResult<InferDocument<TDef>, P>> | null>;
1980
3261
  /**
1981
3262
  * Find a single document matching the filter, or throw if none exists.
1982
3263
  *
1983
- * Behaves identically to {@link findOne} but throws {@link ZodmonNotFoundError}
1984
- * instead of returning `null` when no document matches the filter.
3264
+ * Returns a {@link PopulateOneOrThrowQuery} that can be awaited directly or
3265
+ * chained with `.populate()` calls. Throws {@link ZodmonNotFoundError}
3266
+ * instead of returning `null` when no document matches.
1985
3267
  *
1986
3268
  * @param filter - Type-safe filter to match documents.
1987
- * @param options - Optional projection and validation overrides.
1988
- * @returns The matched document (never null).
3269
+ * @param options - Optional validation overrides.
3270
+ * @returns A populate builder that resolves to the matched document (never null).
1989
3271
  * @throws {ZodmonNotFoundError} When no document matches the filter.
1990
3272
  * @throws {ZodmonValidationError} When the fetched document fails schema validation in strict mode.
1991
3273
  *
@@ -1995,8 +3277,36 @@ declare class CollectionHandle<TDef extends AnyCollection = AnyCollection> {
1995
3277
  * const user = await users.findOneOrThrow({ name: 'Ada' })
1996
3278
  * console.log(user.role) // guaranteed non-null
1997
3279
  * ```
3280
+ *
3281
+ * @example
3282
+ * ```ts
3283
+ * const post = await posts.findOneOrThrow({ title: 'Hello' })
3284
+ * .populate('authorId', 'author')
3285
+ * ```
3286
+ */
3287
+ findOneOrThrow(filter: TypedFilter<InferDocument<TDef>>, options?: FindOneOptions): PopulateOneOrThrowQuery<TDef, InferDocument<TDef>>;
3288
+ /**
3289
+ * Find a single document matching the filter with a type-safe projection, or throw if none exists.
3290
+ *
3291
+ * Returns only the fields specified by the projection. The return type is
3292
+ * narrowed to reflect which fields are included or excluded. Throws
3293
+ * {@link ZodmonNotFoundError} instead of returning `null`.
3294
+ * Projected queries do not support `.populate()`.
3295
+ *
3296
+ * @param filter - Type-safe filter to match documents.
3297
+ * @param options - Projection and optional validation overrides.
3298
+ * @returns The projected document (never null).
3299
+ * @throws {ZodmonNotFoundError} When no document matches the filter.
3300
+ * @throws {ZodmonValidationError} When the fetched document fails schema validation in strict mode.
3301
+ *
3302
+ * @example
3303
+ * ```ts
3304
+ * const users = db.use(Users)
3305
+ * const user = await users.findOneOrThrow({ name: 'Ada' }, { project: { name: 1 } })
3306
+ * console.log(user.name) // typed as string
3307
+ * ```
1998
3308
  */
1999
- findOneOrThrow(filter: TypedFilter<InferDocument<TDef>>, options?: FindOneOptions): Promise<InferDocument<TDef>>;
3309
+ findOneOrThrow<P extends TypedProjection<InferDocument<TDef>>>(filter: TypedFilter<InferDocument<TDef>>, options: FindOneProjectionOptions<InferDocument<TDef>, P>): Promise<Prettify<ProjectionResult<InferDocument<TDef>, P>>>;
2000
3310
  /**
2001
3311
  * Find all documents matching the filter, returning a chainable typed cursor.
2002
3312
  *
@@ -2018,6 +3328,24 @@ declare class CollectionHandle<TDef extends AnyCollection = AnyCollection> {
2018
3328
  * ```
2019
3329
  */
2020
3330
  find(filter: TypedFilter<InferDocument<TDef>>, options?: FindOptions): TypedFindCursor<TDef, IndexNames<TDef>>;
3331
+ /**
3332
+ * Find all documents matching the filter with a type-safe projection, returning a chainable typed cursor.
3333
+ *
3334
+ * The return type is narrowed to reflect which fields are included or excluded.
3335
+ *
3336
+ * @param filter - Type-safe filter to match documents.
3337
+ * @param options - Projection and optional validation overrides.
3338
+ * @returns A typed cursor whose output type matches the projection.
3339
+ *
3340
+ * @example
3341
+ * ```ts
3342
+ * const users = db.use(Users)
3343
+ * const names = await users.find({ role: 'admin' }, { project: { name: 1 } })
3344
+ * .toArray()
3345
+ * // names[0].name — string
3346
+ * ```
3347
+ */
3348
+ find<P extends TypedProjection<InferDocument<TDef>>>(filter: TypedFilter<InferDocument<TDef>>, options: FindProjectionOptions<InferDocument<TDef>, P>): TypedFindCursor<TDef, IndexNames<TDef>, Prettify<ProjectionResult<InferDocument<TDef>, P>>>;
2021
3349
  /**
2022
3350
  * Update a single document matching the filter.
2023
3351
  *
@@ -2213,7 +3541,8 @@ declare class AggregatePipeline<TDef extends AnyCollection, TOutput> {
2213
3541
  protected readonly definition: TDef;
2214
3542
  private readonly nativeCollection;
2215
3543
  private readonly stages;
2216
- constructor(definition: TDef, nativeCollection: Collection<InferDocument<TDef>>, stages: Document[]);
3544
+ private readonly session;
3545
+ constructor(definition: TDef, nativeCollection: Collection<InferDocument<TDef>>, stages: Document[], session?: ClientSession);
2217
3546
  /**
2218
3547
  * Append an arbitrary aggregation stage to the pipeline (escape hatch).
2219
3548
  *
@@ -2329,10 +3658,21 @@ declare class AggregatePipeline<TDef extends AnyCollection, TOutput> {
2329
3658
  * .toArray()
2330
3659
  * // subset[0].role → 'engineer' | 'designer'
2331
3660
  * ```
3661
+ *
3662
+ * @example
3663
+ * ```ts
3664
+ * // Field-vs-field comparison via $expr callback
3665
+ * const overRefunded = await orders.aggregate()
3666
+ * .match(
3667
+ * { status: 'completed' },
3668
+ * (expr) => expr.gt('totalAmount', expr.field('refundedAmount')),
3669
+ * )
3670
+ * .toArray()
3671
+ * ```
2332
3672
  */
2333
3673
  match<TNarrow extends {
2334
3674
  [K in keyof TNarrow]: K extends keyof TOutput ? TOutput[K] : never;
2335
- } = {}, F extends TypedFilter<TOutput> = TypedFilter<TOutput>>(filter: F): AggregatePipeline<TDef, Prettify<Omit<NarrowFromFilter<TOutput, F>, keyof TNarrow> & TNarrow>>;
3675
+ } = {}, F extends TypedFilter<TOutput> = TypedFilter<TOutput>>(filter: F, exprCb?: (expr: ExpressionBuilder<TOutput>) => Expression<boolean>): AggregatePipeline<TDef, Prettify<Omit<NarrowFromFilter<TOutput, F>, keyof TNarrow> & TNarrow>>;
2336
3676
  /**
2337
3677
  * Sort documents by one or more fields.
2338
3678
  *
@@ -2449,7 +3789,7 @@ declare class AggregatePipeline<TDef extends AnyCollection, TOutput> {
2449
3789
  * and compile-time field validation. Builder methods resolve return types
2450
3790
  * to the actual field type (`T[K]`), not `unknown`.
2451
3791
  *
2452
- * @param field - A field name or array of field names to group by.
3792
+ * @param field - A field name, array of field names, or `null` to aggregate all documents into a single group.
2453
3793
  * @param accumulators - A callback `(acc) => ({ ... })` or plain accumulator object.
2454
3794
  * @returns A new pipeline with the `$group` stage appended.
2455
3795
  *
@@ -2480,9 +3820,22 @@ declare class AggregatePipeline<TDef extends AnyCollection, TOutput> {
2480
3820
  * .groupBy(['role', 'dept'], acc => ({ count: acc.count() }))
2481
3821
  * .toArray()
2482
3822
  * ```
3823
+ *
3824
+ * @example
3825
+ * ```ts
3826
+ * // Null groupBy — aggregate all documents into a single summary
3827
+ * const totals = await aggregate(orders)
3828
+ * .groupBy(null, acc => ({
3829
+ * grandTotal: acc.sum('amount'),
3830
+ * orderCount: acc.count(),
3831
+ * }))
3832
+ * .toArray()
3833
+ * // → [{ _id: null, grandTotal: 2740, orderCount: 7 }]
3834
+ * ```
2483
3835
  */
2484
- groupBy<K extends keyof TOutput & string, TAccum extends Record<string, Accumulator>>(field: K, accumulators: ((acc: AccumulatorBuilder<TOutput>) => TAccum) | TAccum): AggregatePipeline<TDef, GroupByResult<TOutput, K, TAccum>>;
2485
- groupBy<K extends keyof TOutput & string, TAccum extends Record<string, Accumulator>>(field: K[], accumulators: ((acc: AccumulatorBuilder<TOutput>) => TAccum) | TAccum): AggregatePipeline<TDef, GroupByCompoundResult<TOutput, K, TAccum>>;
3836
+ groupBy<TAccum extends Record<string, Accumulator>>(field: null, accumulators: ((acc: AccumulatorBuilder<TOutput>) => TAccum) | TAccum): AggregatePipeline<TDef, GroupByNullResult<TAccum>>;
3837
+ groupBy<K extends DotPath<TOutput>, TAccum extends Record<string, Accumulator>>(field: K, accumulators: ((acc: AccumulatorBuilder<TOutput>) => TAccum) | TAccum): AggregatePipeline<TDef, GroupByResult<TOutput, K, TAccum>>;
3838
+ groupBy<K extends DotPath<TOutput>, TAccum extends Record<string, Accumulator>>(field: K[], accumulators: ((acc: AccumulatorBuilder<TOutput>) => TAccum) | TAccum): AggregatePipeline<TDef, GroupByCompoundResult<TOutput, K, TAccum>>;
2486
3839
  /**
2487
3840
  * Add new fields or overwrite existing ones in the output documents.
2488
3841
  *
@@ -2617,6 +3970,37 @@ declare class AggregatePipeline<TDef extends AnyCollection, TOutput> {
2617
3970
  }): AggregatePipeline<TDef, Prettify<TOutput & {
2618
3971
  [P in TAs]: InferDocument<TForeignDef>[];
2619
3972
  }>>;
3973
+ /**
3974
+ * Run multiple sub-pipelines on the same input documents in parallel.
3975
+ *
3976
+ * Each key in `spec` maps to a callback that receives a fresh `SubPipeline`
3977
+ * starting from `TOutput`. The callback chains stages and returns the terminal
3978
+ * pipeline. Zodmon extracts the accumulated stages at runtime to build the
3979
+ * `$facet` document. The output type is fully inferred — no annotation needed.
3980
+ *
3981
+ * Sub-pipelines support all stage methods including `.raw()` for operators not
3982
+ * yet first-class. Execution methods (`toArray`, `explain`) are not available
3983
+ * inside branches.
3984
+ *
3985
+ * @param spec - An object mapping branch names to sub-pipeline builder callbacks.
3986
+ * @returns A new pipeline whose output is one document with each branch name mapped to an array of results.
3987
+ *
3988
+ * @example
3989
+ * ```ts
3990
+ * const [report] = await aggregate(orders)
3991
+ * .facet({
3992
+ * byCategory: (sub) => sub
3993
+ * .groupBy('category', acc => ({ count: acc.count() }))
3994
+ * .sort({ count: -1 }),
3995
+ * totals: (sub) => sub
3996
+ * .groupBy(null, acc => ({ grandTotal: acc.sum('amount') })),
3997
+ * })
3998
+ * .toArray()
3999
+ * // report.byCategory → { _id: 'electronics' | 'books' | 'clothing'; count: number }[]
4000
+ * // report.totals → { _id: null; grandTotal: number }[]
4001
+ * ```
4002
+ */
4003
+ facet<TSpec extends Record<string, (sub: SubPipeline<TDef, TOutput>) => AggregatePipeline<TDef, any>>>(spec: TSpec): AggregatePipeline<TDef, Prettify<InferFacetOutput<TSpec>>>;
2620
4004
  /**
2621
4005
  * Count documents per group, sorted by count descending.
2622
4006
  *
@@ -2713,6 +4097,8 @@ declare class AggregatePipeline<TDef extends AnyCollection, TOutput> {
2713
4097
  bottom(n: number, options: {
2714
4098
  by: keyof TOutput & string;
2715
4099
  }): AggregatePipeline<TDef, TOutput>;
4100
+ /** @internal Used by facet() to extract branch stages. Not part of the public API. */
4101
+ getStages(): Document[];
2716
4102
  }
2717
4103
  /**
2718
4104
  * Create a new aggregation pipeline for a collection.
@@ -2732,6 +4118,99 @@ declare class AggregatePipeline<TDef extends AnyCollection, TOutput> {
2732
4118
  * ```
2733
4119
  */
2734
4120
  declare function aggregate<TDef extends AnyCollection>(handle: CollectionHandle<TDef>): AggregatePipeline<TDef, InferDocument<TDef>>;
4121
+ /**
4122
+ * A sub-pipeline passed to each `.facet()` branch callback.
4123
+ *
4124
+ * All stage-building methods are available. Execution methods (`toArray`,
4125
+ * `explain`, `[Symbol.asyncIterator]`) are omitted — sub-pipelines are
4126
+ * not executed directly; they only accumulate stages for the parent `$facet` stage.
4127
+ *
4128
+ * @typeParam TDef - The collection definition type.
4129
+ * @typeParam TOutput - The current output document type.
4130
+ *
4131
+ * @example
4132
+ * ```ts
4133
+ * .facet({
4134
+ * summary: (sub: SubPipeline<typeof Orders, Order>) => sub.groupBy(null, acc => ({ total: acc.sum('amount') }))
4135
+ * })
4136
+ * ```
4137
+ */
4138
+ type SubPipeline<TDef extends AnyCollection, TOutput> = Omit<AggregatePipeline<TDef, TOutput>, 'toArray' | 'explain' | typeof Symbol.asyncIterator>;
4139
+ /**
4140
+ * Maps a `.facet()` spec object to its inferred output shape.
4141
+ *
4142
+ * Each key maps to the array of results produced by that branch's terminal pipeline.
4143
+ * Uses `any` for the sub-pipeline parameter to avoid TypeScript contravariance
4144
+ * complications — inference of `TOut` from the return type is what matters.
4145
+ *
4146
+ * @example
4147
+ * ```ts
4148
+ * type Spec = {
4149
+ * byCategory: (sub: SubPipeline<any, any>) => AggregatePipeline<any, { _id: string; count: number }>
4150
+ * }
4151
+ * type Result = InferFacetOutput<Spec>
4152
+ * // ^? { byCategory: { _id: string; count: number }[] }
4153
+ * ```
4154
+ */
4155
+ type InferFacetOutput<TSpec> = {
4156
+ [K in keyof TSpec]: TSpec[K] extends (sub: any) => AggregatePipeline<any, infer TOut> ? TOut[] : never;
4157
+ };
4158
+
4159
+ /**
4160
+ * The callback signature for {@link Database.transaction}.
4161
+ *
4162
+ * @typeParam T - The return type of the transaction callback.
4163
+ *
4164
+ * @example
4165
+ * ```ts
4166
+ * const fn: TransactionFn<void> = async (tx) => {
4167
+ * const txUsers = tx.use(users)
4168
+ * await txUsers.insertOne({ name: 'Ada' })
4169
+ * }
4170
+ * ```
4171
+ */
4172
+ type TransactionFn<T> = (tx: TransactionContext) => Promise<T>;
4173
+ /**
4174
+ * Transaction context passed to the {@link Database.transaction} callback.
4175
+ *
4176
+ * Use {@link use} to bind existing collection handles to this transaction's
4177
+ * session. All operations on the returned handle participate in the
4178
+ * transaction -- auto-committed on success, auto-rolled-back on error.
4179
+ *
4180
+ * @example
4181
+ * ```ts
4182
+ * await db.transaction(async (tx) => {
4183
+ * const txUsers = tx.use(users)
4184
+ * const txPosts = tx.use(posts)
4185
+ * const user = await txUsers.insertOne({ name: 'Ada' })
4186
+ * await txPosts.insertOne({ authorId: user._id, title: 'Hello' })
4187
+ * })
4188
+ * ```
4189
+ */
4190
+ declare class TransactionContext {
4191
+ /** @internal */
4192
+ private readonly session;
4193
+ /** @internal */
4194
+ constructor(session: ClientSession);
4195
+ /**
4196
+ * Bind a collection handle to this transaction's session.
4197
+ *
4198
+ * Returns a cloned handle whose CRUD operations automatically include
4199
+ * the transaction session. The original handle is not modified.
4200
+ *
4201
+ * @param handle - An existing collection handle from `db.use()`.
4202
+ * @returns A new handle bound to the transaction session.
4203
+ *
4204
+ * @example
4205
+ * ```ts
4206
+ * await db.transaction(async (tx) => {
4207
+ * const txUsers = tx.use(users)
4208
+ * await txUsers.insertOne({ name: 'Ada' })
4209
+ * })
4210
+ * ```
4211
+ */
4212
+ use<TDef extends AnyCollection>(handle: CollectionHandle<TDef>): CollectionHandle<TDef>;
4213
+ }
2735
4214
 
2736
4215
  /**
2737
4216
  * Wraps a MongoDB `MongoClient` and `Db`, providing typed collection access
@@ -2788,11 +4267,40 @@ declare class Database {
2788
4267
  */
2789
4268
  syncIndexes(options?: SyncIndexesOptions): Promise<Record<string, SyncIndexesResult>>;
2790
4269
  /**
2791
- * Execute a function within a MongoDB transaction with auto-commit/rollback.
4270
+ * Execute a function within a MongoDB transaction.
4271
+ *
4272
+ * Starts a client session and runs the callback inside
4273
+ * `session.withTransaction()`. The driver handles commit on success,
4274
+ * abort on error, and automatic retries for transient transaction errors.
4275
+ *
4276
+ * The return value of `fn` is forwarded as the return value of this method.
4277
+ *
4278
+ * @param fn - Async callback receiving a {@link TransactionContext}.
4279
+ * @returns The value returned by `fn`.
4280
+ *
4281
+ * @example
4282
+ * ```ts
4283
+ * const user = await db.transaction(async (tx) => {
4284
+ * const txUsers = tx.use(users)
4285
+ * return await txUsers.insertOne({ name: 'Ada' })
4286
+ * })
4287
+ * ```
2792
4288
  *
2793
- * Stub — full implementation in TASK-106.
4289
+ * @example
4290
+ * ```ts
4291
+ * // Rollback on error
4292
+ * try {
4293
+ * await db.transaction(async (tx) => {
4294
+ * const txUsers = tx.use(users)
4295
+ * await txUsers.insertOne({ name: 'Ada' })
4296
+ * throw new Error('abort!')
4297
+ * })
4298
+ * } catch (err) {
4299
+ * // insert was rolled back, err is the original error
4300
+ * }
4301
+ * ```
2794
4302
  */
2795
- transaction<T>(_fn: () => Promise<T>): Promise<T>;
4303
+ transaction<T>(fn: TransactionFn<T>): Promise<T>;
2796
4304
  /**
2797
4305
  * Close the underlying `MongoClient` connection. Safe to call even if
2798
4306
  * no connection was established (the driver handles this gracefully).
@@ -3610,6 +5118,68 @@ type WarnableDefinition = {
3610
5118
  */
3611
5119
  declare function checkUnindexedFields(definition: WarnableDefinition, filter: Record<string, unknown>): void;
3612
5120
 
5121
+ /**
5122
+ * Walk through Zod wrappers to find the inner schema with ref metadata.
5123
+ *
5124
+ * Mirrors the type-level `UnwrapRef<T>` at runtime. Peels ZodOptional,
5125
+ * ZodNullable, ZodDefault (via `_zod.def.innerType`), and ZodArray
5126
+ * (via `_zod.def.element`).
5127
+ *
5128
+ * @param schema - The Zod schema to unwrap.
5129
+ * @returns The innermost schema after stripping wrappers.
5130
+ *
5131
+ * @example
5132
+ * ```ts
5133
+ * const inner = unwrapRefSchema(Posts.shape.categoryIds)
5134
+ * const ref = getRefMetadata(inner) // { collection: Categories }
5135
+ * ```
5136
+ */
5137
+ declare function unwrapRefSchema(schema: z.ZodType): z.ZodType;
5138
+ /**
5139
+ * Resolve a `.populate()` call into a `PopulateStep`.
5140
+ *
5141
+ * For top-level fields, inspects the collection's shape directly.
5142
+ * For nested dot-paths, walks previous steps to find the parent
5143
+ * collection definition, then resolves the leaf field in that shape.
5144
+ *
5145
+ * @param definition - The root collection definition.
5146
+ * @param previousSteps - Steps from earlier `.populate()` calls.
5147
+ * @param path - The field path (e.g. `'authorId'` or `'author.companyId'`).
5148
+ * @param as - The output field name (renamed or original).
5149
+ * @param projection - Optional projection to pass to the `$in` query.
5150
+ * @returns A resolved `PopulateStep`.
5151
+ * @throws When the field has no `.ref()` metadata or the parent path is not populated.
5152
+ *
5153
+ * @example
5154
+ * ```ts
5155
+ * const step = resolvePopulateStep(Posts, [], 'authorId', 'author')
5156
+ * // { leafField: 'authorId', as: 'author', targetCollection: Users, ... }
5157
+ * ```
5158
+ */
5159
+ declare function resolvePopulateStep(definition: AnyCollection, previousSteps: readonly PopulateStep[], path: string, as: string, projection?: Record<string, 0 | 1 | boolean>): PopulateStep;
5160
+ /**
5161
+ * Execute populate steps against a set of documents.
5162
+ *
5163
+ * Processes steps in order. For each step:
5164
+ * 1. Navigate to the parent level for nested paths
5165
+ * 2. Collect unique IDs from the leaf field
5166
+ * 3. Batch query the target collection with `$in`
5167
+ * 4. Merge results back, applying rename if needed
5168
+ *
5169
+ * Mutates the documents in place and returns them.
5170
+ *
5171
+ * @param documents - The documents to populate (mutated in place).
5172
+ * @param steps - The populate steps to execute in order.
5173
+ * @param getCollection - Function to get a native MongoDB Collection by name.
5174
+ * @returns The populated documents.
5175
+ *
5176
+ * @example
5177
+ * ```ts
5178
+ * const docs = await executePopulate(rawDocs, steps, name => db.collection(name))
5179
+ * ```
5180
+ */
5181
+ declare function executePopulate(documents: Document[], steps: readonly PopulateStep[], getCollection: (name: string) => Collection): Promise<Document[]>;
5182
+
3613
5183
  /**
3614
5184
  * Convenience namespace that groups all query operators under a single import.
3615
5185
  *
@@ -3918,4 +5488,4 @@ type UpdateFilterOf<TDef extends AnyCollection> = TypedUpdateFilter<InferDocumen
3918
5488
  */
3919
5489
  type SortOf<TDef extends AnyCollection> = TypedSort<InferDocument<TDef>>;
3920
5490
 
3921
- export { $, $addToSet, $and, $avg, $count, $eq, $exists, $first, $gt, $gte, $in, $last, $lt, $lte, $max, $min, $ne, $nin, $nor, $not, $or, $push, $regex, $sum, type Accumulator, type AccumulatorBuilder, type AddToSetEach, type AddToSetFields, AggregatePipeline, type AnyCollection, type ArrayElement, type CollectionDefinition, CollectionHandle, type CollectionName, type CollectionOptions, type ComparisonOperators, type CompoundIndexDefinition, type CurrentDateFields, type CursorPage, type CursorPaginateOptions, Database, type DotPathType, type DotPaths, type Expression, type ExpressionBuilder, type ExtractRefCollection, type FieldIndexDefinition, type FieldRef, type FieldRefType, type FilterOf, type FindOneAndDeleteOptions, type FindOneAndUpdateOptions, type FindOneOptions, type FindOptions, type GroupByCompoundResult, type GroupByResult, type HandleOf, type IncFields, IndexBuilder, type IndexMetadata, type IndexNames, type IndexOptions, type IndexSpec, type InferAccumulator, type InferAccumulators, type InferAddedFields, type InferDocument, type InferExpression, type InferInsert, type NarrowFromFilter, type OffsetPage, type OffsetPaginateOptions, type PopFields, type Prettify, type PullFields, type PushFields, type PushModifiers, type RefFields, type RefMarker, type RefMetadata, type RenameFields, type ResolvedShape, type SetFields, type SortOf, type StaleIndex, type SyncIndexesOptions, type SyncIndexesResult, type TypedFilter, TypedFindCursor, type TypedSort, type TypedUpdateFilter, type UnsetFields, type UnwindResult, type UpdateFilterOf, type UpdateOptions, type ValidationMode, type ZodObjectId, ZodmonAuthError, ZodmonBulkWriteError, ZodmonDocValidationError, ZodmonDuplicateKeyError, ZodmonError, ZodmonIndexError, ZodmonNetworkError, ZodmonNotFoundError, ZodmonQueryError, ZodmonTimeoutError, ZodmonValidationError, ZodmonWriteConflictError, aggregate, checkUnindexedFields, collection, createAccumulatorBuilder, createClient, createExpressionBuilder, deleteMany, deleteOne, extractComparableOptions, extractDbName, extractFieldIndexes, find, findOne, findOneAndDelete, findOneAndUpdate, findOneOrThrow, generateIndexName, getIndexMetadata, getRefMetadata, index, insertMany, insertOne, isOid, objectId, oid, raw, serializeIndexKey, syncIndexes, toCompoundIndexSpec, toFieldIndexSpec, updateMany, updateOne, wrapMongoError };
5491
+ export { $, $addToSet, $and, $avg, $count, $eq, $exists, $first, $gt, $gte, $in, $last, $lt, $lte, $max, $min, $ne, $nin, $nor, $not, $or, $push, $regex, $sum, type Accumulator, type AccumulatorBuilder, type AddToSetEach, type AddToSetFields, AggregatePipeline, type AnyCollection, type ApplyPopulate, type ApplyPopulateProjected, type ArrayElement, type CollectionDefinition, CollectionHandle, type CollectionName, type CollectionOptions, type ComparisonOperators, type CompoundIndexDefinition, type CurrentDateFields, type CursorPage, type CursorPaginateOptions, Database, type DeepPopulate, type DotPath, type DotPathValue, type DotSubPaths, type ExclusionProjection, type Expression, type ExpressionBuilder, type ExtractRefCollection, type FieldIndexDefinition, type FieldOrExpr, type FieldRef, type FieldRefType, type FilterOf, type FindOneAndDeleteOptions, type FindOneAndUpdateOptions, type FindOneOptions, type FindOneProjectionOptions, type FindOptions, type FindProjectionOptions, type GroupByCompoundResult, type GroupByNullResult, type GroupByResult, type HandleOf, type IncFields, type InclusionProjection, IndexBuilder, type IndexMetadata, type IndexNames, type IndexOptions, type IndexSpec, type InferAccumulator, type InferAccumulators, type InferAddedFields, type InferDocument, type InferExpression, type InferInsert, type NarrowFromFilter, type NestedRefPath, type OffsetPage, type OffsetPaginateOptions, type PopFields, PopulateCursor, type PopulateField, type PopulateFieldProjected, PopulateOneOrThrowQuery, PopulateOneQuery, type PopulateProjectionConfig, PopulateRefBuilder, type PopulateStep, type Populated, type Prettify, type ProjectionResult, type PullFields, type PushFields, type PushModifiers, type RefFields, type RefMarker, type RefMetadata, type RenameFields, type ReplaceDots, type ResolvedShape, type SetFields, type SortOf, type StaleIndex, type SyncIndexesOptions, type SyncIndexesResult, TransactionContext, type TransactionFn, type TypedFilter, TypedFindCursor, type TypedProjection, type TypedSort, type TypedUpdateFilter, type UnsetFields, type UnwindResult, type UnwrapRef, type UpdateFilterOf, type UpdateOptions, type ValidationMode, type ZodObjectId, ZodmonAuthError, ZodmonBulkWriteError, ZodmonDocValidationError, ZodmonDuplicateKeyError, ZodmonError, ZodmonIndexError, ZodmonNetworkError, ZodmonNotFoundError, ZodmonQueryError, ZodmonTimeoutError, ZodmonValidationError, ZodmonWriteConflictError, aggregate, checkUnindexedFields, collection, createAccumulatorBuilder, createClient, createExpressionBuilder, createPopulateCursor, deleteMany, deleteOne, deriveProjectedSchema, executePopulate, extractComparableOptions, extractDbName, extractFieldIndexes, find, findOne, findOneAndDelete, findOneAndUpdate, findOneOrThrow, generateIndexName, getIndexMetadata, getRefMetadata, index, insertMany, insertOne, isInclusionProjection, isOid, objectId, oid, raw, resolvePopulateStep, serializeIndexKey, syncIndexes, toCompoundIndexSpec, toFieldIndexSpec, unwrapRefSchema, updateMany, updateOne, wrapMongoError };