@zodmon/core 0.2.0 → 0.3.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.cts CHANGED
@@ -1,5 +1,5 @@
1
+ import { ObjectId, Document, Collection, MongoClientOptions } from 'mongodb';
1
2
  import { ZodPipe, ZodCustom, ZodTransform, z } from 'zod';
2
- import { ObjectId } from 'mongodb';
3
3
 
4
4
  /**
5
5
  * Options controlling how a field-level MongoDB index is created.
@@ -271,6 +271,105 @@ type CollectionDefinition<TShape extends z.core.$ZodShape = z.core.$ZodShape> =
271
271
  /** Erased collection type for use in generic contexts. */
272
272
  type AnyCollection = CollectionDefinition<z.core.$ZodShape>;
273
273
 
274
+ /**
275
+ * Typed wrapper around a MongoDB driver `Collection`.
276
+ *
277
+ * Created by {@link Database.use}. Holds the original `CollectionDefinition`
278
+ * (for runtime schema validation and index metadata) alongside the native
279
+ * driver collection parameterized with the inferred document type.
280
+ *
281
+ * CRUD methods (insertOne, find, etc.) are added to this class by
282
+ * subsequent modules — the handle itself is intentionally behavior-free.
283
+ *
284
+ * @typeParam TDoc - The document type inferred from the Zod schema
285
+ * (e.g. `{ _id: ObjectId; name: string }`). Defaults to `Document`.
286
+ */
287
+ declare class CollectionHandle<TDoc extends Document = Document> {
288
+ /** The collection definition containing schema, name, and index metadata. */
289
+ readonly definition: AnyCollection;
290
+ /** The underlying MongoDB driver collection, typed to `TDoc`. */
291
+ readonly native: Collection<TDoc>;
292
+ constructor(definition: AnyCollection, native: Collection<TDoc>);
293
+ }
294
+
295
+ /**
296
+ * Wraps a MongoDB `MongoClient` and `Db`, providing typed collection access
297
+ * through {@link CollectionHandle}s.
298
+ *
299
+ * Connection is lazy — the driver connects on the first operation, not at
300
+ * construction time. Call {@link close} for graceful shutdown.
301
+ *
302
+ * @example
303
+ * ```ts
304
+ * const db = createClient('mongodb://localhost:27017', 'myapp')
305
+ * const users = db.use(UsersCollection)
306
+ * await users.native.insertOne({ _id: oid(), name: 'Ada' })
307
+ * await db.close()
308
+ * ```
309
+ */
310
+ declare class Database {
311
+ private readonly _client;
312
+ private readonly _db;
313
+ /** Registered collection definitions, keyed by name. Used by syncIndexes(). */
314
+ private readonly _collections;
315
+ constructor(uri: string, dbName: string, options?: MongoClientOptions);
316
+ /**
317
+ * Register a collection definition and return a typed {@link CollectionHandle}.
318
+ *
319
+ * The handle's `native` property is a MongoDB `Collection<TDoc>` where `TDoc`
320
+ * is the document type inferred from the definition's Zod schema. Calling
321
+ * `use()` multiple times with the same definition is safe — each call returns
322
+ * a new lightweight handle backed by the same underlying driver collection.
323
+ *
324
+ * @param def - A collection definition created by `collection()`.
325
+ * @returns A typed collection handle for CRUD operations.
326
+ */
327
+ use<TShape extends z.core.$ZodShape>(def: CollectionDefinition<TShape>): CollectionHandle<InferDocument<CollectionDefinition<TShape>>>;
328
+ /**
329
+ * Synchronize indexes defined in registered collections with MongoDB.
330
+ *
331
+ * Stub — full implementation in TASK-92.
332
+ */
333
+ syncIndexes(): Promise<void>;
334
+ /**
335
+ * Execute a function within a MongoDB transaction with auto-commit/rollback.
336
+ *
337
+ * Stub — full implementation in TASK-106.
338
+ */
339
+ transaction<T>(_fn: () => Promise<T>): Promise<T>;
340
+ /**
341
+ * Close the underlying `MongoClient` connection. Safe to call even if
342
+ * no connection was established (the driver handles this gracefully).
343
+ */
344
+ close(): Promise<void>;
345
+ }
346
+ /**
347
+ * Extract the database name from a MongoDB connection URI.
348
+ *
349
+ * Handles standard URIs, multi-host/replica set, SRV (`mongodb+srv://`),
350
+ * auth credentials, query parameters, and percent-encoded database names.
351
+ * Returns `undefined` when no database name is present.
352
+ */
353
+ declare function extractDbName(uri: string): string | undefined;
354
+ /**
355
+ * Create a new {@link Database} instance wrapping a MongoDB connection.
356
+ *
357
+ * The connection is lazy — the driver connects on the first operation.
358
+ * Pass any `MongoClientOptions` to configure connection pooling, timeouts, etc.
359
+ *
360
+ * When `dbName` is omitted, the database name is extracted from the URI path
361
+ * (e.g. `mongodb://localhost:27017/myapp` → `'myapp'`). If no database name
362
+ * is found in either the arguments or the URI, a warning is logged and
363
+ * MongoDB's default `'test'` database is used.
364
+ *
365
+ * @param uri - MongoDB connection string (e.g. `mongodb://localhost:27017`).
366
+ * @param dbName - The database name to use.
367
+ * @param options - Optional MongoDB driver client options.
368
+ * @returns A new `Database` instance.
369
+ */
370
+ declare function createClient(uri: string, dbName: string, options?: MongoClientOptions): Database;
371
+ declare function createClient(uri: string, options?: MongoClientOptions): Database;
372
+
274
373
  /**
275
374
  * Walk a Zod shape and extract field-level index metadata from each field.
276
375
  *
@@ -405,6 +504,319 @@ declare function oid(value: ObjectId): ObjectId;
405
504
  */
406
505
  declare function isOid(value: unknown): value is ObjectId;
407
506
 
507
+ /**
508
+ * Comparison operators for a field value of type `V`.
509
+ *
510
+ * Maps each MongoDB comparison operator to its expected value type.
511
+ * `$regex` is only available when `V` extends `string`.
512
+ *
513
+ * Used as the operator object that can be assigned to a field in {@link TypedFilter}.
514
+ *
515
+ * @example
516
+ * ```ts
517
+ * // As a raw object (without builder functions)
518
+ * const filter: TypedFilter<User> = { age: { $gt: 25, $lte: 65 } }
519
+ *
520
+ * // $regex only available on string fields
521
+ * const filter: TypedFilter<User> = { name: { $regex: /^A/i } }
522
+ * ```
523
+ */
524
+ type ComparisonOperators<V> = {
525
+ /** Matches values equal to the specified value. */
526
+ $eq?: V;
527
+ /** Matches values not equal to the specified value. */
528
+ $ne?: V;
529
+ /** Matches values greater than the specified value. */
530
+ $gt?: V;
531
+ /** Matches values greater than or equal to the specified value. */
532
+ $gte?: V;
533
+ /** Matches values less than the specified value. */
534
+ $lt?: V;
535
+ /** Matches values less than or equal to the specified value. */
536
+ $lte?: V;
537
+ /** Matches any value in the specified array. */
538
+ $in?: V[];
539
+ /** Matches none of the values in the specified array. */
540
+ $nin?: V[];
541
+ /** Matches documents where the field exists (`true`) or does not exist (`false`). */
542
+ $exists?: boolean;
543
+ /** Negates a comparison operator. */
544
+ $not?: ComparisonOperators<V>;
545
+ } & (V extends string ? {
546
+ $regex?: RegExp | string;
547
+ } : unknown);
548
+ /** Depth counter for limiting dot-notation recursion. Index = current depth, value = next depth. */
549
+ type Prev = [never, 0, 1, 2];
550
+ /**
551
+ * Generates a union of all valid dot-separated paths for nested object fields in `T`.
552
+ *
553
+ * Recursion is limited to 3 levels deep to prevent TypeScript compilation performance issues.
554
+ * Only plain object fields are traversed — arrays, `Date`, `RegExp`, and `ObjectId` are
555
+ * treated as leaf nodes and do not produce sub-paths.
556
+ *
557
+ * @example
558
+ * ```ts
559
+ * type User = { address: { city: string; geo: { lat: number; lng: number } } }
560
+ *
561
+ * // DotPaths<User> = 'address.city' | 'address.geo' | 'address.geo.lat' | 'address.geo.lng'
562
+ * ```
563
+ */
564
+ type DotPaths<T, Depth extends number = 3> = Depth extends 0 ? never : {
565
+ [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;
566
+ }[keyof T & string];
567
+ /**
568
+ * Resolves the value type at a dot-separated path `P` within type `T`.
569
+ *
570
+ * Splits `P` on the first `.` and recursively descends into `T`'s nested types.
571
+ * Returns `never` if the path is invalid.
572
+ *
573
+ * @example
574
+ * ```ts
575
+ * type User = { address: { city: string; geo: { lat: number } } }
576
+ *
577
+ * // DotPathType<User, 'address.city'> = string
578
+ * // DotPathType<User, 'address.geo.lat'> = number
579
+ * ```
580
+ */
581
+ 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;
582
+ /**
583
+ * Strict type-safe MongoDB filter query type.
584
+ *
585
+ * Validates filter objects at compile time — rejects nonexistent fields, type mismatches,
586
+ * and invalid operator usage. Unlike the MongoDB driver's `Filter<T>`, does NOT allow
587
+ * arbitrary keys via `& Document`.
588
+ *
589
+ * Supports three forms of filter expressions:
590
+ * - **Direct field values** (implicit `$eq`): `{ name: 'Alice' }`
591
+ * - **Comparison operators**: `{ age: { $gt: 25 } }` or `{ age: $gt(25) }`
592
+ * - **Dot notation** for nested fields up to 3 levels: `{ 'address.city': 'NYC' }`
593
+ *
594
+ * Logical operators `$and`, `$or`, and `$nor` accept arrays of `TypedFilter<T>`
595
+ * for composing complex queries.
596
+ *
597
+ * @example
598
+ * ```ts
599
+ * // Simple equality
600
+ * const filter: TypedFilter<User> = { name: 'Alice' }
601
+ *
602
+ * // Builder functions mixed with object literals
603
+ * const filter: TypedFilter<User> = { age: $gte(18), role: $in(['admin', 'mod']) }
604
+ *
605
+ * // Logical composition
606
+ * const filter = $and<User>(
607
+ * $or<User>({ role: 'admin' }, { role: 'moderator' }),
608
+ * { age: $gte(18) },
609
+ * { email: $exists() },
610
+ * )
611
+ *
612
+ * // Dynamic conditional building
613
+ * const conditions: TypedFilter<User>[] = []
614
+ * if (name) conditions.push({ name })
615
+ * if (minAge) conditions.push({ age: $gte(minAge) })
616
+ * const filter = conditions.length ? $and<User>(...conditions) : {}
617
+ * ```
618
+ */
619
+ type TypedFilter<T> = {
620
+ [K in keyof T]?: T[K] | ComparisonOperators<T[K]>;
621
+ } & {
622
+ [P in DotPaths<T>]?: DotPathType<T, P> | ComparisonOperators<DotPathType<T, P>>;
623
+ } & {
624
+ /** Joins clauses with a logical AND. Matches documents that satisfy all filters. */
625
+ $and?: TypedFilter<T>[];
626
+ /** Joins clauses with a logical OR. Matches documents that satisfy at least one filter. */
627
+ $or?: TypedFilter<T>[];
628
+ /** Joins clauses with a logical NOR. Matches documents that fail all filters. */
629
+ $nor?: TypedFilter<T>[];
630
+ };
631
+
632
+ /**
633
+ * Matches values equal to the specified value.
634
+ *
635
+ * @example
636
+ * ```ts
637
+ * // Explicit equality (equivalent to { name: 'Alice' })
638
+ * users.find({ name: $eq('Alice') })
639
+ * ```
640
+ */
641
+ declare const $eq: <V>(value: V) => {
642
+ $eq: V;
643
+ };
644
+ /**
645
+ * Matches values not equal to the specified value.
646
+ *
647
+ * @example
648
+ * ```ts
649
+ * users.find({ role: $ne('banned') })
650
+ * ```
651
+ */
652
+ declare const $ne: <V>(value: V) => {
653
+ $ne: V;
654
+ };
655
+ /**
656
+ * Matches values greater than the specified value.
657
+ *
658
+ * @example
659
+ * ```ts
660
+ * users.find({ age: $gt(18) })
661
+ * ```
662
+ */
663
+ declare const $gt: <V>(value: V) => {
664
+ $gt: V;
665
+ };
666
+ /**
667
+ * Matches values greater than or equal to the specified value.
668
+ *
669
+ * @example
670
+ * ```ts
671
+ * users.find({ age: $gte(18) })
672
+ * ```
673
+ */
674
+ declare const $gte: <V>(value: V) => {
675
+ $gte: V;
676
+ };
677
+ /**
678
+ * Matches values less than the specified value.
679
+ *
680
+ * @example
681
+ * ```ts
682
+ * users.find({ age: $lt(65) })
683
+ * ```
684
+ */
685
+ declare const $lt: <V>(value: V) => {
686
+ $lt: V;
687
+ };
688
+ /**
689
+ * Matches values less than or equal to the specified value.
690
+ *
691
+ * @example
692
+ * ```ts
693
+ * users.find({ age: $lte(65) })
694
+ * ```
695
+ */
696
+ declare const $lte: <V>(value: V) => {
697
+ $lte: V;
698
+ };
699
+ /**
700
+ * Matches any value in the specified array.
701
+ *
702
+ * @example
703
+ * ```ts
704
+ * users.find({ role: $in(['admin', 'moderator']) })
705
+ * ```
706
+ */
707
+ declare const $in: <V>(values: V[]) => {
708
+ $in: V[];
709
+ };
710
+ /**
711
+ * Matches none of the values in the specified array.
712
+ *
713
+ * @example
714
+ * ```ts
715
+ * users.find({ role: $nin(['banned', 'suspended']) })
716
+ * ```
717
+ */
718
+ declare const $nin: <V>(values: V[]) => {
719
+ $nin: V[];
720
+ };
721
+ /**
722
+ * Matches documents where the field exists (or does not exist).
723
+ * Defaults to `true` when called with no arguments.
724
+ *
725
+ * @example
726
+ * ```ts
727
+ * // Field must exist
728
+ * users.find({ email: $exists() })
729
+ *
730
+ * // Field must not exist
731
+ * users.find({ deletedAt: $exists(false) })
732
+ * ```
733
+ */
734
+ declare const $exists: (flag?: boolean) => {
735
+ $exists: boolean;
736
+ };
737
+ /**
738
+ * Matches string values against a regular expression pattern.
739
+ * Only valid on string fields.
740
+ *
741
+ * @example
742
+ * ```ts
743
+ * users.find({ name: $regex(/^A/i) })
744
+ * users.find({ email: $regex('^admin@') })
745
+ * ```
746
+ */
747
+ declare const $regex: (pattern: RegExp | string) => {
748
+ $regex: RegExp | string;
749
+ };
750
+ /**
751
+ * Negates a comparison operator. Wraps the given operator object
752
+ * in a `$not` condition.
753
+ *
754
+ * @example
755
+ * ```ts
756
+ * // Age is NOT greater than 65
757
+ * users.find({ age: $not($gt(65)) })
758
+ *
759
+ * // Name does NOT match pattern
760
+ * users.find({ name: $not($regex(/^test/)) })
761
+ * ```
762
+ */
763
+ declare const $not: <O extends Record<string, unknown>>(op: O) => {
764
+ $not: O;
765
+ };
766
+ /**
767
+ * Joins filter clauses with a logical OR. Matches documents that satisfy
768
+ * at least one of the provided filters.
769
+ *
770
+ * @example
771
+ * ```ts
772
+ * users.find($or({ role: 'admin' }, { age: $gte(18) }))
773
+ *
774
+ * // Dynamic composition
775
+ * const conditions: TypedFilter<User>[] = []
776
+ * if (name) conditions.push({ name })
777
+ * if (role) conditions.push({ role })
778
+ * users.find($or(...conditions))
779
+ * ```
780
+ */
781
+ declare const $or: <T>(...filters: TypedFilter<T>[]) => TypedFilter<T>;
782
+ /**
783
+ * Joins filter clauses with a logical AND. Matches documents that satisfy
784
+ * all of the provided filters. Useful for dynamic filter building where
785
+ * multiple conditions on the same field would conflict in an object literal.
786
+ *
787
+ * @example
788
+ * ```ts
789
+ * users.find($and(
790
+ * $or({ role: 'admin' }, { role: 'moderator' }),
791
+ * { age: $gte(18) },
792
+ * { email: $exists() },
793
+ * ))
794
+ * ```
795
+ */
796
+ declare const $and: <T>(...filters: TypedFilter<T>[]) => TypedFilter<T>;
797
+ /**
798
+ * Joins filter clauses with a logical NOR. Matches documents that fail
799
+ * all of the provided filters.
800
+ *
801
+ * @example
802
+ * ```ts
803
+ * // Exclude banned and suspended users
804
+ * users.find($nor({ role: 'banned' }, { role: 'suspended' }))
805
+ * ```
806
+ */
807
+ declare const $nor: <T>(...filters: TypedFilter<T>[]) => TypedFilter<T>;
808
+ /**
809
+ * Escape hatch for unsupported or raw MongoDB filter operators.
810
+ * Wraps an untyped filter object so it can be passed where `TypedFilter<T>` is expected.
811
+ * Use when you need operators not covered by the type system (e.g., `$text`, `$geoNear`).
812
+ *
813
+ * @example
814
+ * ```ts
815
+ * users.find(raw({ $text: { $search: 'mongodb tutorial' } }))
816
+ * ```
817
+ */
818
+ declare const raw: <T = any>(filter: Record<string, unknown>) => TypedFilter<T>;
819
+
408
820
  /**
409
821
  * Type-level marker that carries the target collection type through the
410
822
  * type system. Intersected with the schema return type by `.ref()` so
@@ -489,4 +901,4 @@ declare function getRefMetadata(schema: unknown): RefMetadata | undefined;
489
901
  */
490
902
  declare function installRefExtension(): void;
491
903
 
492
- export { type AnyCollection, type CollectionDefinition, type CollectionOptions, type CompoundIndexDefinition, type FieldIndexDefinition, IndexBuilder, type IndexMetadata, type IndexOptions, type InferDocument, type RefMarker, type RefMetadata, type ResolvedShape, type ZodObjectId, collection, extractFieldIndexes, getIndexMetadata, getRefMetadata, index, installExtensions, installRefExtension, isOid, objectId, oid };
904
+ export { $and, $eq, $exists, $gt, $gte, $in, $lt, $lte, $ne, $nin, $nor, $not, $or, $regex, type AnyCollection, type CollectionDefinition, CollectionHandle, type CollectionOptions, type ComparisonOperators, type CompoundIndexDefinition, Database, type DotPathType, type DotPaths, type FieldIndexDefinition, IndexBuilder, type IndexMetadata, type IndexOptions, type InferDocument, type RefMarker, type RefMetadata, type ResolvedShape, type TypedFilter, type ZodObjectId, collection, createClient, extractDbName, extractFieldIndexes, getIndexMetadata, getRefMetadata, index, installExtensions, installRefExtension, isOid, objectId, oid, raw };