···1-import { type Db, type MongoClientOptions, MongoClient } from "mongodb";
2import { ConnectionError } from "../errors.ts";
34/**
5 * Connection management module
6- *
7 * Handles MongoDB connection lifecycle including connect, disconnect,
8 * and connection state management.
9 */
···2021/**
22 * Connect to MongoDB with connection pooling, retry logic, and resilience options
23- *
24 * The MongoDB driver handles connection pooling and automatic retries.
25 * Retry logic is enabled by default for both reads and writes in MongoDB 4.2+.
26- *
27 * @param uri - MongoDB connection string
28 * @param dbName - Name of the database to connect to
29 * @param options - Connection options (pooling, retries, timeouts, etc.)
30 * @returns Connection object with client and db
31- *
32 * @example
33 * Basic connection with pooling:
34 * ```ts
···40 * socketTimeoutMS: 45000,
41 * });
42 * ```
43- *
44 * @example
45 * Production-ready connection with retry logic and resilience:
46 * ```ts
···48 * // Connection pooling
49 * maxPoolSize: 10,
50 * minPoolSize: 2,
51- *
52 * // Automatic retry logic (enabled by default)
53 * retryReads: true, // Retry failed read operations
54 * retryWrites: true, // Retry failed write operations
55- *
56 * // Timeouts
57 * connectTimeoutMS: 10000, // Initial connection timeout
58 * socketTimeoutMS: 45000, // Socket operation timeout
59 * serverSelectionTimeoutMS: 10000, // Server selection timeout
60- *
61 * // Connection resilience
62 * maxIdleTimeMS: 30000, // Close idle connections
63 * heartbeatFrequencyMS: 10000, // Server health check interval
64- *
65 * // Optional: Compression for reduced bandwidth
66 * compressors: ['snappy', 'zlib'],
67 * });
···85 return connection;
86 } catch (error) {
87 throw new ConnectionError(
88- `Failed to connect to MongoDB: ${error instanceof Error ? error.message : String(error)}`,
89- uri
0090 );
91 }
92}
···103104/**
105 * Get the current database connection
106- *
107 * @returns MongoDB Db instance
108 * @throws {ConnectionError} If not connected
109 * @internal
···117118/**
119 * Get the current connection state
120- *
121 * @returns Connection object or null if not connected
122 * @internal
123 */
···1+import { type Db, MongoClient, type MongoClientOptions } from "mongodb";
2import { ConnectionError } from "../errors.ts";
34/**
5 * Connection management module
6+ *
7 * Handles MongoDB connection lifecycle including connect, disconnect,
8 * and connection state management.
9 */
···2021/**
22 * Connect to MongoDB with connection pooling, retry logic, and resilience options
23+ *
24 * The MongoDB driver handles connection pooling and automatic retries.
25 * Retry logic is enabled by default for both reads and writes in MongoDB 4.2+.
26+ *
27 * @param uri - MongoDB connection string
28 * @param dbName - Name of the database to connect to
29 * @param options - Connection options (pooling, retries, timeouts, etc.)
30 * @returns Connection object with client and db
31+ *
32 * @example
33 * Basic connection with pooling:
34 * ```ts
···40 * socketTimeoutMS: 45000,
41 * });
42 * ```
43+ *
44 * @example
45 * Production-ready connection with retry logic and resilience:
46 * ```ts
···48 * // Connection pooling
49 * maxPoolSize: 10,
50 * minPoolSize: 2,
51+ *
52 * // Automatic retry logic (enabled by default)
53 * retryReads: true, // Retry failed read operations
54 * retryWrites: true, // Retry failed write operations
55+ *
56 * // Timeouts
57 * connectTimeoutMS: 10000, // Initial connection timeout
58 * socketTimeoutMS: 45000, // Socket operation timeout
59 * serverSelectionTimeoutMS: 10000, // Server selection timeout
60+ *
61 * // Connection resilience
62 * maxIdleTimeMS: 30000, // Close idle connections
63 * heartbeatFrequencyMS: 10000, // Server health check interval
64+ *
65 * // Optional: Compression for reduced bandwidth
66 * compressors: ['snappy', 'zlib'],
67 * });
···85 return connection;
86 } catch (error) {
87 throw new ConnectionError(
88+ `Failed to connect to MongoDB: ${
89+ error instanceof Error ? error.message : String(error)
90+ }`,
91+ uri,
92 );
93 }
94}
···105106/**
107 * Get the current database connection
108+ *
109 * @returns MongoDB Db instance
110 * @throws {ConnectionError} If not connected
111 * @internal
···119120/**
121 * Get the current connection state
122+ *
123 * @returns Connection object or null if not connected
124 * @internal
125 */
+5-5
client/health.ts
···23/**
4 * Health check module
5- *
6 * Provides functionality for monitoring MongoDB connection health
7 * including ping operations and response time measurement.
8 */
910/**
11 * Health check details of the MongoDB connection
12- *
13 * @property healthy - Overall health status of the connection
14 * @property connected - Whether a connection is established
15 * @property responseTimeMs - Response time in milliseconds (if connection is healthy)
···2627/**
28 * Check the health of the MongoDB connection
29- *
30 * Performs a ping operation to verify the database is responsive
31 * and returns detailed health information including response time.
32- *
33 * @returns Health check result with status and metrics
34- *
35 * @example
36 * ```ts
37 * const health = await healthCheck();
···23/**
4 * Health check module
5+ *
6 * Provides functionality for monitoring MongoDB connection health
7 * including ping operations and response time measurement.
8 */
910/**
11 * Health check details of the MongoDB connection
12+ *
13 * @property healthy - Overall health status of the connection
14 * @property connected - Whether a connection is established
15 * @property responseTimeMs - Response time in milliseconds (if connection is healthy)
···2627/**
28 * Check the health of the MongoDB connection
29+ *
30 * Performs a ping operation to verify the database is responsive
31 * and returns detailed health information including response time.
32+ *
33 * @returns Health check result with status and metrics
34+ *
35 * @example
36 * ```ts
37 * const health = await healthCheck();
+5-12
client/index.ts
···1/**
2 * Client module - MongoDB connection and session management
3- *
4 * This module provides all client-level functionality including:
5 * - Connection management (connect, disconnect)
6 * - Health monitoring (healthCheck)
···10// Re-export connection management
11export {
12 connect,
0013 disconnect,
14 getDb,
15- type ConnectOptions,
16- type Connection,
17} from "./connection.ts";
1819// Re-export health monitoring
20-export {
21- healthCheck,
22- type HealthCheckResult,
23-} from "./health.ts";
2425// Re-export transaction management
26-export {
27- startSession,
28- endSession,
29- withTransaction,
30-} from "./transactions.ts";
···1/**
2 * Client module - MongoDB connection and session management
3+ *
4 * This module provides all client-level functionality including:
5 * - Connection management (connect, disconnect)
6 * - Health monitoring (healthCheck)
···10// Re-export connection management
11export {
12 connect,
13+ type Connection,
14+ type ConnectOptions,
15 disconnect,
16 getDb,
0017} from "./connection.ts";
1819// Re-export health monitoring
20+export { healthCheck, type HealthCheckResult } from "./health.ts";
0002122// Re-export transaction management
23+export { endSession, startSession, withTransaction } from "./transactions.ts";
0000
+12-12
client/transactions.ts
···45/**
6 * Transaction management module
7- *
8 * Provides session and transaction management functionality including
9 * automatic transaction handling and manual session control.
10 */
1112/**
13 * Start a new client session for transactions
14- *
15 * Sessions must be ended when done using `endSession()`
16- *
17 * @returns New MongoDB ClientSession
18 * @throws {ConnectionError} If not connected
19- *
20 * @example
21 * ```ts
22 * const session = startSession();
···3738/**
39 * End a client session
40- *
41 * @param session - The session to end
42 */
43export async function endSession(session: ClientSession): Promise<void> {
···4647/**
48 * Execute a function within a transaction
49- *
50 * Automatically handles session creation, transaction start/commit/abort, and cleanup.
51 * If the callback throws an error, the transaction is automatically aborted.
52- *
53 * @param callback - Async function to execute within the transaction. Receives the session as parameter.
54 * @param options - Optional transaction options (read/write concern, etc.)
55 * @returns The result from the callback function
56- *
57 * @example
58 * ```ts
59 * const result = await withTransaction(async (session) => {
···65 */
66export async function withTransaction<T>(
67 callback: (session: ClientSession) => Promise<T>,
68- options?: TransactionOptions
69): Promise<T> {
70 const session = startSession();
71-72 try {
73 let result: T;
74-75 await session.withTransaction(async () => {
76 result = await callback(session);
77 }, options);
78-79 return result!;
80 } finally {
81 await endSession(session);
···45/**
6 * Transaction management module
7+ *
8 * Provides session and transaction management functionality including
9 * automatic transaction handling and manual session control.
10 */
1112/**
13 * Start a new client session for transactions
14+ *
15 * Sessions must be ended when done using `endSession()`
16+ *
17 * @returns New MongoDB ClientSession
18 * @throws {ConnectionError} If not connected
19+ *
20 * @example
21 * ```ts
22 * const session = startSession();
···3738/**
39 * End a client session
40+ *
41 * @param session - The session to end
42 */
43export async function endSession(session: ClientSession): Promise<void> {
···4647/**
48 * Execute a function within a transaction
49+ *
50 * Automatically handles session creation, transaction start/commit/abort, and cleanup.
51 * If the callback throws an error, the transaction is automatically aborted.
52+ *
53 * @param callback - Async function to execute within the transaction. Receives the session as parameter.
54 * @param options - Optional transaction options (read/write concern, etc.)
55 * @returns The result from the callback function
56+ *
57 * @example
58 * ```ts
59 * const result = await withTransaction(async (session) => {
···65 */
66export async function withTransaction<T>(
67 callback: (session: ClientSession) => Promise<T>,
68+ options?: TransactionOptions,
69): Promise<T> {
70 const session = startSession();
71+72 try {
73 let result: T;
74+75 await session.withTransaction(async () => {
76 result = await callback(session);
77 }, options);
78+79 return result!;
80 } finally {
81 await endSession(session);
···1-export type { Schema, Infer, Input } from "./types.ts";
2-export {
3- connect,
4- disconnect,
5- healthCheck,
6- startSession,
7 endSession,
0008 withTransaction,
9- type ConnectOptions,
10- type HealthCheckResult
11} from "./client/index.ts";
12export { Model } from "./model/index.ts";
13export {
14- NozzleError,
15- ValidationError,
16 ConnectionError,
17- ConfigurationError,
18 DocumentNotFoundError,
019 OperationError,
20- AsyncValidationError,
21} from "./errors.ts";
2223// Re-export MongoDB types that users might need
···1+export type { Infer, Input, Schema } from "./types.ts";
2+export {
3+ connect,
4+ type ConnectOptions,
5+ disconnect,
06 endSession,
7+ healthCheck,
8+ type HealthCheckResult,
9+ startSession,
10 withTransaction,
0011} from "./client/index.ts";
12export { Model } from "./model/index.ts";
13export {
14+ AsyncValidationError,
15+ ConfigurationError,
16 ConnectionError,
017 DocumentNotFoundError,
18+ NozzleError,
19 OperationError,
20+ ValidationError,
21} from "./errors.ts";
2223// Re-export MongoDB types that users might need
+81-62
model/core.ts
···1import type { z } from "@zod/zod";
2import type {
003 Collection,
004 DeleteResult,
5 Document,
6 Filter,
0007 InsertManyResult,
8- InsertOneResult,
9 InsertOneOptions,
10- FindOptions,
11- UpdateOptions,
12- ReplaceOptions,
13- FindOneAndUpdateOptions,
14- FindOneAndReplaceOptions,
15- DeleteOptions,
16- CountDocumentsOptions,
17- AggregateOptions,
18 OptionalUnlessRequiredId,
00019 UpdateResult,
20 WithId,
21- BulkWriteOptions,
22- UpdateFilter,
23- ModifyResult,
24} from "mongodb";
25import { ObjectId } from "mongodb";
26-import type { Schema, Infer, Input } from "../types.ts";
27-import { parse, parsePartial, parseReplace, applyDefaultsForUpsert } from "./validation.ts";
000002829/**
30 * Core CRUD operations for the Model class
31- *
32 * This module contains all basic create, read, update, and delete operations
33 * with automatic Zod validation and transaction support.
34 */
3536/**
37 * Insert a single document into the collection
38- *
39 * @param collection - MongoDB collection
40 * @param schema - Zod schema for validation
41 * @param data - Document data to insert
···46 collection: Collection<Infer<T>>,
47 schema: T,
48 data: Input<T>,
49- options?: InsertOneOptions
50): Promise<InsertOneResult<Infer<T>>> {
51 const validatedData = parse(schema, data);
52 return await collection.insertOne(
53 validatedData as OptionalUnlessRequiredId<Infer<T>>,
54- options
55 );
56}
5758/**
59 * Insert multiple documents into the collection
60- *
61 * @param collection - MongoDB collection
62 * @param schema - Zod schema for validation
63 * @param data - Array of document data to insert
···68 collection: Collection<Infer<T>>,
69 schema: T,
70 data: Input<T>[],
71- options?: BulkWriteOptions
72): Promise<InsertManyResult<Infer<T>>> {
73 const validatedData = data.map((item) => parse(schema, item));
74 return await collection.insertMany(
75 validatedData as OptionalUnlessRequiredId<Infer<T>>[],
76- options
77 );
78}
7980/**
81 * Find multiple documents matching the query
82- *
83 * @param collection - MongoDB collection
84 * @param query - MongoDB query filter
85 * @param options - Find options (including session for transactions)
···88export async function find<T extends Schema>(
89 collection: Collection<Infer<T>>,
90 query: Filter<Infer<T>>,
91- options?: FindOptions
92): Promise<(WithId<Infer<T>>)[]> {
93 return await collection.find(query, options).toArray();
94}
9596/**
97 * Find a single document matching the query
98- *
99 * @param collection - MongoDB collection
100 * @param query - MongoDB query filter
101 * @param options - Find options (including session for transactions)
···104export async function findOne<T extends Schema>(
105 collection: Collection<Infer<T>>,
106 query: Filter<Infer<T>>,
107- options?: FindOptions
108): Promise<WithId<Infer<T>> | null> {
109 return await collection.findOne(query, options);
110}
111112/**
113 * Find a document by its MongoDB ObjectId
114- *
115 * @param collection - MongoDB collection
116 * @param id - Document ID (string or ObjectId)
117 * @param options - Find options (including session for transactions)
···120export async function findById<T extends Schema>(
121 collection: Collection<Infer<T>>,
122 id: string | ObjectId,
123- options?: FindOptions
124): Promise<WithId<Infer<T>> | null> {
125 const objectId = typeof id === "string" ? new ObjectId(id) : id;
126- return await findOne(collection, { _id: objectId } as Filter<Infer<T>>, options);
0000127}
128129/**
130 * Update multiple documents matching the query
131- *
132 * Case handling:
133 * - If upsert: false (or undefined) → Normal update, no defaults applied
134 * - If upsert: true → Defaults added to $setOnInsert for new document creation
135- *
136 * @param collection - MongoDB collection
137 * @param schema - Zod schema for validation
138 * @param query - MongoDB query filter
···145 schema: T,
146 query: Filter<Infer<T>>,
147 data: Partial<z.infer<T>>,
148- options?: UpdateOptions
149): Promise<UpdateResult<Infer<T>>> {
150 const validatedData = parsePartial(schema, data);
151- let updateDoc: UpdateFilter<Infer<T>> = { $set: validatedData as Partial<Infer<T>> };
152-00153 // If this is an upsert, apply defaults using $setOnInsert
154 if (options?.upsert) {
155 updateDoc = applyDefaultsForUpsert(schema, query, updateDoc);
156 }
157-158 return await collection.updateMany(query, updateDoc, options);
159}
160161/**
162 * Update a single document matching the query
163- *
164 * Case handling:
165 * - If upsert: false (or undefined) → Normal update, no defaults applied
166 * - If upsert: true → Defaults added to $setOnInsert for new document creation
167- *
168 * @param collection - MongoDB collection
169 * @param schema - Zod schema for validation
170 * @param query - MongoDB query filter
···177 schema: T,
178 query: Filter<Infer<T>>,
179 data: Partial<z.infer<T>>,
180- options?: UpdateOptions
181): Promise<UpdateResult<Infer<T>>> {
182 const validatedData = parsePartial(schema, data);
183- let updateDoc: UpdateFilter<Infer<T>> = { $set: validatedData as Partial<Infer<T>> };
184-00185 // If this is an upsert, apply defaults using $setOnInsert
186 if (options?.upsert) {
187 updateDoc = applyDefaultsForUpsert(schema, query, updateDoc);
188 }
189-190 return await collection.updateOne(query, updateDoc, options);
191}
192193/**
194 * Replace a single document matching the query
195- *
196 * Case handling:
197 * - If upsert: false (or undefined) → Normal replace on existing doc, no additional defaults
198 * - If upsert: true → Defaults applied via parse() since we're passing a full document
199- *
200 * Note: For replace operations, defaults are automatically applied by the schema's
201 * parse() function which treats missing fields as candidates for defaults. This works
202 * for both regular replaces and upsert-creates since we're providing a full document.
203- *
204 * @param collection - MongoDB collection
205 * @param schema - Zod schema for validation
206 * @param query - MongoDB query filter
···213 schema: T,
214 query: Filter<Infer<T>>,
215 data: Input<T>,
216- options?: ReplaceOptions
217): Promise<UpdateResult<Infer<T>>> {
218 // parseReplace will apply all schema defaults to missing fields
219 // This works correctly for both regular replaces and upsert-created documents
220 const validatedData = parseReplace(schema, data);
221-222 // Remove _id from validatedData for replaceOne (it will use the query's _id)
223 const { _id, ...withoutId } = validatedData as Infer<T> & { _id?: unknown };
224 return await collection.replaceOne(
225 query,
226 withoutId as Infer<T>,
227- options
228 );
229}
230231/**
232 * Find a single document and update it
233- *
234 * Case handling:
235 * - If upsert: false (or undefined) → Normal update
236 * - If upsert: true → Defaults added to $setOnInsert for new document creation
···240 schema: T,
241 query: Filter<Infer<T>>,
242 data: Partial<z.infer<T>>,
243- options?: FindOneAndUpdateOptions
244): Promise<ModifyResult<Infer<T>>> {
245 const validatedData = parsePartial(schema, data);
246- let updateDoc: UpdateFilter<Infer<T>> = { $set: validatedData as Partial<Infer<T>> };
00247248 if (options?.upsert) {
249 updateDoc = applyDefaultsForUpsert(schema, query, updateDoc);
250 }
251252- const resolvedOptions: FindOneAndUpdateOptions & { includeResultMetadata: true } = {
00253 ...(options ?? {}),
254 includeResultMetadata: true as const,
255 };
···259260/**
261 * Find a single document and replace it
262- *
263 * Defaults are applied via parseReplace(), which fills in missing fields
264 * for both normal replacements and upsert-created documents.
265 */
···268 schema: T,
269 query: Filter<Infer<T>>,
270 data: Input<T>,
271- options?: FindOneAndReplaceOptions
272): Promise<ModifyResult<Infer<T>>> {
273 const validatedData = parseReplace(schema, data);
274 const { _id, ...withoutId } = validatedData as Infer<T> & { _id?: unknown };
275276- const resolvedOptions: FindOneAndReplaceOptions & { includeResultMetadata: true } = {
00277 ...(options ?? {}),
278 includeResultMetadata: true as const,
279 };
···281 return await collection.findOneAndReplace(
282 query,
283 withoutId as Infer<T>,
284- resolvedOptions
285 );
286}
287288/**
289 * Delete multiple documents matching the query
290- *
291 * @param collection - MongoDB collection
292 * @param query - MongoDB query filter
293 * @param options - Delete options (including session for transactions)
···296export async function deleteMany<T extends Schema>(
297 collection: Collection<Infer<T>>,
298 query: Filter<Infer<T>>,
299- options?: DeleteOptions
300): Promise<DeleteResult> {
301 return await collection.deleteMany(query, options);
302}
303304/**
305 * Delete a single document matching the query
306- *
307 * @param collection - MongoDB collection
308 * @param query - MongoDB query filter
309 * @param options - Delete options (including session for transactions)
···312export async function deleteOne<T extends Schema>(
313 collection: Collection<Infer<T>>,
314 query: Filter<Infer<T>>,
315- options?: DeleteOptions
316): Promise<DeleteResult> {
317 return await collection.deleteOne(query, options);
318}
319320/**
321 * Count documents matching the query
322- *
323 * @param collection - MongoDB collection
324 * @param query - MongoDB query filter
325 * @param options - Count options (including session for transactions)
···328export async function count<T extends Schema>(
329 collection: Collection<Infer<T>>,
330 query: Filter<Infer<T>>,
331- options?: CountDocumentsOptions
332): Promise<number> {
333 return await collection.countDocuments(query, options);
334}
335336/**
337 * Execute an aggregation pipeline
338- *
339 * @param collection - MongoDB collection
340 * @param pipeline - MongoDB aggregation pipeline
341 * @param options - Aggregate options (including session for transactions)
···344export async function aggregate<T extends Schema>(
345 collection: Collection<Infer<T>>,
346 pipeline: Document[],
347- options?: AggregateOptions
348): Promise<Document[]> {
349 return await collection.aggregate(pipeline, options).toArray();
350}
···1import type { z } from "@zod/zod";
2import type {
3+ AggregateOptions,
4+ BulkWriteOptions,
5 Collection,
6+ CountDocumentsOptions,
7+ DeleteOptions,
8 DeleteResult,
9 Document,
10 Filter,
11+ FindOneAndReplaceOptions,
12+ FindOneAndUpdateOptions,
13+ FindOptions,
14 InsertManyResult,
015 InsertOneOptions,
16+ InsertOneResult,
17+ ModifyResult,
00000018 OptionalUnlessRequiredId,
19+ ReplaceOptions,
20+ UpdateFilter,
21+ UpdateOptions,
22 UpdateResult,
23 WithId,
00024} from "mongodb";
25import { ObjectId } from "mongodb";
26+import type { Infer, Input, Schema } from "../types.ts";
27+import {
28+ applyDefaultsForUpsert,
29+ parse,
30+ parsePartial,
31+ parseReplace,
32+} from "./validation.ts";
3334/**
35 * Core CRUD operations for the Model class
36+ *
37 * This module contains all basic create, read, update, and delete operations
38 * with automatic Zod validation and transaction support.
39 */
4041/**
42 * Insert a single document into the collection
43+ *
44 * @param collection - MongoDB collection
45 * @param schema - Zod schema for validation
46 * @param data - Document data to insert
···51 collection: Collection<Infer<T>>,
52 schema: T,
53 data: Input<T>,
54+ options?: InsertOneOptions,
55): Promise<InsertOneResult<Infer<T>>> {
56 const validatedData = parse(schema, data);
57 return await collection.insertOne(
58 validatedData as OptionalUnlessRequiredId<Infer<T>>,
59+ options,
60 );
61}
6263/**
64 * Insert multiple documents into the collection
65+ *
66 * @param collection - MongoDB collection
67 * @param schema - Zod schema for validation
68 * @param data - Array of document data to insert
···73 collection: Collection<Infer<T>>,
74 schema: T,
75 data: Input<T>[],
76+ options?: BulkWriteOptions,
77): Promise<InsertManyResult<Infer<T>>> {
78 const validatedData = data.map((item) => parse(schema, item));
79 return await collection.insertMany(
80 validatedData as OptionalUnlessRequiredId<Infer<T>>[],
81+ options,
82 );
83}
8485/**
86 * Find multiple documents matching the query
87+ *
88 * @param collection - MongoDB collection
89 * @param query - MongoDB query filter
90 * @param options - Find options (including session for transactions)
···93export async function find<T extends Schema>(
94 collection: Collection<Infer<T>>,
95 query: Filter<Infer<T>>,
96+ options?: FindOptions,
97): Promise<(WithId<Infer<T>>)[]> {
98 return await collection.find(query, options).toArray();
99}
100101/**
102 * Find a single document matching the query
103+ *
104 * @param collection - MongoDB collection
105 * @param query - MongoDB query filter
106 * @param options - Find options (including session for transactions)
···109export async function findOne<T extends Schema>(
110 collection: Collection<Infer<T>>,
111 query: Filter<Infer<T>>,
112+ options?: FindOptions,
113): Promise<WithId<Infer<T>> | null> {
114 return await collection.findOne(query, options);
115}
116117/**
118 * Find a document by its MongoDB ObjectId
119+ *
120 * @param collection - MongoDB collection
121 * @param id - Document ID (string or ObjectId)
122 * @param options - Find options (including session for transactions)
···125export async function findById<T extends Schema>(
126 collection: Collection<Infer<T>>,
127 id: string | ObjectId,
128+ options?: FindOptions,
129): Promise<WithId<Infer<T>> | null> {
130 const objectId = typeof id === "string" ? new ObjectId(id) : id;
131+ return await findOne(
132+ collection,
133+ { _id: objectId } as Filter<Infer<T>>,
134+ options,
135+ );
136}
137138/**
139 * Update multiple documents matching the query
140+ *
141 * Case handling:
142 * - If upsert: false (or undefined) → Normal update, no defaults applied
143 * - If upsert: true → Defaults added to $setOnInsert for new document creation
144+ *
145 * @param collection - MongoDB collection
146 * @param schema - Zod schema for validation
147 * @param query - MongoDB query filter
···154 schema: T,
155 query: Filter<Infer<T>>,
156 data: Partial<z.infer<T>>,
157+ options?: UpdateOptions,
158): Promise<UpdateResult<Infer<T>>> {
159 const validatedData = parsePartial(schema, data);
160+ let updateDoc: UpdateFilter<Infer<T>> = {
161+ $set: validatedData as Partial<Infer<T>>,
162+ };
163+164 // If this is an upsert, apply defaults using $setOnInsert
165 if (options?.upsert) {
166 updateDoc = applyDefaultsForUpsert(schema, query, updateDoc);
167 }
168+169 return await collection.updateMany(query, updateDoc, options);
170}
171172/**
173 * Update a single document matching the query
174+ *
175 * Case handling:
176 * - If upsert: false (or undefined) → Normal update, no defaults applied
177 * - If upsert: true → Defaults added to $setOnInsert for new document creation
178+ *
179 * @param collection - MongoDB collection
180 * @param schema - Zod schema for validation
181 * @param query - MongoDB query filter
···188 schema: T,
189 query: Filter<Infer<T>>,
190 data: Partial<z.infer<T>>,
191+ options?: UpdateOptions,
192): Promise<UpdateResult<Infer<T>>> {
193 const validatedData = parsePartial(schema, data);
194+ let updateDoc: UpdateFilter<Infer<T>> = {
195+ $set: validatedData as Partial<Infer<T>>,
196+ };
197+198 // If this is an upsert, apply defaults using $setOnInsert
199 if (options?.upsert) {
200 updateDoc = applyDefaultsForUpsert(schema, query, updateDoc);
201 }
202+203 return await collection.updateOne(query, updateDoc, options);
204}
205206/**
207 * Replace a single document matching the query
208+ *
209 * Case handling:
210 * - If upsert: false (or undefined) → Normal replace on existing doc, no additional defaults
211 * - If upsert: true → Defaults applied via parse() since we're passing a full document
212+ *
213 * Note: For replace operations, defaults are automatically applied by the schema's
214 * parse() function which treats missing fields as candidates for defaults. This works
215 * for both regular replaces and upsert-creates since we're providing a full document.
216+ *
217 * @param collection - MongoDB collection
218 * @param schema - Zod schema for validation
219 * @param query - MongoDB query filter
···226 schema: T,
227 query: Filter<Infer<T>>,
228 data: Input<T>,
229+ options?: ReplaceOptions,
230): Promise<UpdateResult<Infer<T>>> {
231 // parseReplace will apply all schema defaults to missing fields
232 // This works correctly for both regular replaces and upsert-created documents
233 const validatedData = parseReplace(schema, data);
234+235 // Remove _id from validatedData for replaceOne (it will use the query's _id)
236 const { _id, ...withoutId } = validatedData as Infer<T> & { _id?: unknown };
237 return await collection.replaceOne(
238 query,
239 withoutId as Infer<T>,
240+ options,
241 );
242}
243244/**
245 * Find a single document and update it
246+ *
247 * Case handling:
248 * - If upsert: false (or undefined) → Normal update
249 * - If upsert: true → Defaults added to $setOnInsert for new document creation
···253 schema: T,
254 query: Filter<Infer<T>>,
255 data: Partial<z.infer<T>>,
256+ options?: FindOneAndUpdateOptions,
257): Promise<ModifyResult<Infer<T>>> {
258 const validatedData = parsePartial(schema, data);
259+ let updateDoc: UpdateFilter<Infer<T>> = {
260+ $set: validatedData as Partial<Infer<T>>,
261+ };
262263 if (options?.upsert) {
264 updateDoc = applyDefaultsForUpsert(schema, query, updateDoc);
265 }
266267+ const resolvedOptions: FindOneAndUpdateOptions & {
268+ includeResultMetadata: true;
269+ } = {
270 ...(options ?? {}),
271 includeResultMetadata: true as const,
272 };
···276277/**
278 * Find a single document and replace it
279+ *
280 * Defaults are applied via parseReplace(), which fills in missing fields
281 * for both normal replacements and upsert-created documents.
282 */
···285 schema: T,
286 query: Filter<Infer<T>>,
287 data: Input<T>,
288+ options?: FindOneAndReplaceOptions,
289): Promise<ModifyResult<Infer<T>>> {
290 const validatedData = parseReplace(schema, data);
291 const { _id, ...withoutId } = validatedData as Infer<T> & { _id?: unknown };
292293+ const resolvedOptions: FindOneAndReplaceOptions & {
294+ includeResultMetadata: true;
295+ } = {
296 ...(options ?? {}),
297 includeResultMetadata: true as const,
298 };
···300 return await collection.findOneAndReplace(
301 query,
302 withoutId as Infer<T>,
303+ resolvedOptions,
304 );
305}
306307/**
308 * Delete multiple documents matching the query
309+ *
310 * @param collection - MongoDB collection
311 * @param query - MongoDB query filter
312 * @param options - Delete options (including session for transactions)
···315export async function deleteMany<T extends Schema>(
316 collection: Collection<Infer<T>>,
317 query: Filter<Infer<T>>,
318+ options?: DeleteOptions,
319): Promise<DeleteResult> {
320 return await collection.deleteMany(query, options);
321}
322323/**
324 * Delete a single document matching the query
325+ *
326 * @param collection - MongoDB collection
327 * @param query - MongoDB query filter
328 * @param options - Delete options (including session for transactions)
···331export async function deleteOne<T extends Schema>(
332 collection: Collection<Infer<T>>,
333 query: Filter<Infer<T>>,
334+ options?: DeleteOptions,
335): Promise<DeleteResult> {
336 return await collection.deleteOne(query, options);
337}
338339/**
340 * Count documents matching the query
341+ *
342 * @param collection - MongoDB collection
343 * @param query - MongoDB query filter
344 * @param options - Count options (including session for transactions)
···347export async function count<T extends Schema>(
348 collection: Collection<Infer<T>>,
349 query: Filter<Infer<T>>,
350+ options?: CountDocumentsOptions,
351): Promise<number> {
352 return await collection.countDocuments(query, options);
353}
354355/**
356 * Execute an aggregation pipeline
357+ *
358 * @param collection - MongoDB collection
359 * @param pipeline - MongoDB aggregation pipeline
360 * @param options - Aggregate options (including session for transactions)
···363export async function aggregate<T extends Schema>(
364 collection: Collection<Infer<T>>,
365 pipeline: Document[],
366+ options?: AggregateOptions,
367): Promise<Document[]> {
368 return await collection.aggregate(pipeline, options).toArray();
369}
+90-60
model/index.ts
···1import type { z } from "@zod/zod";
2import type {
003 Collection,
04 CreateIndexesOptions,
05 DeleteResult,
6 Document,
7 DropIndexesOptions,
8 Filter,
0009 IndexDescription,
10 IndexSpecification,
11 InsertManyResult,
012 InsertOneResult,
13- InsertOneOptions,
14- FindOptions,
15- UpdateOptions,
16 ReplaceOptions,
17- FindOneAndUpdateOptions,
18- FindOneAndReplaceOptions,
19- DeleteOptions,
20- CountDocumentsOptions,
21- AggregateOptions,
22- ListIndexesOptions,
23 UpdateResult,
24 WithId,
25- BulkWriteOptions,
26- ModifyResult,
27} from "mongodb";
28import type { ObjectId } from "mongodb";
29import { getDb } from "../client/connection.ts";
30-import type { Schema, Infer, Input, Indexes, ModelDef } from "../types.ts";
31import * as core from "./core.ts";
32import * as indexes from "./indexes.ts";
33import * as pagination from "./pagination.ts";
3435/**
36 * Model class for type-safe MongoDB operations
37- *
38 * Provides a clean API for CRUD operations, pagination, and index management
39 * with automatic Zod validation and TypeScript type safety.
40- *
41 * @example
42 * ```ts
43 * const userSchema = z.object({
44 * name: z.string(),
45 * email: z.string().email(),
46 * });
47- *
48 * const UserModel = new Model("users", userSchema);
49 * await UserModel.insertOne({ name: "Alice", email: "alice@example.com" });
50 * ```
···62 this.schema = definition as T;
63 }
64 this.collection = getDb().collection<Infer<T>>(collectionName);
65-66 // Automatically create indexes if they were provided
67 if (this.indexes && this.indexes.length > 0) {
68 // Fire and forget - indexes will be created asynchronously
69- indexes.syncIndexes(this.collection, this.indexes)
70 }
71 }
72···7677 /**
78 * Insert a single document into the collection
79- *
80 * @param data - Document data to insert
81 * @param options - Insert options (including session for transactions)
82 * @returns Insert result with insertedId
83 */
84 async insertOne(
85 data: Input<T>,
86- options?: InsertOneOptions
87 ): Promise<InsertOneResult<Infer<T>>> {
88 return await core.insertOne(this.collection, this.schema, data, options);
89 }
9091 /**
92 * Insert multiple documents into the collection
93- *
94 * @param data - Array of document data to insert
95 * @param options - Insert options (including session for transactions)
96 * @returns Insert result with insertedIds
97 */
98 async insertMany(
99 data: Input<T>[],
100- options?: BulkWriteOptions
101 ): Promise<InsertManyResult<Infer<T>>> {
102 return await core.insertMany(this.collection, this.schema, data, options);
103 }
104105 /**
106 * Find multiple documents matching the query
107- *
108 * @param query - MongoDB query filter
109 * @param options - Find options (including session for transactions)
110 * @returns Array of matching documents
111 */
112 async find(
113 query: Filter<Infer<T>>,
114- options?: FindOptions
115 ): Promise<(WithId<Infer<T>>)[]> {
116 return await core.find(this.collection, query, options);
117 }
118119 /**
120 * Find a single document matching the query
121- *
122 * @param query - MongoDB query filter
123 * @param options - Find options (including session for transactions)
124 * @returns Matching document or null if not found
125 */
126 async findOne(
127 query: Filter<Infer<T>>,
128- options?: FindOptions
129 ): Promise<WithId<Infer<T>> | null> {
130 return await core.findOne(this.collection, query, options);
131 }
132133 /**
134 * Find a document by its MongoDB ObjectId
135- *
136 * @param id - Document ID (string or ObjectId)
137 * @param options - Find options (including session for transactions)
138 * @returns Matching document or null if not found
139 */
140 async findById(
141 id: string | ObjectId,
142- options?: FindOptions
143 ): Promise<WithId<Infer<T>> | null> {
144 return await core.findById(this.collection, id, options);
145 }
146147 /**
148 * Update multiple documents matching the query
149- *
150 * @param query - MongoDB query filter
151 * @param data - Partial data to update
152 * @param options - Update options (including session for transactions)
···155 async update(
156 query: Filter<Infer<T>>,
157 data: Partial<z.infer<T>>,
158- options?: UpdateOptions
159 ): Promise<UpdateResult<Infer<T>>> {
160- return await core.update(this.collection, this.schema, query, data, options);
000000161 }
162163 /**
164 * Update a single document matching the query
165- *
166 * @param query - MongoDB query filter
167 * @param data - Partial data to update
168 * @param options - Update options (including session for transactions)
···171 async updateOne(
172 query: Filter<Infer<T>>,
173 data: Partial<z.infer<T>>,
174- options?: UpdateOptions
175 ): Promise<UpdateResult<Infer<T>>> {
176- return await core.updateOne(this.collection, this.schema, query, data, options);
000000177 }
178179 /**
180 * Find a single document and update it
181- *
182 * @param query - MongoDB query filter
183 * @param data - Partial data to update
184 * @param options - FindOneAndUpdate options (including upsert and returnDocument)
···187 async findOneAndUpdate(
188 query: Filter<Infer<T>>,
189 data: Partial<z.infer<T>>,
190- options?: FindOneAndUpdateOptions
191 ): Promise<ModifyResult<Infer<T>>> {
192- return await core.findOneAndUpdate(this.collection, this.schema, query, data, options);
000000193 }
194195 /**
196 * Replace a single document matching the query
197- *
198 * @param query - MongoDB query filter
199 * @param data - Complete document data for replacement
200 * @param options - Replace options (including session for transactions)
···203 async replaceOne(
204 query: Filter<Infer<T>>,
205 data: Input<T>,
206- options?: ReplaceOptions
207 ): Promise<UpdateResult<Infer<T>>> {
208- return await core.replaceOne(this.collection, this.schema, query, data, options);
000000209 }
210211 /**
212 * Find a single document and replace it
213- *
214 * @param query - MongoDB query filter
215 * @param data - Complete document data for replacement
216 * @param options - FindOneAndReplace options (including upsert and returnDocument)
···219 async findOneAndReplace(
220 query: Filter<Infer<T>>,
221 data: Input<T>,
222- options?: FindOneAndReplaceOptions
223 ): Promise<ModifyResult<Infer<T>>> {
224- return await core.findOneAndReplace(this.collection, this.schema, query, data, options);
000000225 }
226227 /**
228 * Delete multiple documents matching the query
229- *
230 * @param query - MongoDB query filter
231 * @param options - Delete options (including session for transactions)
232 * @returns Delete result
233 */
234 async delete(
235 query: Filter<Infer<T>>,
236- options?: DeleteOptions
237 ): Promise<DeleteResult> {
238 return await core.deleteMany(this.collection, query, options);
239 }
240241 /**
242 * Delete a single document matching the query
243- *
244 * @param query - MongoDB query filter
245 * @param options - Delete options (including session for transactions)
246 * @returns Delete result
247 */
248 async deleteOne(
249 query: Filter<Infer<T>>,
250- options?: DeleteOptions
251 ): Promise<DeleteResult> {
252 return await core.deleteOne(this.collection, query, options);
253 }
254255 /**
256 * Count documents matching the query
257- *
258 * @param query - MongoDB query filter
259 * @param options - Count options (including session for transactions)
260 * @returns Number of matching documents
261 */
262 async count(
263 query: Filter<Infer<T>>,
264- options?: CountDocumentsOptions
265 ): Promise<number> {
266 return await core.count(this.collection, query, options);
267 }
268269 /**
270 * Execute an aggregation pipeline
271- *
272 * @param pipeline - MongoDB aggregation pipeline
273 * @param options - Aggregate options (including session for transactions)
274 * @returns Array of aggregation results
275 */
276 async aggregate(
277 pipeline: Document[],
278- options?: AggregateOptions
279 ): Promise<Document[]> {
280 return await core.aggregate(this.collection, pipeline, options);
281 }
···286287 /**
288 * Find documents with pagination support
289- *
290 * @param query - MongoDB query filter
291 * @param options - Pagination options (skip, limit, sort)
292 * @returns Array of matching documents
···304305 /**
306 * Create a single index on the collection
307- *
308 * @param keys - Index specification (e.g., { email: 1 } or { name: "text" })
309 * @param options - Index creation options (unique, sparse, expireAfterSeconds, etc.)
310 * @returns The name of the created index
···318319 /**
320 * Create multiple indexes on the collection
321- *
322 * @param indexes - Array of index descriptions
323 * @param options - Index creation options
324 * @returns Array of index names created
···332333 /**
334 * Drop a single index from the collection
335- *
336 * @param index - Index name or specification
337 * @param options - Drop index options
338 */
···345346 /**
347 * Drop all indexes from the collection (except _id index)
348- *
349 * @param options - Drop index options
350 */
351 async dropIndexes(options?: DropIndexesOptions): Promise<void> {
···354355 /**
356 * List all indexes on the collection
357- *
358 * @param options - List indexes options
359 * @returns Array of index information
360 */
···366367 /**
368 * Get index information by name
369- *
370 * @param indexName - Name of the index
371 * @returns Index description or null if not found
372 */
···376377 /**
378 * Check if an index exists
379- *
380 * @param indexName - Name of the index
381 * @returns True if index exists, false otherwise
382 */
···386387 /**
388 * Synchronize indexes - create indexes if they don't exist, update if they differ
389- *
390 * This is useful for ensuring indexes match your schema definition
391- *
392 * @param indexes - Array of index descriptions to synchronize
393 * @param options - Options for index creation
394 * @returns Array of index names that were created
···1import type { z } from "@zod/zod";
2import type {
3+ AggregateOptions,
4+ BulkWriteOptions,
5 Collection,
6+ CountDocumentsOptions,
7 CreateIndexesOptions,
8+ DeleteOptions,
9 DeleteResult,
10 Document,
11 DropIndexesOptions,
12 Filter,
13+ FindOneAndReplaceOptions,
14+ FindOneAndUpdateOptions,
15+ FindOptions,
16 IndexDescription,
17 IndexSpecification,
18 InsertManyResult,
19+ InsertOneOptions,
20 InsertOneResult,
21+ ListIndexesOptions,
22+ ModifyResult,
023 ReplaceOptions,
24+ UpdateOptions,
0000025 UpdateResult,
26 WithId,
0027} from "mongodb";
28import type { ObjectId } from "mongodb";
29import { getDb } from "../client/connection.ts";
30+import type { Indexes, Infer, Input, ModelDef, Schema } from "../types.ts";
31import * as core from "./core.ts";
32import * as indexes from "./indexes.ts";
33import * as pagination from "./pagination.ts";
3435/**
36 * Model class for type-safe MongoDB operations
37+ *
38 * Provides a clean API for CRUD operations, pagination, and index management
39 * with automatic Zod validation and TypeScript type safety.
40+ *
41 * @example
42 * ```ts
43 * const userSchema = z.object({
44 * name: z.string(),
45 * email: z.string().email(),
46 * });
47+ *
48 * const UserModel = new Model("users", userSchema);
49 * await UserModel.insertOne({ name: "Alice", email: "alice@example.com" });
50 * ```
···62 this.schema = definition as T;
63 }
64 this.collection = getDb().collection<Infer<T>>(collectionName);
65+66 // Automatically create indexes if they were provided
67 if (this.indexes && this.indexes.length > 0) {
68 // Fire and forget - indexes will be created asynchronously
69+ indexes.syncIndexes(this.collection, this.indexes);
70 }
71 }
72···7677 /**
78 * Insert a single document into the collection
79+ *
80 * @param data - Document data to insert
81 * @param options - Insert options (including session for transactions)
82 * @returns Insert result with insertedId
83 */
84 async insertOne(
85 data: Input<T>,
86+ options?: InsertOneOptions,
87 ): Promise<InsertOneResult<Infer<T>>> {
88 return await core.insertOne(this.collection, this.schema, data, options);
89 }
9091 /**
92 * Insert multiple documents into the collection
93+ *
94 * @param data - Array of document data to insert
95 * @param options - Insert options (including session for transactions)
96 * @returns Insert result with insertedIds
97 */
98 async insertMany(
99 data: Input<T>[],
100+ options?: BulkWriteOptions,
101 ): Promise<InsertManyResult<Infer<T>>> {
102 return await core.insertMany(this.collection, this.schema, data, options);
103 }
104105 /**
106 * Find multiple documents matching the query
107+ *
108 * @param query - MongoDB query filter
109 * @param options - Find options (including session for transactions)
110 * @returns Array of matching documents
111 */
112 async find(
113 query: Filter<Infer<T>>,
114+ options?: FindOptions,
115 ): Promise<(WithId<Infer<T>>)[]> {
116 return await core.find(this.collection, query, options);
117 }
118119 /**
120 * Find a single document matching the query
121+ *
122 * @param query - MongoDB query filter
123 * @param options - Find options (including session for transactions)
124 * @returns Matching document or null if not found
125 */
126 async findOne(
127 query: Filter<Infer<T>>,
128+ options?: FindOptions,
129 ): Promise<WithId<Infer<T>> | null> {
130 return await core.findOne(this.collection, query, options);
131 }
132133 /**
134 * Find a document by its MongoDB ObjectId
135+ *
136 * @param id - Document ID (string or ObjectId)
137 * @param options - Find options (including session for transactions)
138 * @returns Matching document or null if not found
139 */
140 async findById(
141 id: string | ObjectId,
142+ options?: FindOptions,
143 ): Promise<WithId<Infer<T>> | null> {
144 return await core.findById(this.collection, id, options);
145 }
146147 /**
148 * Update multiple documents matching the query
149+ *
150 * @param query - MongoDB query filter
151 * @param data - Partial data to update
152 * @param options - Update options (including session for transactions)
···155 async update(
156 query: Filter<Infer<T>>,
157 data: Partial<z.infer<T>>,
158+ options?: UpdateOptions,
159 ): Promise<UpdateResult<Infer<T>>> {
160+ return await core.update(
161+ this.collection,
162+ this.schema,
163+ query,
164+ data,
165+ options,
166+ );
167 }
168169 /**
170 * Update a single document matching the query
171+ *
172 * @param query - MongoDB query filter
173 * @param data - Partial data to update
174 * @param options - Update options (including session for transactions)
···177 async updateOne(
178 query: Filter<Infer<T>>,
179 data: Partial<z.infer<T>>,
180+ options?: UpdateOptions,
181 ): Promise<UpdateResult<Infer<T>>> {
182+ return await core.updateOne(
183+ this.collection,
184+ this.schema,
185+ query,
186+ data,
187+ options,
188+ );
189 }
190191 /**
192 * Find a single document and update it
193+ *
194 * @param query - MongoDB query filter
195 * @param data - Partial data to update
196 * @param options - FindOneAndUpdate options (including upsert and returnDocument)
···199 async findOneAndUpdate(
200 query: Filter<Infer<T>>,
201 data: Partial<z.infer<T>>,
202+ options?: FindOneAndUpdateOptions,
203 ): Promise<ModifyResult<Infer<T>>> {
204+ return await core.findOneAndUpdate(
205+ this.collection,
206+ this.schema,
207+ query,
208+ data,
209+ options,
210+ );
211 }
212213 /**
214 * Replace a single document matching the query
215+ *
216 * @param query - MongoDB query filter
217 * @param data - Complete document data for replacement
218 * @param options - Replace options (including session for transactions)
···221 async replaceOne(
222 query: Filter<Infer<T>>,
223 data: Input<T>,
224+ options?: ReplaceOptions,
225 ): Promise<UpdateResult<Infer<T>>> {
226+ return await core.replaceOne(
227+ this.collection,
228+ this.schema,
229+ query,
230+ data,
231+ options,
232+ );
233 }
234235 /**
236 * Find a single document and replace it
237+ *
238 * @param query - MongoDB query filter
239 * @param data - Complete document data for replacement
240 * @param options - FindOneAndReplace options (including upsert and returnDocument)
···243 async findOneAndReplace(
244 query: Filter<Infer<T>>,
245 data: Input<T>,
246+ options?: FindOneAndReplaceOptions,
247 ): Promise<ModifyResult<Infer<T>>> {
248+ return await core.findOneAndReplace(
249+ this.collection,
250+ this.schema,
251+ query,
252+ data,
253+ options,
254+ );
255 }
256257 /**
258 * Delete multiple documents matching the query
259+ *
260 * @param query - MongoDB query filter
261 * @param options - Delete options (including session for transactions)
262 * @returns Delete result
263 */
264 async delete(
265 query: Filter<Infer<T>>,
266+ options?: DeleteOptions,
267 ): Promise<DeleteResult> {
268 return await core.deleteMany(this.collection, query, options);
269 }
270271 /**
272 * Delete a single document matching the query
273+ *
274 * @param query - MongoDB query filter
275 * @param options - Delete options (including session for transactions)
276 * @returns Delete result
277 */
278 async deleteOne(
279 query: Filter<Infer<T>>,
280+ options?: DeleteOptions,
281 ): Promise<DeleteResult> {
282 return await core.deleteOne(this.collection, query, options);
283 }
284285 /**
286 * Count documents matching the query
287+ *
288 * @param query - MongoDB query filter
289 * @param options - Count options (including session for transactions)
290 * @returns Number of matching documents
291 */
292 async count(
293 query: Filter<Infer<T>>,
294+ options?: CountDocumentsOptions,
295 ): Promise<number> {
296 return await core.count(this.collection, query, options);
297 }
298299 /**
300 * Execute an aggregation pipeline
301+ *
302 * @param pipeline - MongoDB aggregation pipeline
303 * @param options - Aggregate options (including session for transactions)
304 * @returns Array of aggregation results
305 */
306 async aggregate(
307 pipeline: Document[],
308+ options?: AggregateOptions,
309 ): Promise<Document[]> {
310 return await core.aggregate(this.collection, pipeline, options);
311 }
···316317 /**
318 * Find documents with pagination support
319+ *
320 * @param query - MongoDB query filter
321 * @param options - Pagination options (skip, limit, sort)
322 * @returns Array of matching documents
···334335 /**
336 * Create a single index on the collection
337+ *
338 * @param keys - Index specification (e.g., { email: 1 } or { name: "text" })
339 * @param options - Index creation options (unique, sparse, expireAfterSeconds, etc.)
340 * @returns The name of the created index
···348349 /**
350 * Create multiple indexes on the collection
351+ *
352 * @param indexes - Array of index descriptions
353 * @param options - Index creation options
354 * @returns Array of index names created
···362363 /**
364 * Drop a single index from the collection
365+ *
366 * @param index - Index name or specification
367 * @param options - Drop index options
368 */
···375376 /**
377 * Drop all indexes from the collection (except _id index)
378+ *
379 * @param options - Drop index options
380 */
381 async dropIndexes(options?: DropIndexesOptions): Promise<void> {
···384385 /**
386 * List all indexes on the collection
387+ *
388 * @param options - List indexes options
389 * @returns Array of index information
390 */
···396397 /**
398 * Get index information by name
399+ *
400 * @param indexName - Name of the index
401 * @returns Index description or null if not found
402 */
···406407 /**
408 * Check if an index exists
409+ *
410 * @param indexName - Name of the index
411 * @returns True if index exists, false otherwise
412 */
···416417 /**
418 * Synchronize indexes - create indexes if they don't exist, update if they differ
419+ *
420 * This is useful for ensuring indexes match your schema definition
421+ *
422 * @param indexes - Array of index descriptions to synchronize
423 * @param options - Options for index creation
424 * @returns Array of index names that were created
+15-15
model/indexes.ts
···6 IndexSpecification,
7 ListIndexesOptions,
8} from "mongodb";
9-import type { Schema, Infer } from "../types.ts";
1011/**
12 * Index management operations for the Model class
13- *
14 * This module contains all index-related operations including creation,
15 * deletion, listing, and synchronization of indexes.
16 */
1718/**
19 * Create a single index on the collection
20- *
21 * @param collection - MongoDB collection
22 * @param keys - Index specification (e.g., { email: 1 } or { name: "text" })
23 * @param options - Index creation options (unique, sparse, expireAfterSeconds, etc.)
···3334/**
35 * Create multiple indexes on the collection
36- *
37 * @param collection - MongoDB collection
38 * @param indexes - Array of index descriptions
39 * @param options - Index creation options
···4950/**
51 * Drop a single index from the collection
52- *
53 * @param collection - MongoDB collection
54 * @param index - Index name or specification
55 * @param options - Drop index options
···6465/**
66 * Drop all indexes from the collection (except _id index)
67- *
68 * @param collection - MongoDB collection
69 * @param options - Drop index options
70 */
71export async function dropIndexes<T extends Schema>(
72 collection: Collection<Infer<T>>,
73- options?: DropIndexesOptions
74): Promise<void> {
75 await collection.dropIndexes(options);
76}
7778/**
79 * List all indexes on the collection
80- *
81 * @param collection - MongoDB collection
82 * @param options - List indexes options
83 * @returns Array of index information
···9293/**
94 * Get index information by name
95- *
96 * @param collection - MongoDB collection
97 * @param indexName - Name of the index
98 * @returns Index description or null if not found
99 */
100export async function getIndex<T extends Schema>(
101 collection: Collection<Infer<T>>,
102- indexName: string
103): Promise<IndexDescription | null> {
104 const indexes = await listIndexes(collection);
105 return indexes.find((idx) => idx.name === indexName) || null;
···107108/**
109 * Check if an index exists
110- *
111 * @param collection - MongoDB collection
112 * @param indexName - Name of the index
113 * @returns True if index exists, false otherwise
114 */
115export async function indexExists<T extends Schema>(
116 collection: Collection<Infer<T>>,
117- indexName: string
118): Promise<boolean> {
119 const index = await getIndex(collection, indexName);
120 return index !== null;
···122123/**
124 * Synchronize indexes - create indexes if they don't exist, update if they differ
125- *
126 * This is useful for ensuring indexes match your schema definition
127- *
128 * @param collection - MongoDB collection
129 * @param indexes - Array of index descriptions to synchronize
130 * @param options - Options for index creation
···167168/**
169 * Generate index name from key specification
170- *
171 * @param keys - Index specification
172 * @returns Generated index name
173 */
···6 IndexSpecification,
7 ListIndexesOptions,
8} from "mongodb";
9+import type { Infer, Schema } from "../types.ts";
1011/**
12 * Index management operations for the Model class
13+ *
14 * This module contains all index-related operations including creation,
15 * deletion, listing, and synchronization of indexes.
16 */
1718/**
19 * Create a single index on the collection
20+ *
21 * @param collection - MongoDB collection
22 * @param keys - Index specification (e.g., { email: 1 } or { name: "text" })
23 * @param options - Index creation options (unique, sparse, expireAfterSeconds, etc.)
···3334/**
35 * Create multiple indexes on the collection
36+ *
37 * @param collection - MongoDB collection
38 * @param indexes - Array of index descriptions
39 * @param options - Index creation options
···4950/**
51 * Drop a single index from the collection
52+ *
53 * @param collection - MongoDB collection
54 * @param index - Index name or specification
55 * @param options - Drop index options
···6465/**
66 * Drop all indexes from the collection (except _id index)
67+ *
68 * @param collection - MongoDB collection
69 * @param options - Drop index options
70 */
71export async function dropIndexes<T extends Schema>(
72 collection: Collection<Infer<T>>,
73+ options?: DropIndexesOptions,
74): Promise<void> {
75 await collection.dropIndexes(options);
76}
7778/**
79 * List all indexes on the collection
80+ *
81 * @param collection - MongoDB collection
82 * @param options - List indexes options
83 * @returns Array of index information
···9293/**
94 * Get index information by name
95+ *
96 * @param collection - MongoDB collection
97 * @param indexName - Name of the index
98 * @returns Index description or null if not found
99 */
100export async function getIndex<T extends Schema>(
101 collection: Collection<Infer<T>>,
102+ indexName: string,
103): Promise<IndexDescription | null> {
104 const indexes = await listIndexes(collection);
105 return indexes.find((idx) => idx.name === indexName) || null;
···107108/**
109 * Check if an index exists
110+ *
111 * @param collection - MongoDB collection
112 * @param indexName - Name of the index
113 * @returns True if index exists, false otherwise
114 */
115export async function indexExists<T extends Schema>(
116 collection: Collection<Infer<T>>,
117+ indexName: string,
118): Promise<boolean> {
119 const index = await getIndex(collection, indexName);
120 return index !== null;
···122123/**
124 * Synchronize indexes - create indexes if they don't exist, update if they differ
125+ *
126 * This is useful for ensuring indexes match your schema definition
127+ *
128 * @param collection - MongoDB collection
129 * @param indexes - Array of index descriptions to synchronize
130 * @param options - Options for index creation
···167168/**
169 * Generate index name from key specification
170+ *
171 * @param keys - Index specification
172 * @returns Generated index name
173 */
+6-11
model/pagination.ts
···1-import type {
2- Collection,
3- Document,
4- Filter,
5- WithId,
6-} from "mongodb";
7-import type { Schema, Infer } from "../types.ts";
89/**
10 * Pagination operations for the Model class
11- *
12 * This module contains pagination-related functionality for finding documents
13 * with skip, limit, and sort options.
14 */
1516/**
17 * Find documents with pagination support
18- *
19 * @param collection - MongoDB collection
20 * @param query - MongoDB query filter
21 * @param options - Pagination options (skip, limit, sort)
22 * @returns Array of matching documents
23- *
24 * @example
25 * ```ts
26- * const users = await findPaginated(collection,
27 * { age: { $gte: 18 } },
28 * { skip: 0, limit: 10, sort: { createdAt: -1 } }
29 * );
···1+import type { Collection, Document, Filter, WithId } from "mongodb";
2+import type { Infer, Schema } from "../types.ts";
0000034/**
5 * Pagination operations for the Model class
6+ *
7 * This module contains pagination-related functionality for finding documents
8 * with skip, limit, and sort options.
9 */
1011/**
12 * Find documents with pagination support
13+ *
14 * @param collection - MongoDB collection
15 * @param query - MongoDB query filter
16 * @param options - Pagination options (skip, limit, sort)
17 * @returns Array of matching documents
18+ *
19 * @example
20 * ```ts
21+ * const users = await findPaginated(collection,
22 * { age: { $gte: 18 } },
23 * { skip: 0, limit: 10, sort: { createdAt: -1 } }
24 * );
+86-51
model/validation.ts
···1import type { z } from "@zod/zod";
2-import type { Schema, Infer, Input } from "../types.ts";
3-import { ValidationError, AsyncValidationError } from "../errors.ts";
4-import type { Document, UpdateFilter, Filter } from "mongodb";
000000000000000000000000000056/**
7 * Validate data for insert operations using Zod schema
8- *
9 * @param schema - Zod schema to validate against
10 * @param data - Data to validate
11 * @returns Validated and typed data
···14 */
15export function parse<T extends Schema>(schema: T, data: Input<T>): Infer<T> {
16 const result = schema.safeParse(data);
17-18 // Check for async validation
19 if (result instanceof Promise) {
20 throw new AsyncValidationError();
21 }
22-23 if (!result.success) {
24 throw new ValidationError(result.error.issues, "insert");
25 }
···2829/**
30 * Validate partial data for update operations using Zod schema
31- *
32 * Important: This function only validates the fields that are provided in the data object.
33 * Unlike parse(), this function does NOT apply defaults for missing fields because
34 * in an update context, missing fields should remain unchanged in the database.
35- *
36 * @param schema - Zod schema to validate against
37 * @param data - Partial data to validate
38 * @returns Validated and typed partial data (only fields present in input)
···43 schema: T,
44 data: Partial<z.infer<T>>,
45): Partial<z.infer<T>> {
000046 // Get the list of fields actually provided in the input
47 const inputKeys = Object.keys(data);
48-49- const result = schema.partial().safeParse(data);
50-51 // Check for async validation
52 if (result instanceof Promise) {
53 throw new AsyncValidationError();
54 }
55-56 if (!result.success) {
57 throw new ValidationError(result.error.issues, "update");
58 }
59-60 // Filter the result to only include fields that were in the input
61 // This prevents defaults from being applied to fields that weren't provided
62 const filtered: Record<string, unknown> = {};
63 for (const key of inputKeys) {
64- if (key in result.data) {
65 filtered[key] = (result.data as Record<string, unknown>)[key];
66 }
67 }
68-69 return filtered as Partial<z.infer<T>>;
70}
7172/**
73 * Validate data for replace operations using Zod schema
74- *
75 * @param schema - Zod schema to validate against
76 * @param data - Data to validate
77 * @returns Validated and typed data
78 * @throws {ValidationError} If validation fails
79 * @throws {AsyncValidationError} If async validation is detected
80 */
81-export function parseReplace<T extends Schema>(schema: T, data: Input<T>): Infer<T> {
00082 const result = schema.safeParse(data);
83-84 // Check for async validation
85 if (result instanceof Promise) {
86 throw new AsyncValidationError();
87 }
88-89 if (!result.success) {
90 throw new ValidationError(result.error.issues, "replace");
91 }
···95/**
96 * Extract default values from a Zod schema
97 * This parses an empty object through the schema to get all defaults applied
98- *
99 * @param schema - Zod schema to extract defaults from
100 * @returns Object containing all default values from the schema
101 */
102-export function extractDefaults<T extends Schema>(schema: T): Partial<Infer<T>> {
0000000103 try {
104 // Make all fields optional, then parse empty object to trigger defaults
105 // This allows us to see which fields get default values
106- const partialSchema = schema.partial();
107 const result = partialSchema.safeParse({});
108-109 if (result instanceof Promise) {
110 // Cannot extract defaults from async schemas
111 return {};
112 }
113-114 // If successful, the result contains all fields that have defaults
115 // Only include fields that were actually added (have values)
116 if (!result.success) {
117 return {};
118 }
119-120 // Filter to only include fields that got values from defaults
121 // (not undefined, which indicates no default)
122 const defaults: Record<string, unknown> = {};
123 const data = result.data as Record<string, unknown>;
124-125 for (const [key, value] of Object.entries(data)) {
126 if (value !== undefined) {
127 defaults[key] = value;
128 }
129 }
130-131 return defaults as Partial<Infer<T>>;
132 } catch {
133 return {};
···137/**
138 * Get all field paths mentioned in an update filter object
139 * This includes fields in $set, $unset, $inc, $push, etc.
140- *
141 * @param update - MongoDB update filter
142 * @returns Set of field paths that are being modified
143 */
144function getModifiedFields(update: UpdateFilter<Document>): Set<string> {
145 const fields = new Set<string>();
146-147- // Operators that modify fields
148- const operators = [
149- '$set', '$unset', '$inc', '$mul', '$rename', '$min', '$max',
150- '$currentDate', '$push', '$pull', '$addToSet', '$pop', '$bit',
151- '$setOnInsert',
152- ];
153-154- for (const op of operators) {
155- if (update[op] && typeof update[op] === 'object') {
156 // Add all field names from this operator
157 for (const field of Object.keys(update[op] as Document)) {
158 fields.add(field);
159 }
160 }
161 }
162-163 return fields;
164}
165···209210/**
211 * Apply schema defaults to an update operation using $setOnInsert
212- *
213 * This is used for upsert operations to ensure defaults are applied when
214 * a new document is created, but not when updating an existing document.
215- *
216 * For each default field:
217 * - If the field is NOT mentioned in any update operator ($set, $inc, etc.)
218 * - If the field is NOT fixed by an equality clause in the query filter
219 * - Add it to $setOnInsert so it's only applied on insert
220- *
221 * @param schema - Zod schema with defaults
222 * @param query - MongoDB query filter
223 * @param update - MongoDB update filter
···226export function applyDefaultsForUpsert<T extends Schema>(
227 schema: T,
228 query: Filter<Infer<T>>,
229- update: UpdateFilter<Infer<T>>
230): UpdateFilter<Infer<T>> {
231 // Extract defaults from schema
232 const defaults = extractDefaults(schema);
233-234 // If no defaults, return update unchanged
235 if (Object.keys(defaults).length === 0) {
236 return update;
237 }
238-239 // Get fields that are already being modified
240 const modifiedFields = getModifiedFields(update as UpdateFilter<Document>);
241 const filterEqualityFields = getEqualityFields(query as Filter<Document>);
242-243 // Build $setOnInsert with defaults for unmodified fields
244 const setOnInsert: Partial<Infer<T>> = {};
245-246 for (const [field, value] of Object.entries(defaults)) {
247 // Only add default if field is not already being modified or fixed by filter equality
248 if (!modifiedFields.has(field) && !filterEqualityFields.has(field)) {
249 setOnInsert[field as keyof Infer<T>] = value as Infer<T>[keyof Infer<T>];
250 }
251 }
252-253 // If there are defaults to add, merge them into $setOnInsert
254 if (Object.keys(setOnInsert).length > 0) {
255 return {
256 ...update,
257 $setOnInsert: {
258 ...(update.$setOnInsert || {}),
259- ...setOnInsert
260- } as Partial<Infer<T>>
261 };
262 }
263-264 return update;
265}
···1import type { z } from "@zod/zod";
2+import type { Infer, Input, Schema } from "../types.ts";
3+import { AsyncValidationError, ValidationError } from "../errors.ts";
4+import type { Document, Filter, UpdateFilter } from "mongodb";
5+6+// Cache frequently reused schema transformations to avoid repeated allocations
7+const partialSchemaCache = new WeakMap<Schema, z.ZodTypeAny>();
8+const defaultsCache = new WeakMap<Schema, Record<string, unknown>>();
9+const updateOperators = [
10+ "$set",
11+ "$unset",
12+ "$inc",
13+ "$mul",
14+ "$rename",
15+ "$min",
16+ "$max",
17+ "$currentDate",
18+ "$push",
19+ "$pull",
20+ "$addToSet",
21+ "$pop",
22+ "$bit",
23+ "$setOnInsert",
24+];
25+26+function getPartialSchema(schema: Schema): z.ZodTypeAny {
27+ const cached = partialSchemaCache.get(schema);
28+ if (cached) return cached;
29+ const partial = schema.partial();
30+ partialSchemaCache.set(schema, partial);
31+ return partial;
32+}
3334/**
35 * Validate data for insert operations using Zod schema
36+ *
37 * @param schema - Zod schema to validate against
38 * @param data - Data to validate
39 * @returns Validated and typed data
···42 */
43export function parse<T extends Schema>(schema: T, data: Input<T>): Infer<T> {
44 const result = schema.safeParse(data);
45+46 // Check for async validation
47 if (result instanceof Promise) {
48 throw new AsyncValidationError();
49 }
50+51 if (!result.success) {
52 throw new ValidationError(result.error.issues, "insert");
53 }
···5657/**
58 * Validate partial data for update operations using Zod schema
59+ *
60 * Important: This function only validates the fields that are provided in the data object.
61 * Unlike parse(), this function does NOT apply defaults for missing fields because
62 * in an update context, missing fields should remain unchanged in the database.
63+ *
64 * @param schema - Zod schema to validate against
65 * @param data - Partial data to validate
66 * @returns Validated and typed partial data (only fields present in input)
···71 schema: T,
72 data: Partial<z.infer<T>>,
73): Partial<z.infer<T>> {
74+ if (!data || Object.keys(data).length === 0) {
75+ return {};
76+ }
77+78 // Get the list of fields actually provided in the input
79 const inputKeys = Object.keys(data);
80+81+ const result = getPartialSchema(schema).safeParse(data);
82+83 // Check for async validation
84 if (result instanceof Promise) {
85 throw new AsyncValidationError();
86 }
87+88 if (!result.success) {
89 throw new ValidationError(result.error.issues, "update");
90 }
91+92 // Filter the result to only include fields that were in the input
93 // This prevents defaults from being applied to fields that weren't provided
94 const filtered: Record<string, unknown> = {};
95 for (const key of inputKeys) {
96+ if (key in (result.data as Record<string, unknown>)) {
97 filtered[key] = (result.data as Record<string, unknown>)[key];
98 }
99 }
100+101 return filtered as Partial<z.infer<T>>;
102}
103104/**
105 * Validate data for replace operations using Zod schema
106+ *
107 * @param schema - Zod schema to validate against
108 * @param data - Data to validate
109 * @returns Validated and typed data
110 * @throws {ValidationError} If validation fails
111 * @throws {AsyncValidationError} If async validation is detected
112 */
113+export function parseReplace<T extends Schema>(
114+ schema: T,
115+ data: Input<T>,
116+): Infer<T> {
117 const result = schema.safeParse(data);
118+119 // Check for async validation
120 if (result instanceof Promise) {
121 throw new AsyncValidationError();
122 }
123+124 if (!result.success) {
125 throw new ValidationError(result.error.issues, "replace");
126 }
···130/**
131 * Extract default values from a Zod schema
132 * This parses an empty object through the schema to get all defaults applied
133+ *
134 * @param schema - Zod schema to extract defaults from
135 * @returns Object containing all default values from the schema
136 */
137+export function extractDefaults<T extends Schema>(
138+ schema: T,
139+): Partial<Infer<T>> {
140+ const cached = defaultsCache.get(schema);
141+ if (cached) {
142+ return cached as Partial<Infer<T>>;
143+ }
144+145 try {
146 // Make all fields optional, then parse empty object to trigger defaults
147 // This allows us to see which fields get default values
148+ const partialSchema = getPartialSchema(schema);
149 const result = partialSchema.safeParse({});
150+151 if (result instanceof Promise) {
152 // Cannot extract defaults from async schemas
153 return {};
154 }
155+156 // If successful, the result contains all fields that have defaults
157 // Only include fields that were actually added (have values)
158 if (!result.success) {
159 return {};
160 }
161+162 // Filter to only include fields that got values from defaults
163 // (not undefined, which indicates no default)
164 const defaults: Record<string, unknown> = {};
165 const data = result.data as Record<string, unknown>;
166+167 for (const [key, value] of Object.entries(data)) {
168 if (value !== undefined) {
169 defaults[key] = value;
170 }
171 }
172+ defaultsCache.set(schema, defaults as Partial<Infer<Schema>>);
173 return defaults as Partial<Infer<T>>;
174 } catch {
175 return {};
···179/**
180 * Get all field paths mentioned in an update filter object
181 * This includes fields in $set, $unset, $inc, $push, etc.
182+ *
183 * @param update - MongoDB update filter
184 * @returns Set of field paths that are being modified
185 */
186function getModifiedFields(update: UpdateFilter<Document>): Set<string> {
187 const fields = new Set<string>();
188+189+ for (const op of updateOperators) {
190+ if (update[op] && typeof update[op] === "object") {
0000000191 // Add all field names from this operator
192 for (const field of Object.keys(update[op] as Document)) {
193 fields.add(field);
194 }
195 }
196 }
197+198 return fields;
199}
200···244245/**
246 * Apply schema defaults to an update operation using $setOnInsert
247+ *
248 * This is used for upsert operations to ensure defaults are applied when
249 * a new document is created, but not when updating an existing document.
250+ *
251 * For each default field:
252 * - If the field is NOT mentioned in any update operator ($set, $inc, etc.)
253 * - If the field is NOT fixed by an equality clause in the query filter
254 * - Add it to $setOnInsert so it's only applied on insert
255+ *
256 * @param schema - Zod schema with defaults
257 * @param query - MongoDB query filter
258 * @param update - MongoDB update filter
···261export function applyDefaultsForUpsert<T extends Schema>(
262 schema: T,
263 query: Filter<Infer<T>>,
264+ update: UpdateFilter<Infer<T>>,
265): UpdateFilter<Infer<T>> {
266 // Extract defaults from schema
267 const defaults = extractDefaults(schema);
268+269 // If no defaults, return update unchanged
270 if (Object.keys(defaults).length === 0) {
271 return update;
272 }
273+274 // Get fields that are already being modified
275 const modifiedFields = getModifiedFields(update as UpdateFilter<Document>);
276 const filterEqualityFields = getEqualityFields(query as Filter<Document>);
277+278 // Build $setOnInsert with defaults for unmodified fields
279 const setOnInsert: Partial<Infer<T>> = {};
280+281 for (const [field, value] of Object.entries(defaults)) {
282 // Only add default if field is not already being modified or fixed by filter equality
283 if (!modifiedFields.has(field) && !filterEqualityFields.has(field)) {
284 setOnInsert[field as keyof Infer<T>] = value as Infer<T>[keyof Infer<T>];
285 }
286 }
287+288 // If there are defaults to add, merge them into $setOnInsert
289 if (Object.keys(setOnInsert).length > 0) {
290 return {
291 ...update,
292 $setOnInsert: {
293 ...(update.$setOnInsert || {}),
294+ ...setOnInsert,
295+ } as Partial<Infer<T>>,
296 };
297 }
298+299 return update;
300}
+42-37
tests/connection_test.ts
···1import { assert, assertEquals, assertExists } from "@std/assert";
2-import { connect, disconnect, healthCheck, type ConnectOptions } from "../mod.ts";
000003import { MongoMemoryServer } from "mongodb-memory-server-core";
45let mongoServer: MongoMemoryServer | null = null;
···27 async fn() {
28 const uri = await setupTestServer();
29 const connection = await connect(uri, "test_db");
30-31 assert(connection);
32 assert(connection.client);
33 assert(connection.db);
···47 maxIdleTimeMS: 30000,
48 connectTimeoutMS: 5000,
49 };
50-51 const connection = await connect(uri, "test_db", options);
52-53 assert(connection);
54 assert(connection.client);
55 assert(connection.db);
56-57 // Verify connection is working
58 const adminDb = connection.db.admin();
59 const serverStatus = await adminDb.serverStatus();
···67 name: "Connection: Singleton - should reuse existing connection",
68 async fn() {
69 const uri = await setupTestServer();
70-71 const connection1 = await connect(uri, "test_db");
72 const connection2 = await connect(uri, "test_db");
73-74 // Should return the same connection instance
75 assertEquals(connection1, connection2);
76 assertEquals(connection1.client, connection2.client);
···84 name: "Connection: Disconnect - should disconnect and allow reconnection",
85 async fn() {
86 const uri = await setupTestServer();
87-88 const connection1 = await connect(uri, "test_db");
89 assert(connection1);
90-91 await disconnect();
92-93 // Should be able to reconnect
94 const connection2 = await connect(uri, "test_db");
95 assert(connection2);
96-97 // Should be a new connection instance
98 assert(connection1 !== connection2);
99 },
···108 const options: ConnectOptions = {
109 maxPoolSize: 5,
110 };
111-112 const connection = await connect(uri, "test_db", options);
113-114 // Verify connection works with custom pool size
115 const collections = await connection.db.listCollections().toArray();
116 assert(Array.isArray(collections));
···120});
121122Deno.test({
123- name: "Connection: Multiple Databases - should handle different database names",
0124 async fn() {
125 const uri = await setupTestServer();
126-127 // Connect to first database
128 const connection1 = await connect(uri, "db1");
129 assertEquals(connection1.db.databaseName, "db1");
130-131 // Disconnect first
132 await disconnect();
133-134 // Connect to second database
135 const connection2 = await connect(uri, "db2");
136 assertEquals(connection2.db.databaseName, "db2");
···143 name: "Health Check: should return unhealthy when not connected",
144 async fn() {
145 const result = await healthCheck();
146-147 assertEquals(result.healthy, false);
148 assertEquals(result.connected, false);
149 assertExists(result.error);
···160 async fn() {
161 const uri = await setupTestServer();
162 await connect(uri, "test_db");
163-164 const result = await healthCheck();
165-166 assertEquals(result.healthy, true);
167 assertEquals(result.connected, true);
168 assertExists(result.responseTimeMs);
···179 async fn() {
180 const uri = await setupTestServer();
181 await connect(uri, "test_db");
182-183 const result = await healthCheck();
184-185 assertEquals(result.healthy, true);
186 assertExists(result.responseTimeMs);
187 // Response time should be reasonable (less than 1 second for in-memory MongoDB)
···196 async fn() {
197 const uri = await setupTestServer();
198 await connect(uri, "test_db");
199-200 // Run health check multiple times
201 const results = await Promise.all([
202 healthCheck(),
203 healthCheck(),
204 healthCheck(),
205 ]);
206-207 // All should be healthy
208 for (const result of results) {
209 assertEquals(result.healthy, true);
···220 async fn() {
221 const uri = await setupTestServer();
222 await connect(uri, "test_db");
223-224 // First check should be healthy
225 let result = await healthCheck();
226 assertEquals(result.healthy, true);
227-228 // Disconnect
229 await disconnect();
230-231 // Second check should be unhealthy
232 result = await healthCheck();
233 assertEquals(result.healthy, false);
···247 serverSelectionTimeoutMS: 5000,
248 connectTimeoutMS: 5000,
249 };
250-251 const connection = await connect(uri, "test_db", options);
252-253 assert(connection);
254 assert(connection.client);
255 assert(connection.db);
256-257 // Verify connection works with retry options
258 const collections = await connection.db.listCollections().toArray();
259 assert(Array.isArray(collections));
···270 // Pooling
271 maxPoolSize: 10,
272 minPoolSize: 2,
273-274 // Retry logic
275 retryReads: true,
276 retryWrites: true,
277-278 // Timeouts
279 connectTimeoutMS: 10000,
280 socketTimeoutMS: 45000,
281 serverSelectionTimeoutMS: 10000,
282-283 // Resilience
284 maxIdleTimeMS: 30000,
285 heartbeatFrequencyMS: 10000,
286 };
287-288 const connection = await connect(uri, "test_db", options);
289-290 assert(connection);
291-292 // Verify connection is working
293 const adminDb = connection.db.admin();
294 const serverStatus = await adminDb.serverStatus();
···297 sanitizeResources: false,
298 sanitizeOps: false,
299});
300-
···1import { assert, assertEquals, assertExists } from "@std/assert";
2+import {
3+ connect,
4+ type ConnectOptions,
5+ disconnect,
6+ healthCheck,
7+} from "../mod.ts";
8import { MongoMemoryServer } from "mongodb-memory-server-core";
910let mongoServer: MongoMemoryServer | null = null;
···32 async fn() {
33 const uri = await setupTestServer();
34 const connection = await connect(uri, "test_db");
35+36 assert(connection);
37 assert(connection.client);
38 assert(connection.db);
···52 maxIdleTimeMS: 30000,
53 connectTimeoutMS: 5000,
54 };
55+56 const connection = await connect(uri, "test_db", options);
57+58 assert(connection);
59 assert(connection.client);
60 assert(connection.db);
61+62 // Verify connection is working
63 const adminDb = connection.db.admin();
64 const serverStatus = await adminDb.serverStatus();
···72 name: "Connection: Singleton - should reuse existing connection",
73 async fn() {
74 const uri = await setupTestServer();
75+76 const connection1 = await connect(uri, "test_db");
77 const connection2 = await connect(uri, "test_db");
78+79 // Should return the same connection instance
80 assertEquals(connection1, connection2);
81 assertEquals(connection1.client, connection2.client);
···89 name: "Connection: Disconnect - should disconnect and allow reconnection",
90 async fn() {
91 const uri = await setupTestServer();
92+93 const connection1 = await connect(uri, "test_db");
94 assert(connection1);
95+96 await disconnect();
97+98 // Should be able to reconnect
99 const connection2 = await connect(uri, "test_db");
100 assert(connection2);
101+102 // Should be a new connection instance
103 assert(connection1 !== connection2);
104 },
···113 const options: ConnectOptions = {
114 maxPoolSize: 5,
115 };
116+117 const connection = await connect(uri, "test_db", options);
118+119 // Verify connection works with custom pool size
120 const collections = await connection.db.listCollections().toArray();
121 assert(Array.isArray(collections));
···125});
126127Deno.test({
128+ name:
129+ "Connection: Multiple Databases - should handle different database names",
130 async fn() {
131 const uri = await setupTestServer();
132+133 // Connect to first database
134 const connection1 = await connect(uri, "db1");
135 assertEquals(connection1.db.databaseName, "db1");
136+137 // Disconnect first
138 await disconnect();
139+140 // Connect to second database
141 const connection2 = await connect(uri, "db2");
142 assertEquals(connection2.db.databaseName, "db2");
···149 name: "Health Check: should return unhealthy when not connected",
150 async fn() {
151 const result = await healthCheck();
152+153 assertEquals(result.healthy, false);
154 assertEquals(result.connected, false);
155 assertExists(result.error);
···166 async fn() {
167 const uri = await setupTestServer();
168 await connect(uri, "test_db");
169+170 const result = await healthCheck();
171+172 assertEquals(result.healthy, true);
173 assertEquals(result.connected, true);
174 assertExists(result.responseTimeMs);
···185 async fn() {
186 const uri = await setupTestServer();
187 await connect(uri, "test_db");
188+189 const result = await healthCheck();
190+191 assertEquals(result.healthy, true);
192 assertExists(result.responseTimeMs);
193 // Response time should be reasonable (less than 1 second for in-memory MongoDB)
···202 async fn() {
203 const uri = await setupTestServer();
204 await connect(uri, "test_db");
205+206 // Run health check multiple times
207 const results = await Promise.all([
208 healthCheck(),
209 healthCheck(),
210 healthCheck(),
211 ]);
212+213 // All should be healthy
214 for (const result of results) {
215 assertEquals(result.healthy, true);
···226 async fn() {
227 const uri = await setupTestServer();
228 await connect(uri, "test_db");
229+230 // First check should be healthy
231 let result = await healthCheck();
232 assertEquals(result.healthy, true);
233+234 // Disconnect
235 await disconnect();
236+237 // Second check should be unhealthy
238 result = await healthCheck();
239 assertEquals(result.healthy, false);
···253 serverSelectionTimeoutMS: 5000,
254 connectTimeoutMS: 5000,
255 };
256+257 const connection = await connect(uri, "test_db", options);
258+259 assert(connection);
260 assert(connection.client);
261 assert(connection.db);
262+263 // Verify connection works with retry options
264 const collections = await connection.db.listCollections().toArray();
265 assert(Array.isArray(collections));
···276 // Pooling
277 maxPoolSize: 10,
278 minPoolSize: 2,
279+280 // Retry logic
281 retryReads: true,
282 retryWrites: true,
283+284 // Timeouts
285 connectTimeoutMS: 10000,
286 socketTimeoutMS: 45000,
287 serverSelectionTimeoutMS: 10000,
288+289 // Resilience
290 maxIdleTimeMS: 30000,
291 heartbeatFrequencyMS: 10000,
292 };
293+294 const connection = await connect(uri, "test_db", options);
295+296 assert(connection);
297+298 // Verify connection is working
299 const adminDb = connection.db.admin();
300 const serverStatus = await adminDb.serverStatus();
···303 sanitizeResources: false,
304 sanitizeOps: false,
305});
0
+1-8
tests/crud_test.ts
···1415Deno.test.beforeAll(async () => {
16 await setupTestDb();
17- UserModel = createUserModel();
18});
1920Deno.test.beforeEach(async () => {
···28Deno.test({
29 name: "CRUD: Insert - should insert a new user successfully",
30 async fn() {
31-32 const newUser: UserInsert = {
33 name: "Test User",
34 email: "test@example.com",
···47Deno.test({
48 name: "CRUD: Find - should find the inserted user",
49 async fn() {
50-51 // First insert a user for this test
52 const newUser: UserInsert = {
53 name: "Find Test User",
···73Deno.test({
74 name: "CRUD: Update - should update user data",
75 async fn() {
76-77 // Insert a user for this test
78 const newUser: UserInsert = {
79 name: "Update Test User",
···106Deno.test({
107 name: "CRUD: Delete - should delete user successfully",
108 async fn() {
109-110 // Insert a user for this test
111 const newUser: UserInsert = {
112 name: "Delete Test User",
···137Deno.test({
138 name: "CRUD: Find Multiple - should find multiple users",
139 async fn() {
140-141 // Insert multiple users
142 const users: UserInsert[] = [
143 { name: "User 1", email: "user1@example.com", age: 20 },
···157 sanitizeResources: false,
158 sanitizeOps: false,
159});
160-161-
···1415Deno.test.beforeAll(async () => {
16 await setupTestDb();
17+ UserModel = createUserModel("users_crud");
18});
1920Deno.test.beforeEach(async () => {
···28Deno.test({
29 name: "CRUD: Insert - should insert a new user successfully",
30 async fn() {
031 const newUser: UserInsert = {
32 name: "Test User",
33 email: "test@example.com",
···46Deno.test({
47 name: "CRUD: Find - should find the inserted user",
48 async fn() {
049 // First insert a user for this test
50 const newUser: UserInsert = {
51 name: "Find Test User",
···71Deno.test({
72 name: "CRUD: Update - should update user data",
73 async fn() {
074 // Insert a user for this test
75 const newUser: UserInsert = {
76 name: "Update Test User",
···103Deno.test({
104 name: "CRUD: Delete - should delete user successfully",
105 async fn() {
0106 // Insert a user for this test
107 const newUser: UserInsert = {
108 name: "Delete Test User",
···133Deno.test({
134 name: "CRUD: Find Multiple - should find multiple users",
135 async fn() {
0136 // Insert multiple users
137 const users: UserInsert[] = [
138 { name: "User 1", email: "user1@example.com", age: 20 },
···152 sanitizeResources: false,
153 sanitizeOps: false,
154});
00
+33-31
tests/defaults_test.ts
···1import { assertEquals, assertExists } from "@std/assert";
2import { z } from "@zod/zod";
3-import { connect, disconnect, Model } from "../mod.ts";
4import { applyDefaultsForUpsert } from "../model/validation.ts";
5-import { MongoMemoryServer } from "mongodb-memory-server-core";
67/**
8 * Test suite for default value handling in different operation types
9- *
10 * This tests the three main cases:
11 * 1. Plain inserts - defaults applied directly
12 * 2. Updates without upsert - defaults NOT applied
···26});
2728let ProductModel: Model<typeof productSchema>;
29-let mongoServer: MongoMemoryServer;
3031Deno.test.beforeAll(async () => {
32- mongoServer = await MongoMemoryServer.create();
33- const uri = mongoServer.getUri();
34- await connect(uri, "test_defaults_db");
35 ProductModel = new Model("test_products_defaults", productSchema);
36});
37···4142Deno.test.afterAll(async () => {
43 await ProductModel.delete({});
44- await disconnect();
45- await mongoServer.stop();
46});
4748Deno.test({
···60 // Verify defaults were applied
61 const product = await ProductModel.findById(result.insertedId);
62 assertExists(product);
63-64 assertEquals(product.name, "Widget");
65 assertEquals(product.price, 29.99);
66 assertEquals(product.category, "general"); // default
···84 createdAt: new Date("2023-01-01"),
85 tags: ["test"],
86 });
87-88 assertExists(insertResult.insertedId);
8990 // Now update it - defaults should NOT be applied
91 await ProductModel.updateOne(
92 { _id: insertResult.insertedId },
93- { price: 24.99 }
94 // No upsert flag
95 );
9697 const updated = await ProductModel.findById(insertResult.insertedId);
98 assertExists(updated);
99-100 assertEquals(updated.price, 24.99); // updated
101 assertEquals(updated.category, "electronics"); // unchanged
102 assertEquals(updated.inStock, false); // unchanged
···107});
108109Deno.test({
110- name: "Defaults: Case 3 - Upsert that creates applies defaults via $setOnInsert",
0111 async fn() {
112 // Upsert with a query that won't match - will create new document
113 const result = await ProductModel.updateOne(
114 { name: "NonExistent" },
115 { price: 39.99 },
116- { upsert: true }
117 );
118119 assertEquals(result.upsertedCount, 1);
···122 // Verify the created document has defaults applied
123 const product = await ProductModel.findOne({ name: "NonExistent" });
124 assertExists(product);
125-126 assertEquals(product.price, 39.99); // from $set
127 assertEquals(product.name, "NonExistent"); // from query
128 assertEquals(product.category, "general"); // default via $setOnInsert
···153 const result = await ProductModel.updateOne(
154 { name: "ExistingProduct" },
155 { price: 44.99 },
156- { upsert: true }
157 );
158159 assertEquals(result.matchedCount, 1);
···163 // Verify defaults were NOT applied (existing values preserved)
164 const product = await ProductModel.findOne({ name: "ExistingProduct" });
165 assertExists(product);
166-167 assertEquals(product.price, 44.99); // updated via $set
168 assertEquals(product.category, "premium"); // preserved (not overwritten with default)
169 assertEquals(product.inStock, false); // preserved
···195 name: "Replaced",
196 price: 15.0,
197 // category, inStock, createdAt, tags not provided - defaults should apply
198- }
199 );
200201 const product = await ProductModel.findById(insertResult.insertedId);
202 assertExists(product);
203-204 assertEquals(product.name, "Replaced");
205 assertEquals(product.price, 15.0);
206 assertEquals(product.category, "general"); // default applied
···223 price: 99.99,
224 // Missing optional fields - defaults should apply
225 },
226- { upsert: true }
227 );
228229 assertEquals(result.upsertedCount, 1);
···231232 const product = await ProductModel.findOne({ name: "NewViaReplace" });
233 assertExists(product);
234-235 assertEquals(product.name, "NewViaReplace");
236 assertEquals(product.price, 99.99);
237 assertEquals(product.category, "general"); // default
···254 category: "custom", // Explicitly setting a field that has a default
255 // inStock not set - should get default
256 },
257- { upsert: true }
258 );
259260 assertEquals(result.upsertedCount, 1);
261262 const product = await ProductModel.findOne({ name: "CustomDefaults" });
263 assertExists(product);
264-265 assertEquals(product.name, "CustomDefaults"); // from query
266 assertEquals(product.price, 25.0); // from $set
267 assertEquals(product.category, "custom"); // from $set (NOT default)
···292 assertExists(product.createdAt);
293 assertEquals(product.inStock, true);
294 assertEquals(product.tags, []);
295-296 if (product.name === "Bulk2") {
297 assertEquals(product.category, "special");
298 } else {
···305});
306307Deno.test({
308- name: "Defaults: applyDefaultsForUpsert preserves existing $setOnInsert values",
0309 fn() {
310 const schema = z.object({
311 name: z.string(),
···328});
329330Deno.test({
331- name: "Defaults: applyDefaultsForUpsert keeps query equality fields untouched",
0332 fn() {
333 const schema = z.object({
334 status: z.string().default("pending"),
···349});
350351Deno.test({
352- name: "Defaults: findOneAndUpdate with upsert preserves query equality fields",
0353 async fn() {
354 await ProductModel.findOneAndUpdate(
355 { name: "FindOneUpsert", category: "special" },
356 { price: 12.5 },
357- { upsert: true }
358 );
359360 const product = await ProductModel.findOne({ name: "FindOneUpsert" });
···379 name: "FindOneReplaceUpsert",
380 price: 77.0,
381 },
382- { upsert: true }
383 );
384385 assertExists(result.lastErrorObject?.upserted);
386387- const product = await ProductModel.findOne({ name: "FindOneReplaceUpsert" });
00388 assertExists(product);
389390 assertEquals(product.name, "FindOneReplaceUpsert");
···1import { assertEquals, assertExists } from "@std/assert";
2import { z } from "@zod/zod";
3+import { Model } from "../mod.ts";
4import { applyDefaultsForUpsert } from "../model/validation.ts";
5+import { setupTestDb, teardownTestDb } from "./utils.ts";
67/**
8 * Test suite for default value handling in different operation types
9+ *
10 * This tests the three main cases:
11 * 1. Plain inserts - defaults applied directly
12 * 2. Updates without upsert - defaults NOT applied
···26});
2728let ProductModel: Model<typeof productSchema>;
02930Deno.test.beforeAll(async () => {
31+ await setupTestDb();
0032 ProductModel = new Model("test_products_defaults", productSchema);
33});
34···3839Deno.test.afterAll(async () => {
40 await ProductModel.delete({});
41+ await teardownTestDb();
042});
4344Deno.test({
···56 // Verify defaults were applied
57 const product = await ProductModel.findById(result.insertedId);
58 assertExists(product);
59+60 assertEquals(product.name, "Widget");
61 assertEquals(product.price, 29.99);
62 assertEquals(product.category, "general"); // default
···80 createdAt: new Date("2023-01-01"),
81 tags: ["test"],
82 });
83+84 assertExists(insertResult.insertedId);
8586 // Now update it - defaults should NOT be applied
87 await ProductModel.updateOne(
88 { _id: insertResult.insertedId },
89+ { price: 24.99 },
90 // No upsert flag
91 );
9293 const updated = await ProductModel.findById(insertResult.insertedId);
94 assertExists(updated);
95+96 assertEquals(updated.price, 24.99); // updated
97 assertEquals(updated.category, "electronics"); // unchanged
98 assertEquals(updated.inStock, false); // unchanged
···103});
104105Deno.test({
106+ name:
107+ "Defaults: Case 3 - Upsert that creates applies defaults via $setOnInsert",
108 async fn() {
109 // Upsert with a query that won't match - will create new document
110 const result = await ProductModel.updateOne(
111 { name: "NonExistent" },
112 { price: 39.99 },
113+ { upsert: true },
114 );
115116 assertEquals(result.upsertedCount, 1);
···119 // Verify the created document has defaults applied
120 const product = await ProductModel.findOne({ name: "NonExistent" });
121 assertExists(product);
122+123 assertEquals(product.price, 39.99); // from $set
124 assertEquals(product.name, "NonExistent"); // from query
125 assertEquals(product.category, "general"); // default via $setOnInsert
···150 const result = await ProductModel.updateOne(
151 { name: "ExistingProduct" },
152 { price: 44.99 },
153+ { upsert: true },
154 );
155156 assertEquals(result.matchedCount, 1);
···160 // Verify defaults were NOT applied (existing values preserved)
161 const product = await ProductModel.findOne({ name: "ExistingProduct" });
162 assertExists(product);
163+164 assertEquals(product.price, 44.99); // updated via $set
165 assertEquals(product.category, "premium"); // preserved (not overwritten with default)
166 assertEquals(product.inStock, false); // preserved
···192 name: "Replaced",
193 price: 15.0,
194 // category, inStock, createdAt, tags not provided - defaults should apply
195+ },
196 );
197198 const product = await ProductModel.findById(insertResult.insertedId);
199 assertExists(product);
200+201 assertEquals(product.name, "Replaced");
202 assertEquals(product.price, 15.0);
203 assertEquals(product.category, "general"); // default applied
···220 price: 99.99,
221 // Missing optional fields - defaults should apply
222 },
223+ { upsert: true },
224 );
225226 assertEquals(result.upsertedCount, 1);
···228229 const product = await ProductModel.findOne({ name: "NewViaReplace" });
230 assertExists(product);
231+232 assertEquals(product.name, "NewViaReplace");
233 assertEquals(product.price, 99.99);
234 assertEquals(product.category, "general"); // default
···251 category: "custom", // Explicitly setting a field that has a default
252 // inStock not set - should get default
253 },
254+ { upsert: true },
255 );
256257 assertEquals(result.upsertedCount, 1);
258259 const product = await ProductModel.findOne({ name: "CustomDefaults" });
260 assertExists(product);
261+262 assertEquals(product.name, "CustomDefaults"); // from query
263 assertEquals(product.price, 25.0); // from $set
264 assertEquals(product.category, "custom"); // from $set (NOT default)
···289 assertExists(product.createdAt);
290 assertEquals(product.inStock, true);
291 assertEquals(product.tags, []);
292+293 if (product.name === "Bulk2") {
294 assertEquals(product.category, "special");
295 } else {
···302});
303304Deno.test({
305+ name:
306+ "Defaults: applyDefaultsForUpsert preserves existing $setOnInsert values",
307 fn() {
308 const schema = z.object({
309 name: z.string(),
···326});
327328Deno.test({
329+ name:
330+ "Defaults: applyDefaultsForUpsert keeps query equality fields untouched",
331 fn() {
332 const schema = z.object({
333 status: z.string().default("pending"),
···348});
349350Deno.test({
351+ name:
352+ "Defaults: findOneAndUpdate with upsert preserves query equality fields",
353 async fn() {
354 await ProductModel.findOneAndUpdate(
355 { name: "FindOneUpsert", category: "special" },
356 { price: 12.5 },
357+ { upsert: true },
358 );
359360 const product = await ProductModel.findOne({ name: "FindOneUpsert" });
···379 name: "FindOneReplaceUpsert",
380 price: 77.0,
381 },
382+ { upsert: true },
383 );
384385 assertExists(result.lastErrorObject?.upserted);
386387+ const product = await ProductModel.findOne({
388+ name: "FindOneReplaceUpsert",
389+ });
390 assertExists(product);
391392 assertEquals(product.name, "FindOneReplaceUpsert");
···1314let mongoServer: MongoMemoryServer | null = null;
15let isSetup = false;
001617-export async function setupTestDb() {
18- if (!isSetup) {
19- // Start MongoDB Memory Server
0000000000020 mongoServer = await MongoMemoryServer.create();
21 const uri = mongoServer.getUri();
22-23- // Connect to the in-memory database
24- await connect(uri, "test_db");
25 isSetup = true;
000026 }
27}
2829export async function teardownTestDb() {
30- if (isSetup) {
00000031 await disconnect();
32 if (mongoServer) {
33 await mongoServer.stop();
34 mongoServer = null;
35 }
036 isSetup = false;
37 }
38}
3940-export function createUserModel(): Model<typeof userSchema> {
41- return new Model("users", userSchema);
0042}
4344export async function cleanupCollection(model: Model<typeof userSchema>) {
45 await model.delete({});
46}
47-
···1314let mongoServer: MongoMemoryServer | null = null;
15let isSetup = false;
16+let setupRefCount = 0;
17+let activeDbName: string | null = null;
1819+export async function setupTestDb(dbName = "test_db") {
20+ setupRefCount++;
21+22+ // If we're already connected, just share the same database
23+ if (isSetup) {
24+ if (activeDbName !== dbName) {
25+ throw new Error(
26+ `Test DB already initialized for ${activeDbName}, requested ${dbName}`,
27+ );
28+ }
29+ return;
30+ }
31+32+ try {
33 mongoServer = await MongoMemoryServer.create();
34 const uri = mongoServer.getUri();
35+36+ await connect(uri, dbName);
37+ activeDbName = dbName;
38 isSetup = true;
39+ } catch (error) {
40+ // Roll back refcount if setup failed so future attempts can retry
41+ setupRefCount = Math.max(0, setupRefCount - 1);
42+ throw error;
43 }
44}
4546export async function teardownTestDb() {
47+ if (setupRefCount === 0) {
48+ return;
49+ }
50+51+ setupRefCount = Math.max(0, setupRefCount - 1);
52+53+ if (isSetup && setupRefCount === 0) {
54 await disconnect();
55 if (mongoServer) {
56 await mongoServer.stop();
57 mongoServer = null;
58 }
59+ activeDbName = null;
60 isSetup = false;
61 }
62}
6364+export function createUserModel(
65+ collectionName = "users",
66+): Model<typeof userSchema> {
67+ return new Model(collectionName, userSchema);
68}
6970export async function cleanupCollection(model: Model<typeof userSchema>) {
71 await model.delete({});
72}
0
+1-8
tests/validation_test.ts
···1415Deno.test.beforeAll(async () => {
16 await setupTestDb();
17- UserModel = createUserModel();
18});
1920Deno.test.beforeEach(async () => {
···28Deno.test({
29 name: "Validation: Schema - should validate user data on insert",
30 async fn() {
31-32 const invalidUser = {
33 name: "Invalid User",
34 email: "not-an-email", // Invalid email
···50Deno.test({
51 name: "Validation: Update - should reject invalid email in update",
52 async fn() {
53-54 // Insert a user for this test
55 const newUser: UserInsert = {
56 name: "Validation Test User",
···79Deno.test({
80 name: "Validation: Update - should reject negative age in update",
81 async fn() {
82-83 // Insert a user for this test
84 const newUser: UserInsert = {
85 name: "Age Validation Test User",
···108Deno.test({
109 name: "Validation: Update - should reject invalid name type in update",
110 async fn() {
111-112 // Insert a user for this test
113 const newUser: UserInsert = {
114 name: "Type Validation Test User",
···137Deno.test({
138 name: "Validation: Update - should accept valid partial updates",
139 async fn() {
140-141 // Insert a user for this test
142 const newUser: UserInsert = {
143 name: "Valid Update Test User",
···168 sanitizeResources: false,
169 sanitizeOps: false,
170});
171-172-
···1415Deno.test.beforeAll(async () => {
16 await setupTestDb();
17+ UserModel = createUserModel("users_validation");
18});
1920Deno.test.beforeEach(async () => {
···28Deno.test({
29 name: "Validation: Schema - should validate user data on insert",
30 async fn() {
031 const invalidUser = {
32 name: "Invalid User",
33 email: "not-an-email", // Invalid email
···49Deno.test({
50 name: "Validation: Update - should reject invalid email in update",
51 async fn() {
052 // Insert a user for this test
53 const newUser: UserInsert = {
54 name: "Validation Test User",
···77Deno.test({
78 name: "Validation: Update - should reject negative age in update",
79 async fn() {
080 // Insert a user for this test
81 const newUser: UserInsert = {
82 name: "Age Validation Test User",
···105Deno.test({
106 name: "Validation: Update - should reject invalid name type in update",
107 async fn() {
0108 // Insert a user for this test
109 const newUser: UserInsert = {
110 name: "Type Validation Test User",
···133Deno.test({
134 name: "Validation: Update - should accept valid partial updates",
135 async fn() {
0136 // Insert a user for this test
137 const newUser: UserInsert = {
138 name: "Valid Update Test User",
···163 sanitizeResources: false,
164 sanitizeOps: false,
165});
00
+5-6
types.ts
···1import type { z } from "@zod/zod";
2-import type { Document, ObjectId, IndexDescription } from "mongodb";
34/**
5 * Type alias for Zod schema objects
···11 */
12export type Infer<T extends Schema> = z.infer<T> & Document;
1314-15/**
16 * Infer the model type from a Zod schema, including MongoDB Document and ObjectId
17 */
18export type InferModel<T extends Schema> = Infer<T> & {
19- _id?: ObjectId;
20- };
2122/**
23 * Infer the input type for a Zod schema (handles defaults)
···3132/**
33 * Complete definition of a model, including schema and indexes
34- *
35 * @example
36 * ```ts
37 * const userDef: ModelDef<typeof userSchema> = {
···46export type ModelDef<T extends Schema> = {
47 schema: T;
48 indexes?: Indexes;
49-};
···1import type { z } from "@zod/zod";
2+import type { Document, IndexDescription, ObjectId } from "mongodb";
34/**
5 * Type alias for Zod schema objects
···11 */
12export type Infer<T extends Schema> = z.infer<T> & Document;
13014/**
15 * Infer the model type from a Zod schema, including MongoDB Document and ObjectId
16 */
17export type InferModel<T extends Schema> = Infer<T> & {
18+ _id?: ObjectId;
19+};
2021/**
22 * Infer the input type for a Zod schema (handles defaults)
···3031/**
32 * Complete definition of a model, including schema and indexes
33+ *
34 * @example
35 * ```ts
36 * const userDef: ModelDef<typeof userSchema> = {
···45export type ModelDef<T extends Schema> = {
46 schema: T;
47 indexes?: Indexes;
48+};