@zodmon/core 0.1.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.ts CHANGED
@@ -1,12 +1,904 @@
1
- import { ObjectId } from 'mongodb';
2
- import { ZodPipe, ZodCustom, ZodTransform } from 'zod';
1
+ import { ObjectId, Document, Collection, MongoClientOptions } from 'mongodb';
2
+ import { ZodPipe, ZodCustom, ZodTransform, z } from 'zod';
3
3
 
4
+ /**
5
+ * Options controlling how a field-level MongoDB index is created.
6
+ *
7
+ * Passed to the `.index()` Zod extension method. Every property is optional;
8
+ * omitting all of them creates a standard ascending, non-unique index.
9
+ */
10
+ type IndexOptions = {
11
+ /** When `true`, MongoDB enforces a unique constraint on this field. */
12
+ unique?: boolean;
13
+ /**
14
+ * When `true`, the index skips documents where the field is `null` or missing.
15
+ * Useful for optional fields that should be indexed only when present.
16
+ */
17
+ sparse?: boolean;
18
+ /** When `true`, creates a MongoDB text index for full-text search on this field. */
19
+ text?: boolean;
20
+ /**
21
+ * When `true`, the index is created in descending order (`-1`).
22
+ * Defaults to ascending (`1`) when omitted.
23
+ */
24
+ descending?: boolean;
25
+ /**
26
+ * TTL in seconds. MongoDB will automatically delete documents once the
27
+ * indexed `Date` field is older than this many seconds. Only valid on
28
+ * fields whose runtime type is `Date`.
29
+ */
30
+ expireAfter?: number;
31
+ /**
32
+ * A partial filter expression. Only documents matching this filter are
33
+ * included in the index. Maps directly to MongoDB's `partialFilterExpression`.
34
+ */
35
+ partial?: Record<string, unknown>;
36
+ };
37
+ /**
38
+ * Metadata stored in the WeakMap sidecar for every schema that has been
39
+ * marked with `.index()`. Always contains `indexed: true` plus any
40
+ * {@link IndexOptions} the caller provided.
41
+ */
42
+ type IndexMetadata = {
43
+ /** Always `true` — acts as a discriminator for "this field is indexed". */
44
+ indexed: true;
45
+ } & IndexOptions;
46
+ declare module 'zod' {
47
+ interface ZodType {
48
+ /**
49
+ * Mark this field for indexing when `syncIndexes()` is called.
50
+ *
51
+ * Stores {@link IndexOptions} in a WeakMap sidecar so the collection
52
+ * factory can later introspect each field and build the appropriate
53
+ * MongoDB index specification.
54
+ *
55
+ * @param options - Optional index configuration (unique, sparse, etc.).
56
+ * @returns The same schema instance for chaining.
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * const UserSchema = z.object({
61
+ * email: z.string().index({ unique: true }),
62
+ * age: z.number().index({ sparse: true }),
63
+ * })
64
+ * ```
65
+ */
66
+ index(options?: IndexOptions): this;
67
+ /**
68
+ * Shorthand for `.index({ unique: true })`.
69
+ *
70
+ * Creates a unique index on this field, causing MongoDB to reject
71
+ * duplicate values.
72
+ *
73
+ * @returns The same schema instance for chaining.
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * const UserSchema = z.object({
78
+ * email: z.string().unique(),
79
+ * })
80
+ * ```
81
+ */
82
+ unique(): this;
83
+ /**
84
+ * Shorthand for `.index({ text: true })`.
85
+ *
86
+ * Creates a MongoDB text index on this field, enabling `$text` queries
87
+ * for full-text search.
88
+ *
89
+ * @returns The same schema instance for chaining.
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * const PostSchema = z.object({
94
+ * body: z.string().text(),
95
+ * })
96
+ * ```
97
+ */
98
+ text(): this;
99
+ /**
100
+ * Shorthand for `.index({ expireAfter: seconds })`.
101
+ *
102
+ * Creates a TTL (Time-To-Live) index. MongoDB will automatically
103
+ * remove documents once the indexed `Date` field is older than
104
+ * the specified number of seconds.
105
+ *
106
+ * @param seconds - Number of seconds after which documents expire.
107
+ * @returns The same schema instance for chaining.
108
+ *
109
+ * @example
110
+ * ```ts
111
+ * const SessionSchema = z.object({
112
+ * createdAt: z.date().expireAfter(3600), // 1 hour TTL
113
+ * })
114
+ * ```
115
+ */
116
+ expireAfter(seconds: number): this;
117
+ }
118
+ }
119
+ /**
120
+ * Retrieve the index metadata attached to a Zod schema, if any.
121
+ *
122
+ * Returns `undefined` when the schema was never marked with an index
123
+ * extension (`.index()`, `.unique()`, `.text()`, or `.expireAfter()`).
124
+ *
125
+ * @param schema - The Zod schema to inspect. Accepts `unknown` for
126
+ * convenience; non-object values safely return `undefined`.
127
+ * @returns The {@link IndexMetadata} for the schema, or `undefined`.
128
+ *
129
+ * @example
130
+ * ```ts
131
+ * const email = z.string().index({ unique: true })
132
+ * const meta = getIndexMetadata(email)
133
+ * // => { indexed: true, unique: true }
134
+ * ```
135
+ */
136
+ declare function getIndexMetadata(schema: unknown): IndexMetadata | undefined;
137
+ /**
138
+ * Monkey-patch Zod's `ZodType.prototype` with Zodmon extension methods
139
+ * (`.index()`, `.unique()`, `.text()`, `.expireAfter()`).
140
+ *
141
+ * In Zod v4, methods are copied from `ZodType.prototype` to each instance
142
+ * during construction via the internal `init` loop (`Object.keys(proto)` ->
143
+ * copy to instance). Extension methods use `enumerable: true` so they are
144
+ * picked up by this loop for every schema created after installation.
145
+ *
146
+ * The function is idempotent: calling it more than once is a safe no-op,
147
+ * guarded by a non-enumerable `Symbol.for('zodmon_extensions')` property
148
+ * on the prototype.
149
+ *
150
+ * This function is called at module level when `extensions.ts` is first
151
+ * imported, so consumers never need to call it manually. It is exported
152
+ * primarily for use in tests.
153
+ *
154
+ * @example
155
+ * ```ts
156
+ * import { installExtensions } from '@zodmon/core/schema/extensions'
157
+ * installExtensions() // safe to call multiple times
158
+ *
159
+ * const indexed = z.string().index({ unique: true })
160
+ * ```
161
+ */
162
+ declare function installExtensions(): void;
163
+
164
+ /**
165
+ * The Zod type produced by {@link objectId}. A pipeline that validates an
166
+ * input as either a `string` (24-char hex) or an `ObjectId` instance, then
167
+ * transforms it into a concrete `ObjectId`.
168
+ *
169
+ * Use `z.infer<ZodObjectId>` to extract the output type (`ObjectId`) and
170
+ * `z.input<ZodObjectId>` for the input type (`string | ObjectId`).
171
+ */
172
+ type ZodObjectId = ZodPipe<ZodCustom<string | ObjectId, string | ObjectId>, ZodTransform<ObjectId, string | ObjectId>>;
173
+ /**
174
+ * Create a Zod schema that validates and coerces values into MongoDB
175
+ * `ObjectId` instances.
176
+ *
177
+ * Accepts either:
178
+ * - An existing `ObjectId` instance (passed through unchanged).
179
+ * - A 24-character hexadecimal string (coerced to `ObjectId`).
180
+ *
181
+ * All other inputs are rejected with the message `"Invalid ObjectId"`.
182
+ *
183
+ * @returns A {@link ZodObjectId} schema.
184
+ *
185
+ * @example
186
+ * ```ts
187
+ * const schema = objectId()
188
+ *
189
+ * schema.parse(new ObjectId()) // OK — pass-through
190
+ * schema.parse('64f1a2b3c4d5e6f7a8b9c0d1') // OK — coerced to ObjectId
191
+ * schema.parse('not-valid') // throws ZodError
192
+ * ```
193
+ *
194
+ * @example Inside a z.object() shape:
195
+ * ```ts
196
+ * const UserSchema = z.object({
197
+ * _id: objectId(),
198
+ * name: z.string(),
199
+ * })
200
+ * ```
201
+ */
202
+ declare function objectId(): ZodObjectId;
203
+
204
+ /**
205
+ * A compound index definition declared at the collection level.
206
+ * Maps field paths to sort direction (1 ascending, -1 descending)
207
+ * with optional index-level configuration.
208
+ *
209
+ * Generic over `TKeys` so that only fields present in the collection
210
+ * schema are accepted — referencing a nonexistent field is a type error.
211
+ */
212
+ type CompoundIndexDefinition<TKeys extends string = string> = {
213
+ fields: Partial<Record<TKeys, 1 | -1>>;
214
+ options?: {
215
+ unique?: boolean;
216
+ sparse?: boolean;
217
+ partial?: Record<string, unknown>;
218
+ name?: string;
219
+ };
220
+ };
221
+ /**
222
+ * A resolved field-level index extracted from schema metadata.
223
+ * Produced by walking the shape and calling getIndexMetadata() on each field.
224
+ */
225
+ type FieldIndexDefinition = {
226
+ field: string;
227
+ } & IndexOptions;
228
+ /** Options passed to collection() as the third argument. */
229
+ type CollectionOptions<TKeys extends string = string> = {
230
+ indexes?: CompoundIndexDefinition<TKeys>[];
231
+ validation?: 'strict' | 'strip' | 'passthrough';
232
+ warnUnindexedQueries?: boolean;
233
+ schemaVersion?: number;
234
+ migrate?: (doc: Record<string, unknown>, version: number) => Record<string, unknown>;
235
+ writeback?: boolean;
236
+ };
237
+ /**
238
+ * Resolves the final shape for a collection schema.
239
+ *
240
+ * If the user-provided shape already contains `_id`, it is used as-is.
241
+ * Otherwise, `_id: ZodObjectId` is prepended automatically.
242
+ * This allows custom id types (nanoid, UUID, etc.) when needed.
243
+ */
244
+ type ResolvedShape<TShape extends z.core.$ZodShape> = '_id' extends keyof TShape ? TShape : {
245
+ _id: ZodObjectId;
246
+ } & TShape;
247
+ /**
248
+ * The document type inferred from a collection definition.
249
+ * Uses the compiled schema directly, so it reflects whatever `_id`
250
+ * type was provided (custom or default ObjectId).
251
+ *
252
+ * Uses a structural constraint (`{ schema: z.ZodType }`) instead of
253
+ * `CollectionDefinition<z.core.$ZodShape>` because the conditional
254
+ * `ResolvedShape` type creates variance issues with index signatures.
255
+ */
256
+ type InferDocument<TDef extends {
257
+ readonly schema: z.ZodType;
258
+ }> = z.infer<TDef['schema']>;
259
+ /**
260
+ * The immutable definition object returned by collection().
261
+ * Holds everything needed to later create a live collection handle.
262
+ */
263
+ type CollectionDefinition<TShape extends z.core.$ZodShape = z.core.$ZodShape> = {
264
+ readonly name: string;
265
+ readonly schema: z.ZodObject<ResolvedShape<TShape>>;
266
+ readonly shape: TShape;
267
+ readonly fieldIndexes: FieldIndexDefinition[];
268
+ readonly compoundIndexes: CompoundIndexDefinition<Extract<keyof TShape, string>>[];
269
+ readonly options: Required<Pick<CollectionOptions, 'validation'>> & Omit<CollectionOptions, 'indexes' | 'validation'>;
270
+ };
271
+ /** Erased collection type for use in generic contexts. */
272
+ type AnyCollection = CollectionDefinition<z.core.$ZodShape>;
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
+
373
+ /**
374
+ * Walk a Zod shape and extract field-level index metadata from each field.
375
+ *
376
+ * Returns an array of {@link FieldIndexDefinition} for every field that has
377
+ * been marked with `.index()`, `.unique()`, `.text()`, or `.expireAfter()`.
378
+ * Fields without index metadata are silently skipped.
379
+ *
380
+ * @param shape - A Zod shape object (the value passed to `z.object()`).
381
+ * @returns An array of field index definitions with the field name attached.
382
+ */
383
+ declare function extractFieldIndexes(shape: z.core.$ZodShape): FieldIndexDefinition[];
384
+ /**
385
+ * Define a MongoDB collection with a Zod schema.
386
+ *
387
+ * Creates a {@link CollectionDefinition} that:
388
+ * - Adds `_id: objectId()` if the shape doesn't already include `_id`
389
+ * - Uses the user-provided `_id` schema if one is present (e.g. nanoid, UUID)
390
+ * - Extracts field-level index metadata from the shape
391
+ * - Separates compound indexes from the rest of the options
392
+ * - Returns an immutable definition object
393
+ *
394
+ * @param name - The MongoDB collection name.
395
+ * @param shape - A Zod shape object defining the document fields. May include a custom `_id`.
396
+ * @param options - Optional collection-level configuration including compound indexes.
397
+ * @returns A {@link CollectionDefinition} ready for use with `createClient()`.
398
+ *
399
+ * @example
400
+ * ```ts
401
+ * const Users = collection('users', {
402
+ * email: z.string().unique(),
403
+ * name: z.string().index(),
404
+ * age: z.number().optional(),
405
+ * })
406
+ * ```
407
+ */
408
+ declare function collection<TShape extends z.core.$ZodShape>(name: string, shape: TShape, options?: CollectionOptions<Extract<keyof TShape, string>>): CollectionDefinition<TShape>;
409
+
410
+ /**
411
+ * A builder for compound index definitions.
412
+ *
413
+ * Provides a fluent API for declaring compound indexes with options like
414
+ * `unique`, `sparse`, and custom `name`. Each method returns a new
415
+ * IndexBuilder instance (immutable pattern — the original is never mutated).
416
+ *
417
+ * IndexBuilder is structurally compatible with {@link CompoundIndexDefinition}
418
+ * so instances can be used directly in `CollectionOptions.indexes`.
419
+ *
420
+ * Dot-notation paths like `'address.city'` are accepted at the value level
421
+ * (any string satisfies `TKeys`), but type-level validation against nested
422
+ * schema paths is deferred to a future release.
423
+ *
424
+ * @example
425
+ * ```ts
426
+ * index({ email: 1, role: -1 }).unique().name('email_role_idx')
427
+ * ```
428
+ */
429
+
430
+ type IndexDirection = 1 | -1;
431
+ type CompoundIndexOptions = NonNullable<CompoundIndexDefinition['options']>;
432
+ declare class IndexBuilder<TKeys extends string> {
433
+ readonly fields: Partial<Record<TKeys, IndexDirection>>;
434
+ readonly options: CompoundIndexOptions;
435
+ constructor(fields: Record<TKeys, IndexDirection>);
436
+ private _clone;
437
+ unique(): IndexBuilder<TKeys>;
438
+ sparse(): IndexBuilder<TKeys>;
439
+ name(name: string): IndexBuilder<TKeys>;
440
+ }
441
+ /**
442
+ * Create a compound index definition with a fluent builder API.
443
+ *
444
+ * Returns an {@link IndexBuilder} that is structurally compatible with
445
+ * `CompoundIndexDefinition`, so it can be used directly in
446
+ * `CollectionOptions.indexes` alongside plain objects.
447
+ *
448
+ * @param fields - An object mapping field names to sort direction (1 or -1).
449
+ * @returns An {@link IndexBuilder} instance.
450
+ *
451
+ * @example
452
+ * ```ts
453
+ * collection('users', { email: z.string(), role: z.string() }, {
454
+ * indexes: [
455
+ * index({ email: 1, role: -1 }).unique(),
456
+ * { fields: { role: 1 } },
457
+ * ],
458
+ * })
459
+ * ```
460
+ */
461
+ declare function index<TKeys extends string>(fields: Record<TKeys, IndexDirection>): IndexBuilder<TKeys>;
462
+
463
+ /**
464
+ * Create or coerce a MongoDB `ObjectId`.
465
+ *
466
+ * - Called with **no arguments**: generates a brand-new `ObjectId`.
467
+ * - Called with a **hex string**: coerces it to an `ObjectId` via
468
+ * `ObjectId.createFromHexString`.
469
+ * - Called with an **existing `ObjectId`**: returns it unchanged.
470
+ *
471
+ * This is a convenience wrapper that removes the need for `new ObjectId()`
472
+ * boilerplate throughout application code.
473
+ *
474
+ * @param value - Optional hex string or `ObjectId` to coerce. Omit to
475
+ * generate a new `ObjectId`.
476
+ * @returns An `ObjectId` instance.
477
+ *
478
+ * @example
479
+ * ```ts
480
+ * oid() // new random ObjectId
481
+ * oid('64f1a2b3c4d5e6f7a8b9c0d1') // coerce hex string
482
+ * oid(existingId) // pass-through
483
+ * ```
484
+ */
4
485
  declare function oid(): ObjectId;
5
486
  declare function oid(value: string): ObjectId;
6
487
  declare function oid(value: ObjectId): ObjectId;
488
+ /**
489
+ * Type guard that narrows an `unknown` value to `ObjectId`.
490
+ *
491
+ * Uses `instanceof` internally, so it works with any value without risk
492
+ * of throwing.
493
+ *
494
+ * @param value - The value to check.
495
+ * @returns `true` if `value` is an `ObjectId` instance.
496
+ *
497
+ * @example
498
+ * ```ts
499
+ * const raw: unknown = getFromDb()
500
+ * if (isOid(raw)) {
501
+ * console.log(raw.toHexString()) // raw is narrowed to ObjectId
502
+ * }
503
+ * ```
504
+ */
7
505
  declare function isOid(value: unknown): value is ObjectId;
8
506
 
9
- type ZodObjectId = ZodPipe<ZodCustom<string | ObjectId, string | ObjectId>, ZodTransform<ObjectId, string | ObjectId>>;
10
- declare function objectId(): ZodObjectId;
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
+
820
+ /**
821
+ * Type-level marker that carries the target collection type through the
822
+ * type system. Intersected with the schema return type by `.ref()` so
823
+ * that `RefFields<T>` (future) can extract ref relationships.
824
+ *
825
+ * This is a phantom brand — no runtime value has this property.
826
+ */
827
+ type RefMarker<TCollection extends AnyCollection = AnyCollection> = {
828
+ readonly _ref: TCollection;
829
+ };
830
+ /**
831
+ * Metadata stored in the WeakMap sidecar for schemas marked with `.ref()`.
832
+ * Holds a reference to the target collection definition object.
833
+ */
834
+ type RefMetadata = {
835
+ readonly collection: AnyCollection;
836
+ };
837
+ /**
838
+ * Module augmentation: adds `.ref()` to all `ZodType` schemas.
839
+ *
840
+ * The intersection constraint `this['_zod']['output'] extends InferDocument<TCollection>['_id']`
841
+ * ensures compile-time type safety: the field's output type must match the
842
+ * target collection's `_id` type. Mismatches produce a type error.
843
+ *
844
+ * Supports both default ObjectId `_id` and custom `_id` types (string, nanoid, etc.):
845
+ * - `objectId().ref(Users)` compiles when Users has ObjectId `_id`
846
+ * - `z.string().ref(Orgs)` compiles when Orgs has string `_id`
847
+ * - `z.string().ref(Users)` is a type error (string ≠ ObjectId)
848
+ * - `objectId().ref(Orgs)` is a type error (ObjectId ≠ string)
849
+ */
850
+ declare module 'zod' {
851
+ interface ZodType {
852
+ /**
853
+ * Declare a typed foreign key reference to another collection.
854
+ *
855
+ * Stores the target collection definition in metadata for runtime
856
+ * populate resolution, and brands the return type with
857
+ * `RefMarker<TCollection>` so `RefFields<T>` can extract refs
858
+ * at the type level.
859
+ *
860
+ * The field's output type must match the target collection's `_id` type.
861
+ * Mismatched types produce a compile error.
862
+ *
863
+ * Apply `.ref()` before wrapper methods like `.optional()` or `.nullable()`:
864
+ * `objectId().ref(Users).optional()` — not `objectId().optional().ref(Users)`.
865
+ *
866
+ * @param collection - The target collection definition object.
867
+ * @returns The same schema instance, branded with the ref marker.
868
+ *
869
+ * @example
870
+ * ```ts
871
+ * const Posts = collection('posts', {
872
+ * authorId: objectId().ref(Users),
873
+ * title: z.string(),
874
+ * })
875
+ * ```
876
+ */
877
+ ref<TCollection extends AnyCollection>(collection: TCollection & (this['_zod']['output'] extends InferDocument<TCollection>['_id'] ? unknown : never)): this & RefMarker<TCollection>;
878
+ }
879
+ }
880
+ /**
881
+ * Retrieve the ref metadata attached to a Zod schema, if any.
882
+ *
883
+ * Returns `undefined` when the schema was never marked with `.ref()`.
884
+ *
885
+ * @param schema - The Zod schema to inspect. Accepts `unknown` for
886
+ * convenience; non-object values safely return `undefined`.
887
+ * @returns The {@link RefMetadata} for the schema, or `undefined`.
888
+ *
889
+ * @example
890
+ * ```ts
891
+ * const authorId = objectId().ref(Users)
892
+ * const meta = getRefMetadata(authorId)
893
+ * // => { collection: Users }
894
+ * ```
895
+ */
896
+ declare function getRefMetadata(schema: unknown): RefMetadata | undefined;
897
+ /**
898
+ * Install the `.ref()` extension method on `ZodType.prototype`.
899
+ *
900
+ * Idempotent — safe to call multiple times.
901
+ */
902
+ declare function installRefExtension(): void;
11
903
 
12
- export { type ZodObjectId, 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 };