···11-import { type Db, type MongoClientOptions, MongoClient } from "mongodb";
11+import { type Db, MongoClient, type MongoClientOptions } from "mongodb";
22import { ConnectionError } from "../errors.ts";
3344/**
55 * Connection management module
66- *
66+ *
77 * Handles MongoDB connection lifecycle including connect, disconnect,
88 * and connection state management.
99 */
···20202121/**
2222 * Connect to MongoDB with connection pooling, retry logic, and resilience options
2323- *
2323+ *
2424 * The MongoDB driver handles connection pooling and automatic retries.
2525 * Retry logic is enabled by default for both reads and writes in MongoDB 4.2+.
2626- *
2626+ *
2727 * @param uri - MongoDB connection string
2828 * @param dbName - Name of the database to connect to
2929 * @param options - Connection options (pooling, retries, timeouts, etc.)
3030 * @returns Connection object with client and db
3131- *
3131+ *
3232 * @example
3333 * Basic connection with pooling:
3434 * ```ts
···4040 * socketTimeoutMS: 45000,
4141 * });
4242 * ```
4343- *
4343+ *
4444 * @example
4545 * Production-ready connection with retry logic and resilience:
4646 * ```ts
···4848 * // Connection pooling
4949 * maxPoolSize: 10,
5050 * minPoolSize: 2,
5151- *
5151+ *
5252 * // Automatic retry logic (enabled by default)
5353 * retryReads: true, // Retry failed read operations
5454 * retryWrites: true, // Retry failed write operations
5555- *
5555+ *
5656 * // Timeouts
5757 * connectTimeoutMS: 10000, // Initial connection timeout
5858 * socketTimeoutMS: 45000, // Socket operation timeout
5959 * serverSelectionTimeoutMS: 10000, // Server selection timeout
6060- *
6060+ *
6161 * // Connection resilience
6262 * maxIdleTimeMS: 30000, // Close idle connections
6363 * heartbeatFrequencyMS: 10000, // Server health check interval
6464- *
6464+ *
6565 * // Optional: Compression for reduced bandwidth
6666 * compressors: ['snappy', 'zlib'],
6767 * });
···8585 return connection;
8686 } catch (error) {
8787 throw new ConnectionError(
8888- `Failed to connect to MongoDB: ${error instanceof Error ? error.message : String(error)}`,
8989- uri
8888+ `Failed to connect to MongoDB: ${
8989+ error instanceof Error ? error.message : String(error)
9090+ }`,
9191+ uri,
9092 );
9193 }
9294}
···103105104106/**
105107 * Get the current database connection
106106- *
108108+ *
107109 * @returns MongoDB Db instance
108110 * @throws {ConnectionError} If not connected
109111 * @internal
···117119118120/**
119121 * Get the current connection state
120120- *
122122+ *
121123 * @returns Connection object or null if not connected
122124 * @internal
123125 */
+5-5
client/health.ts
···2233/**
44 * Health check module
55- *
55+ *
66 * Provides functionality for monitoring MongoDB connection health
77 * including ping operations and response time measurement.
88 */
991010/**
1111 * Health check details of the MongoDB connection
1212- *
1212+ *
1313 * @property healthy - Overall health status of the connection
1414 * @property connected - Whether a connection is established
1515 * @property responseTimeMs - Response time in milliseconds (if connection is healthy)
···26262727/**
2828 * Check the health of the MongoDB connection
2929- *
2929+ *
3030 * Performs a ping operation to verify the database is responsive
3131 * and returns detailed health information including response time.
3232- *
3232+ *
3333 * @returns Health check result with status and metrics
3434- *
3434+ *
3535 * @example
3636 * ```ts
3737 * const health = await healthCheck();
+5-12
client/index.ts
···11/**
22 * Client module - MongoDB connection and session management
33- *
33+ *
44 * This module provides all client-level functionality including:
55 * - Connection management (connect, disconnect)
66 * - Health monitoring (healthCheck)
···1010// Re-export connection management
1111export {
1212 connect,
1313+ type Connection,
1414+ type ConnectOptions,
1315 disconnect,
1416 getDb,
1515- type ConnectOptions,
1616- type Connection,
1717} from "./connection.ts";
18181919// Re-export health monitoring
2020-export {
2121- healthCheck,
2222- type HealthCheckResult,
2323-} from "./health.ts";
2020+export { healthCheck, type HealthCheckResult } from "./health.ts";
24212522// Re-export transaction management
2626-export {
2727- startSession,
2828- endSession,
2929- withTransaction,
3030-} from "./transactions.ts";
2323+export { endSession, startSession, withTransaction } from "./transactions.ts";
+12-12
client/transactions.ts
···4455/**
66 * Transaction management module
77- *
77+ *
88 * Provides session and transaction management functionality including
99 * automatic transaction handling and manual session control.
1010 */
11111212/**
1313 * Start a new client session for transactions
1414- *
1414+ *
1515 * Sessions must be ended when done using `endSession()`
1616- *
1616+ *
1717 * @returns New MongoDB ClientSession
1818 * @throws {ConnectionError} If not connected
1919- *
1919+ *
2020 * @example
2121 * ```ts
2222 * const session = startSession();
···37373838/**
3939 * End a client session
4040- *
4040+ *
4141 * @param session - The session to end
4242 */
4343export async function endSession(session: ClientSession): Promise<void> {
···46464747/**
4848 * Execute a function within a transaction
4949- *
4949+ *
5050 * Automatically handles session creation, transaction start/commit/abort, and cleanup.
5151 * If the callback throws an error, the transaction is automatically aborted.
5252- *
5252+ *
5353 * @param callback - Async function to execute within the transaction. Receives the session as parameter.
5454 * @param options - Optional transaction options (read/write concern, etc.)
5555 * @returns The result from the callback function
5656- *
5656+ *
5757 * @example
5858 * ```ts
5959 * const result = await withTransaction(async (session) => {
···6565 */
6666export async function withTransaction<T>(
6767 callback: (session: ClientSession) => Promise<T>,
6868- options?: TransactionOptions
6868+ options?: TransactionOptions,
6969): Promise<T> {
7070 const session = startSession();
7171-7171+7272 try {
7373 let result: T;
7474-7474+7575 await session.withTransaction(async () => {
7676 result = await callback(session);
7777 }, options);
7878-7878+7979 return result!;
8080 } finally {
8181 await endSession(session);
···11-export type { Schema, Infer, Input } from "./types.ts";
22-export {
33- connect,
44- disconnect,
55- healthCheck,
66- startSession,
11+export type { Infer, Input, Schema } from "./types.ts";
22+export {
33+ connect,
44+ type ConnectOptions,
55+ disconnect,
76 endSession,
77+ healthCheck,
88+ type HealthCheckResult,
99+ startSession,
810 withTransaction,
99- type ConnectOptions,
1010- type HealthCheckResult
1111} from "./client/index.ts";
1212export { Model } from "./model/index.ts";
1313export {
1414- NozzleError,
1515- ValidationError,
1414+ AsyncValidationError,
1515+ ConfigurationError,
1616 ConnectionError,
1717- ConfigurationError,
1817 DocumentNotFoundError,
1818+ NozzleError,
1919 OperationError,
2020- AsyncValidationError,
2020+ ValidationError,
2121} from "./errors.ts";
22222323// Re-export MongoDB types that users might need
+81-62
model/core.ts
···11import type { z } from "@zod/zod";
22import type {
33+ AggregateOptions,
44+ BulkWriteOptions,
35 Collection,
66+ CountDocumentsOptions,
77+ DeleteOptions,
48 DeleteResult,
59 Document,
610 Filter,
1111+ FindOneAndReplaceOptions,
1212+ FindOneAndUpdateOptions,
1313+ FindOptions,
714 InsertManyResult,
88- InsertOneResult,
915 InsertOneOptions,
1010- FindOptions,
1111- UpdateOptions,
1212- ReplaceOptions,
1313- FindOneAndUpdateOptions,
1414- FindOneAndReplaceOptions,
1515- DeleteOptions,
1616- CountDocumentsOptions,
1717- AggregateOptions,
1616+ InsertOneResult,
1717+ ModifyResult,
1818 OptionalUnlessRequiredId,
1919+ ReplaceOptions,
2020+ UpdateFilter,
2121+ UpdateOptions,
1922 UpdateResult,
2023 WithId,
2121- BulkWriteOptions,
2222- UpdateFilter,
2323- ModifyResult,
2424} from "mongodb";
2525import { ObjectId } from "mongodb";
2626-import type { Schema, Infer, Input } from "../types.ts";
2727-import { parse, parsePartial, parseReplace, applyDefaultsForUpsert } from "./validation.ts";
2626+import type { Infer, Input, Schema } from "../types.ts";
2727+import {
2828+ applyDefaultsForUpsert,
2929+ parse,
3030+ parsePartial,
3131+ parseReplace,
3232+} from "./validation.ts";
28332934/**
3035 * Core CRUD operations for the Model class
3131- *
3636+ *
3237 * This module contains all basic create, read, update, and delete operations
3338 * with automatic Zod validation and transaction support.
3439 */
35403641/**
3742 * Insert a single document into the collection
3838- *
4343+ *
3944 * @param collection - MongoDB collection
4045 * @param schema - Zod schema for validation
4146 * @param data - Document data to insert
···4651 collection: Collection<Infer<T>>,
4752 schema: T,
4853 data: Input<T>,
4949- options?: InsertOneOptions
5454+ options?: InsertOneOptions,
5055): Promise<InsertOneResult<Infer<T>>> {
5156 const validatedData = parse(schema, data);
5257 return await collection.insertOne(
5358 validatedData as OptionalUnlessRequiredId<Infer<T>>,
5454- options
5959+ options,
5560 );
5661}
57625863/**
5964 * Insert multiple documents into the collection
6060- *
6565+ *
6166 * @param collection - MongoDB collection
6267 * @param schema - Zod schema for validation
6368 * @param data - Array of document data to insert
···6873 collection: Collection<Infer<T>>,
6974 schema: T,
7075 data: Input<T>[],
7171- options?: BulkWriteOptions
7676+ options?: BulkWriteOptions,
7277): Promise<InsertManyResult<Infer<T>>> {
7378 const validatedData = data.map((item) => parse(schema, item));
7479 return await collection.insertMany(
7580 validatedData as OptionalUnlessRequiredId<Infer<T>>[],
7676- options
8181+ options,
7782 );
7883}
79848085/**
8186 * Find multiple documents matching the query
8282- *
8787+ *
8388 * @param collection - MongoDB collection
8489 * @param query - MongoDB query filter
8590 * @param options - Find options (including session for transactions)
···8893export async function find<T extends Schema>(
8994 collection: Collection<Infer<T>>,
9095 query: Filter<Infer<T>>,
9191- options?: FindOptions
9696+ options?: FindOptions,
9297): Promise<(WithId<Infer<T>>)[]> {
9398 return await collection.find(query, options).toArray();
9499}
9510096101/**
97102 * Find a single document matching the query
9898- *
103103+ *
99104 * @param collection - MongoDB collection
100105 * @param query - MongoDB query filter
101106 * @param options - Find options (including session for transactions)
···104109export async function findOne<T extends Schema>(
105110 collection: Collection<Infer<T>>,
106111 query: Filter<Infer<T>>,
107107- options?: FindOptions
112112+ options?: FindOptions,
108113): Promise<WithId<Infer<T>> | null> {
109114 return await collection.findOne(query, options);
110115}
111116112117/**
113118 * Find a document by its MongoDB ObjectId
114114- *
119119+ *
115120 * @param collection - MongoDB collection
116121 * @param id - Document ID (string or ObjectId)
117122 * @param options - Find options (including session for transactions)
···120125export async function findById<T extends Schema>(
121126 collection: Collection<Infer<T>>,
122127 id: string | ObjectId,
123123- options?: FindOptions
128128+ options?: FindOptions,
124129): Promise<WithId<Infer<T>> | null> {
125130 const objectId = typeof id === "string" ? new ObjectId(id) : id;
126126- return await findOne(collection, { _id: objectId } as Filter<Infer<T>>, options);
131131+ return await findOne(
132132+ collection,
133133+ { _id: objectId } as Filter<Infer<T>>,
134134+ options,
135135+ );
127136}
128137129138/**
130139 * Update multiple documents matching the query
131131- *
140140+ *
132141 * Case handling:
133142 * - If upsert: false (or undefined) → Normal update, no defaults applied
134143 * - If upsert: true → Defaults added to $setOnInsert for new document creation
135135- *
144144+ *
136145 * @param collection - MongoDB collection
137146 * @param schema - Zod schema for validation
138147 * @param query - MongoDB query filter
···145154 schema: T,
146155 query: Filter<Infer<T>>,
147156 data: Partial<z.infer<T>>,
148148- options?: UpdateOptions
157157+ options?: UpdateOptions,
149158): Promise<UpdateResult<Infer<T>>> {
150159 const validatedData = parsePartial(schema, data);
151151- let updateDoc: UpdateFilter<Infer<T>> = { $set: validatedData as Partial<Infer<T>> };
152152-160160+ let updateDoc: UpdateFilter<Infer<T>> = {
161161+ $set: validatedData as Partial<Infer<T>>,
162162+ };
163163+153164 // If this is an upsert, apply defaults using $setOnInsert
154165 if (options?.upsert) {
155166 updateDoc = applyDefaultsForUpsert(schema, query, updateDoc);
156167 }
157157-168168+158169 return await collection.updateMany(query, updateDoc, options);
159170}
160171161172/**
162173 * Update a single document matching the query
163163- *
174174+ *
164175 * Case handling:
165176 * - If upsert: false (or undefined) → Normal update, no defaults applied
166177 * - If upsert: true → Defaults added to $setOnInsert for new document creation
167167- *
178178+ *
168179 * @param collection - MongoDB collection
169180 * @param schema - Zod schema for validation
170181 * @param query - MongoDB query filter
···177188 schema: T,
178189 query: Filter<Infer<T>>,
179190 data: Partial<z.infer<T>>,
180180- options?: UpdateOptions
191191+ options?: UpdateOptions,
181192): Promise<UpdateResult<Infer<T>>> {
182193 const validatedData = parsePartial(schema, data);
183183- let updateDoc: UpdateFilter<Infer<T>> = { $set: validatedData as Partial<Infer<T>> };
184184-194194+ let updateDoc: UpdateFilter<Infer<T>> = {
195195+ $set: validatedData as Partial<Infer<T>>,
196196+ };
197197+185198 // If this is an upsert, apply defaults using $setOnInsert
186199 if (options?.upsert) {
187200 updateDoc = applyDefaultsForUpsert(schema, query, updateDoc);
188201 }
189189-202202+190203 return await collection.updateOne(query, updateDoc, options);
191204}
192205193206/**
194207 * Replace a single document matching the query
195195- *
208208+ *
196209 * Case handling:
197210 * - If upsert: false (or undefined) → Normal replace on existing doc, no additional defaults
198211 * - If upsert: true → Defaults applied via parse() since we're passing a full document
199199- *
212212+ *
200213 * Note: For replace operations, defaults are automatically applied by the schema's
201214 * parse() function which treats missing fields as candidates for defaults. This works
202215 * for both regular replaces and upsert-creates since we're providing a full document.
203203- *
216216+ *
204217 * @param collection - MongoDB collection
205218 * @param schema - Zod schema for validation
206219 * @param query - MongoDB query filter
···213226 schema: T,
214227 query: Filter<Infer<T>>,
215228 data: Input<T>,
216216- options?: ReplaceOptions
229229+ options?: ReplaceOptions,
217230): Promise<UpdateResult<Infer<T>>> {
218231 // parseReplace will apply all schema defaults to missing fields
219232 // This works correctly for both regular replaces and upsert-created documents
220233 const validatedData = parseReplace(schema, data);
221221-234234+222235 // Remove _id from validatedData for replaceOne (it will use the query's _id)
223236 const { _id, ...withoutId } = validatedData as Infer<T> & { _id?: unknown };
224237 return await collection.replaceOne(
225238 query,
226239 withoutId as Infer<T>,
227227- options
240240+ options,
228241 );
229242}
230243231244/**
232245 * Find a single document and update it
233233- *
246246+ *
234247 * Case handling:
235248 * - If upsert: false (or undefined) → Normal update
236249 * - If upsert: true → Defaults added to $setOnInsert for new document creation
···240253 schema: T,
241254 query: Filter<Infer<T>>,
242255 data: Partial<z.infer<T>>,
243243- options?: FindOneAndUpdateOptions
256256+ options?: FindOneAndUpdateOptions,
244257): Promise<ModifyResult<Infer<T>>> {
245258 const validatedData = parsePartial(schema, data);
246246- let updateDoc: UpdateFilter<Infer<T>> = { $set: validatedData as Partial<Infer<T>> };
259259+ let updateDoc: UpdateFilter<Infer<T>> = {
260260+ $set: validatedData as Partial<Infer<T>>,
261261+ };
247262248263 if (options?.upsert) {
249264 updateDoc = applyDefaultsForUpsert(schema, query, updateDoc);
250265 }
251266252252- const resolvedOptions: FindOneAndUpdateOptions & { includeResultMetadata: true } = {
267267+ const resolvedOptions: FindOneAndUpdateOptions & {
268268+ includeResultMetadata: true;
269269+ } = {
253270 ...(options ?? {}),
254271 includeResultMetadata: true as const,
255272 };
···259276260277/**
261278 * Find a single document and replace it
262262- *
279279+ *
263280 * Defaults are applied via parseReplace(), which fills in missing fields
264281 * for both normal replacements and upsert-created documents.
265282 */
···268285 schema: T,
269286 query: Filter<Infer<T>>,
270287 data: Input<T>,
271271- options?: FindOneAndReplaceOptions
288288+ options?: FindOneAndReplaceOptions,
272289): Promise<ModifyResult<Infer<T>>> {
273290 const validatedData = parseReplace(schema, data);
274291 const { _id, ...withoutId } = validatedData as Infer<T> & { _id?: unknown };
275292276276- const resolvedOptions: FindOneAndReplaceOptions & { includeResultMetadata: true } = {
293293+ const resolvedOptions: FindOneAndReplaceOptions & {
294294+ includeResultMetadata: true;
295295+ } = {
277296 ...(options ?? {}),
278297 includeResultMetadata: true as const,
279298 };
···281300 return await collection.findOneAndReplace(
282301 query,
283302 withoutId as Infer<T>,
284284- resolvedOptions
303303+ resolvedOptions,
285304 );
286305}
287306288307/**
289308 * Delete multiple documents matching the query
290290- *
309309+ *
291310 * @param collection - MongoDB collection
292311 * @param query - MongoDB query filter
293312 * @param options - Delete options (including session for transactions)
···296315export async function deleteMany<T extends Schema>(
297316 collection: Collection<Infer<T>>,
298317 query: Filter<Infer<T>>,
299299- options?: DeleteOptions
318318+ options?: DeleteOptions,
300319): Promise<DeleteResult> {
301320 return await collection.deleteMany(query, options);
302321}
303322304323/**
305324 * Delete a single document matching the query
306306- *
325325+ *
307326 * @param collection - MongoDB collection
308327 * @param query - MongoDB query filter
309328 * @param options - Delete options (including session for transactions)
···312331export async function deleteOne<T extends Schema>(
313332 collection: Collection<Infer<T>>,
314333 query: Filter<Infer<T>>,
315315- options?: DeleteOptions
334334+ options?: DeleteOptions,
316335): Promise<DeleteResult> {
317336 return await collection.deleteOne(query, options);
318337}
319338320339/**
321340 * Count documents matching the query
322322- *
341341+ *
323342 * @param collection - MongoDB collection
324343 * @param query - MongoDB query filter
325344 * @param options - Count options (including session for transactions)
···328347export async function count<T extends Schema>(
329348 collection: Collection<Infer<T>>,
330349 query: Filter<Infer<T>>,
331331- options?: CountDocumentsOptions
350350+ options?: CountDocumentsOptions,
332351): Promise<number> {
333352 return await collection.countDocuments(query, options);
334353}
335354336355/**
337356 * Execute an aggregation pipeline
338338- *
357357+ *
339358 * @param collection - MongoDB collection
340359 * @param pipeline - MongoDB aggregation pipeline
341360 * @param options - Aggregate options (including session for transactions)
···344363export async function aggregate<T extends Schema>(
345364 collection: Collection<Infer<T>>,
346365 pipeline: Document[],
347347- options?: AggregateOptions
366366+ options?: AggregateOptions,
348367): Promise<Document[]> {
349368 return await collection.aggregate(pipeline, options).toArray();
350369}
+90-60
model/index.ts
···11import type { z } from "@zod/zod";
22import type {
33+ AggregateOptions,
44+ BulkWriteOptions,
35 Collection,
66+ CountDocumentsOptions,
47 CreateIndexesOptions,
88+ DeleteOptions,
59 DeleteResult,
610 Document,
711 DropIndexesOptions,
812 Filter,
1313+ FindOneAndReplaceOptions,
1414+ FindOneAndUpdateOptions,
1515+ FindOptions,
916 IndexDescription,
1017 IndexSpecification,
1118 InsertManyResult,
1919+ InsertOneOptions,
1220 InsertOneResult,
1313- InsertOneOptions,
1414- FindOptions,
1515- UpdateOptions,
2121+ ListIndexesOptions,
2222+ ModifyResult,
1623 ReplaceOptions,
1717- FindOneAndUpdateOptions,
1818- FindOneAndReplaceOptions,
1919- DeleteOptions,
2020- CountDocumentsOptions,
2121- AggregateOptions,
2222- ListIndexesOptions,
2424+ UpdateOptions,
2325 UpdateResult,
2426 WithId,
2525- BulkWriteOptions,
2626- ModifyResult,
2727} from "mongodb";
2828import type { ObjectId } from "mongodb";
2929import { getDb } from "../client/connection.ts";
3030-import type { Schema, Infer, Input, Indexes, ModelDef } from "../types.ts";
3030+import type { Indexes, Infer, Input, ModelDef, Schema } from "../types.ts";
3131import * as core from "./core.ts";
3232import * as indexes from "./indexes.ts";
3333import * as pagination from "./pagination.ts";
34343535/**
3636 * Model class for type-safe MongoDB operations
3737- *
3737+ *
3838 * Provides a clean API for CRUD operations, pagination, and index management
3939 * with automatic Zod validation and TypeScript type safety.
4040- *
4040+ *
4141 * @example
4242 * ```ts
4343 * const userSchema = z.object({
4444 * name: z.string(),
4545 * email: z.string().email(),
4646 * });
4747- *
4747+ *
4848 * const UserModel = new Model("users", userSchema);
4949 * await UserModel.insertOne({ name: "Alice", email: "alice@example.com" });
5050 * ```
···6262 this.schema = definition as T;
6363 }
6464 this.collection = getDb().collection<Infer<T>>(collectionName);
6565-6565+6666 // Automatically create indexes if they were provided
6767 if (this.indexes && this.indexes.length > 0) {
6868 // Fire and forget - indexes will be created asynchronously
6969- indexes.syncIndexes(this.collection, this.indexes)
6969+ indexes.syncIndexes(this.collection, this.indexes);
7070 }
7171 }
7272···76767777 /**
7878 * Insert a single document into the collection
7979- *
7979+ *
8080 * @param data - Document data to insert
8181 * @param options - Insert options (including session for transactions)
8282 * @returns Insert result with insertedId
8383 */
8484 async insertOne(
8585 data: Input<T>,
8686- options?: InsertOneOptions
8686+ options?: InsertOneOptions,
8787 ): Promise<InsertOneResult<Infer<T>>> {
8888 return await core.insertOne(this.collection, this.schema, data, options);
8989 }
90909191 /**
9292 * Insert multiple documents into the collection
9393- *
9393+ *
9494 * @param data - Array of document data to insert
9595 * @param options - Insert options (including session for transactions)
9696 * @returns Insert result with insertedIds
9797 */
9898 async insertMany(
9999 data: Input<T>[],
100100- options?: BulkWriteOptions
100100+ options?: BulkWriteOptions,
101101 ): Promise<InsertManyResult<Infer<T>>> {
102102 return await core.insertMany(this.collection, this.schema, data, options);
103103 }
104104105105 /**
106106 * Find multiple documents matching the query
107107- *
107107+ *
108108 * @param query - MongoDB query filter
109109 * @param options - Find options (including session for transactions)
110110 * @returns Array of matching documents
111111 */
112112 async find(
113113 query: Filter<Infer<T>>,
114114- options?: FindOptions
114114+ options?: FindOptions,
115115 ): Promise<(WithId<Infer<T>>)[]> {
116116 return await core.find(this.collection, query, options);
117117 }
118118119119 /**
120120 * Find a single document matching the query
121121- *
121121+ *
122122 * @param query - MongoDB query filter
123123 * @param options - Find options (including session for transactions)
124124 * @returns Matching document or null if not found
125125 */
126126 async findOne(
127127 query: Filter<Infer<T>>,
128128- options?: FindOptions
128128+ options?: FindOptions,
129129 ): Promise<WithId<Infer<T>> | null> {
130130 return await core.findOne(this.collection, query, options);
131131 }
132132133133 /**
134134 * Find a document by its MongoDB ObjectId
135135- *
135135+ *
136136 * @param id - Document ID (string or ObjectId)
137137 * @param options - Find options (including session for transactions)
138138 * @returns Matching document or null if not found
139139 */
140140 async findById(
141141 id: string | ObjectId,
142142- options?: FindOptions
142142+ options?: FindOptions,
143143 ): Promise<WithId<Infer<T>> | null> {
144144 return await core.findById(this.collection, id, options);
145145 }
146146147147 /**
148148 * Update multiple documents matching the query
149149- *
149149+ *
150150 * @param query - MongoDB query filter
151151 * @param data - Partial data to update
152152 * @param options - Update options (including session for transactions)
···155155 async update(
156156 query: Filter<Infer<T>>,
157157 data: Partial<z.infer<T>>,
158158- options?: UpdateOptions
158158+ options?: UpdateOptions,
159159 ): Promise<UpdateResult<Infer<T>>> {
160160- return await core.update(this.collection, this.schema, query, data, options);
160160+ return await core.update(
161161+ this.collection,
162162+ this.schema,
163163+ query,
164164+ data,
165165+ options,
166166+ );
161167 }
162168163169 /**
164170 * Update a single document matching the query
165165- *
171171+ *
166172 * @param query - MongoDB query filter
167173 * @param data - Partial data to update
168174 * @param options - Update options (including session for transactions)
···171177 async updateOne(
172178 query: Filter<Infer<T>>,
173179 data: Partial<z.infer<T>>,
174174- options?: UpdateOptions
180180+ options?: UpdateOptions,
175181 ): Promise<UpdateResult<Infer<T>>> {
176176- return await core.updateOne(this.collection, this.schema, query, data, options);
182182+ return await core.updateOne(
183183+ this.collection,
184184+ this.schema,
185185+ query,
186186+ data,
187187+ options,
188188+ );
177189 }
178190179191 /**
180192 * Find a single document and update it
181181- *
193193+ *
182194 * @param query - MongoDB query filter
183195 * @param data - Partial data to update
184196 * @param options - FindOneAndUpdate options (including upsert and returnDocument)
···187199 async findOneAndUpdate(
188200 query: Filter<Infer<T>>,
189201 data: Partial<z.infer<T>>,
190190- options?: FindOneAndUpdateOptions
202202+ options?: FindOneAndUpdateOptions,
191203 ): Promise<ModifyResult<Infer<T>>> {
192192- return await core.findOneAndUpdate(this.collection, this.schema, query, data, options);
204204+ return await core.findOneAndUpdate(
205205+ this.collection,
206206+ this.schema,
207207+ query,
208208+ data,
209209+ options,
210210+ );
193211 }
194212195213 /**
196214 * Replace a single document matching the query
197197- *
215215+ *
198216 * @param query - MongoDB query filter
199217 * @param data - Complete document data for replacement
200218 * @param options - Replace options (including session for transactions)
···203221 async replaceOne(
204222 query: Filter<Infer<T>>,
205223 data: Input<T>,
206206- options?: ReplaceOptions
224224+ options?: ReplaceOptions,
207225 ): Promise<UpdateResult<Infer<T>>> {
208208- return await core.replaceOne(this.collection, this.schema, query, data, options);
226226+ return await core.replaceOne(
227227+ this.collection,
228228+ this.schema,
229229+ query,
230230+ data,
231231+ options,
232232+ );
209233 }
210234211235 /**
212236 * Find a single document and replace it
213213- *
237237+ *
214238 * @param query - MongoDB query filter
215239 * @param data - Complete document data for replacement
216240 * @param options - FindOneAndReplace options (including upsert and returnDocument)
···219243 async findOneAndReplace(
220244 query: Filter<Infer<T>>,
221245 data: Input<T>,
222222- options?: FindOneAndReplaceOptions
246246+ options?: FindOneAndReplaceOptions,
223247 ): Promise<ModifyResult<Infer<T>>> {
224224- return await core.findOneAndReplace(this.collection, this.schema, query, data, options);
248248+ return await core.findOneAndReplace(
249249+ this.collection,
250250+ this.schema,
251251+ query,
252252+ data,
253253+ options,
254254+ );
225255 }
226256227257 /**
228258 * Delete multiple documents matching the query
229229- *
259259+ *
230260 * @param query - MongoDB query filter
231261 * @param options - Delete options (including session for transactions)
232262 * @returns Delete result
233263 */
234264 async delete(
235265 query: Filter<Infer<T>>,
236236- options?: DeleteOptions
266266+ options?: DeleteOptions,
237267 ): Promise<DeleteResult> {
238268 return await core.deleteMany(this.collection, query, options);
239269 }
240270241271 /**
242272 * Delete a single document matching the query
243243- *
273273+ *
244274 * @param query - MongoDB query filter
245275 * @param options - Delete options (including session for transactions)
246276 * @returns Delete result
247277 */
248278 async deleteOne(
249279 query: Filter<Infer<T>>,
250250- options?: DeleteOptions
280280+ options?: DeleteOptions,
251281 ): Promise<DeleteResult> {
252282 return await core.deleteOne(this.collection, query, options);
253283 }
254284255285 /**
256286 * Count documents matching the query
257257- *
287287+ *
258288 * @param query - MongoDB query filter
259289 * @param options - Count options (including session for transactions)
260290 * @returns Number of matching documents
261291 */
262292 async count(
263293 query: Filter<Infer<T>>,
264264- options?: CountDocumentsOptions
294294+ options?: CountDocumentsOptions,
265295 ): Promise<number> {
266296 return await core.count(this.collection, query, options);
267297 }
268298269299 /**
270300 * Execute an aggregation pipeline
271271- *
301301+ *
272302 * @param pipeline - MongoDB aggregation pipeline
273303 * @param options - Aggregate options (including session for transactions)
274304 * @returns Array of aggregation results
275305 */
276306 async aggregate(
277307 pipeline: Document[],
278278- options?: AggregateOptions
308308+ options?: AggregateOptions,
279309 ): Promise<Document[]> {
280310 return await core.aggregate(this.collection, pipeline, options);
281311 }
···286316287317 /**
288318 * Find documents with pagination support
289289- *
319319+ *
290320 * @param query - MongoDB query filter
291321 * @param options - Pagination options (skip, limit, sort)
292322 * @returns Array of matching documents
···304334305335 /**
306336 * Create a single index on the collection
307307- *
337337+ *
308338 * @param keys - Index specification (e.g., { email: 1 } or { name: "text" })
309339 * @param options - Index creation options (unique, sparse, expireAfterSeconds, etc.)
310340 * @returns The name of the created index
···318348319349 /**
320350 * Create multiple indexes on the collection
321321- *
351351+ *
322352 * @param indexes - Array of index descriptions
323353 * @param options - Index creation options
324354 * @returns Array of index names created
···332362333363 /**
334364 * Drop a single index from the collection
335335- *
365365+ *
336366 * @param index - Index name or specification
337367 * @param options - Drop index options
338368 */
···345375346376 /**
347377 * Drop all indexes from the collection (except _id index)
348348- *
378378+ *
349379 * @param options - Drop index options
350380 */
351381 async dropIndexes(options?: DropIndexesOptions): Promise<void> {
···354384355385 /**
356386 * List all indexes on the collection
357357- *
387387+ *
358388 * @param options - List indexes options
359389 * @returns Array of index information
360390 */
···366396367397 /**
368398 * Get index information by name
369369- *
399399+ *
370400 * @param indexName - Name of the index
371401 * @returns Index description or null if not found
372402 */
···376406377407 /**
378408 * Check if an index exists
379379- *
409409+ *
380410 * @param indexName - Name of the index
381411 * @returns True if index exists, false otherwise
382412 */
···386416387417 /**
388418 * Synchronize indexes - create indexes if they don't exist, update if they differ
389389- *
419419+ *
390420 * This is useful for ensuring indexes match your schema definition
391391- *
421421+ *
392422 * @param indexes - Array of index descriptions to synchronize
393423 * @param options - Options for index creation
394424 * @returns Array of index names that were created
+15-15
model/indexes.ts
···66 IndexSpecification,
77 ListIndexesOptions,
88} from "mongodb";
99-import type { Schema, Infer } from "../types.ts";
99+import type { Infer, Schema } from "../types.ts";
10101111/**
1212 * Index management operations for the Model class
1313- *
1313+ *
1414 * This module contains all index-related operations including creation,
1515 * deletion, listing, and synchronization of indexes.
1616 */
17171818/**
1919 * Create a single index on the collection
2020- *
2020+ *
2121 * @param collection - MongoDB collection
2222 * @param keys - Index specification (e.g., { email: 1 } or { name: "text" })
2323 * @param options - Index creation options (unique, sparse, expireAfterSeconds, etc.)
···33333434/**
3535 * Create multiple indexes on the collection
3636- *
3636+ *
3737 * @param collection - MongoDB collection
3838 * @param indexes - Array of index descriptions
3939 * @param options - Index creation options
···49495050/**
5151 * Drop a single index from the collection
5252- *
5252+ *
5353 * @param collection - MongoDB collection
5454 * @param index - Index name or specification
5555 * @param options - Drop index options
···64646565/**
6666 * Drop all indexes from the collection (except _id index)
6767- *
6767+ *
6868 * @param collection - MongoDB collection
6969 * @param options - Drop index options
7070 */
7171export async function dropIndexes<T extends Schema>(
7272 collection: Collection<Infer<T>>,
7373- options?: DropIndexesOptions
7373+ options?: DropIndexesOptions,
7474): Promise<void> {
7575 await collection.dropIndexes(options);
7676}
77777878/**
7979 * List all indexes on the collection
8080- *
8080+ *
8181 * @param collection - MongoDB collection
8282 * @param options - List indexes options
8383 * @returns Array of index information
···92929393/**
9494 * Get index information by name
9595- *
9595+ *
9696 * @param collection - MongoDB collection
9797 * @param indexName - Name of the index
9898 * @returns Index description or null if not found
9999 */
100100export async function getIndex<T extends Schema>(
101101 collection: Collection<Infer<T>>,
102102- indexName: string
102102+ indexName: string,
103103): Promise<IndexDescription | null> {
104104 const indexes = await listIndexes(collection);
105105 return indexes.find((idx) => idx.name === indexName) || null;
···107107108108/**
109109 * Check if an index exists
110110- *
110110+ *
111111 * @param collection - MongoDB collection
112112 * @param indexName - Name of the index
113113 * @returns True if index exists, false otherwise
114114 */
115115export async function indexExists<T extends Schema>(
116116 collection: Collection<Infer<T>>,
117117- indexName: string
117117+ indexName: string,
118118): Promise<boolean> {
119119 const index = await getIndex(collection, indexName);
120120 return index !== null;
···122122123123/**
124124 * Synchronize indexes - create indexes if they don't exist, update if they differ
125125- *
125125+ *
126126 * This is useful for ensuring indexes match your schema definition
127127- *
127127+ *
128128 * @param collection - MongoDB collection
129129 * @param indexes - Array of index descriptions to synchronize
130130 * @param options - Options for index creation
···167167168168/**
169169 * Generate index name from key specification
170170- *
170170+ *
171171 * @param keys - Index specification
172172 * @returns Generated index name
173173 */
+6-11
model/pagination.ts
···11-import type {
22- Collection,
33- Document,
44- Filter,
55- WithId,
66-} from "mongodb";
77-import type { Schema, Infer } from "../types.ts";
11+import type { Collection, Document, Filter, WithId } from "mongodb";
22+import type { Infer, Schema } from "../types.ts";
8394/**
105 * Pagination operations for the Model class
1111- *
66+ *
127 * This module contains pagination-related functionality for finding documents
138 * with skip, limit, and sort options.
149 */
15101611/**
1712 * Find documents with pagination support
1818- *
1313+ *
1914 * @param collection - MongoDB collection
2015 * @param query - MongoDB query filter
2116 * @param options - Pagination options (skip, limit, sort)
2217 * @returns Array of matching documents
2323- *
1818+ *
2419 * @example
2520 * ```ts
2626- * const users = await findPaginated(collection,
2121+ * const users = await findPaginated(collection,
2722 * { age: { $gte: 18 } },
2823 * { skip: 0, limit: 10, sort: { createdAt: -1 } }
2924 * );
+86-51
model/validation.ts
···11import type { z } from "@zod/zod";
22-import type { Schema, Infer, Input } from "../types.ts";
33-import { ValidationError, AsyncValidationError } from "../errors.ts";
44-import type { Document, UpdateFilter, Filter } from "mongodb";
22+import type { Infer, Input, Schema } from "../types.ts";
33+import { AsyncValidationError, ValidationError } from "../errors.ts";
44+import type { Document, Filter, UpdateFilter } from "mongodb";
55+66+// Cache frequently reused schema transformations to avoid repeated allocations
77+const partialSchemaCache = new WeakMap<Schema, z.ZodTypeAny>();
88+const defaultsCache = new WeakMap<Schema, Record<string, unknown>>();
99+const updateOperators = [
1010+ "$set",
1111+ "$unset",
1212+ "$inc",
1313+ "$mul",
1414+ "$rename",
1515+ "$min",
1616+ "$max",
1717+ "$currentDate",
1818+ "$push",
1919+ "$pull",
2020+ "$addToSet",
2121+ "$pop",
2222+ "$bit",
2323+ "$setOnInsert",
2424+];
2525+2626+function getPartialSchema(schema: Schema): z.ZodTypeAny {
2727+ const cached = partialSchemaCache.get(schema);
2828+ if (cached) return cached;
2929+ const partial = schema.partial();
3030+ partialSchemaCache.set(schema, partial);
3131+ return partial;
3232+}
533634/**
735 * Validate data for insert operations using Zod schema
88- *
3636+ *
937 * @param schema - Zod schema to validate against
1038 * @param data - Data to validate
1139 * @returns Validated and typed data
···1442 */
1543export function parse<T extends Schema>(schema: T, data: Input<T>): Infer<T> {
1644 const result = schema.safeParse(data);
1717-4545+1846 // Check for async validation
1947 if (result instanceof Promise) {
2048 throw new AsyncValidationError();
2149 }
2222-5050+2351 if (!result.success) {
2452 throw new ValidationError(result.error.issues, "insert");
2553 }
···28562957/**
3058 * Validate partial data for update operations using Zod schema
3131- *
5959+ *
3260 * Important: This function only validates the fields that are provided in the data object.
3361 * Unlike parse(), this function does NOT apply defaults for missing fields because
3462 * in an update context, missing fields should remain unchanged in the database.
3535- *
6363+ *
3664 * @param schema - Zod schema to validate against
3765 * @param data - Partial data to validate
3866 * @returns Validated and typed partial data (only fields present in input)
···4371 schema: T,
4472 data: Partial<z.infer<T>>,
4573): Partial<z.infer<T>> {
7474+ if (!data || Object.keys(data).length === 0) {
7575+ return {};
7676+ }
7777+4678 // Get the list of fields actually provided in the input
4779 const inputKeys = Object.keys(data);
4848-4949- const result = schema.partial().safeParse(data);
5050-8080+8181+ const result = getPartialSchema(schema).safeParse(data);
8282+5183 // Check for async validation
5284 if (result instanceof Promise) {
5385 throw new AsyncValidationError();
5486 }
5555-8787+5688 if (!result.success) {
5789 throw new ValidationError(result.error.issues, "update");
5890 }
5959-9191+6092 // Filter the result to only include fields that were in the input
6193 // This prevents defaults from being applied to fields that weren't provided
6294 const filtered: Record<string, unknown> = {};
6395 for (const key of inputKeys) {
6464- if (key in result.data) {
9696+ if (key in (result.data as Record<string, unknown>)) {
6597 filtered[key] = (result.data as Record<string, unknown>)[key];
6698 }
6799 }
6868-100100+69101 return filtered as Partial<z.infer<T>>;
70102}
7110372104/**
73105 * Validate data for replace operations using Zod schema
7474- *
106106+ *
75107 * @param schema - Zod schema to validate against
76108 * @param data - Data to validate
77109 * @returns Validated and typed data
78110 * @throws {ValidationError} If validation fails
79111 * @throws {AsyncValidationError} If async validation is detected
80112 */
8181-export function parseReplace<T extends Schema>(schema: T, data: Input<T>): Infer<T> {
113113+export function parseReplace<T extends Schema>(
114114+ schema: T,
115115+ data: Input<T>,
116116+): Infer<T> {
82117 const result = schema.safeParse(data);
8383-118118+84119 // Check for async validation
85120 if (result instanceof Promise) {
86121 throw new AsyncValidationError();
87122 }
8888-123123+89124 if (!result.success) {
90125 throw new ValidationError(result.error.issues, "replace");
91126 }
···95130/**
96131 * Extract default values from a Zod schema
97132 * This parses an empty object through the schema to get all defaults applied
9898- *
133133+ *
99134 * @param schema - Zod schema to extract defaults from
100135 * @returns Object containing all default values from the schema
101136 */
102102-export function extractDefaults<T extends Schema>(schema: T): Partial<Infer<T>> {
137137+export function extractDefaults<T extends Schema>(
138138+ schema: T,
139139+): Partial<Infer<T>> {
140140+ const cached = defaultsCache.get(schema);
141141+ if (cached) {
142142+ return cached as Partial<Infer<T>>;
143143+ }
144144+103145 try {
104146 // Make all fields optional, then parse empty object to trigger defaults
105147 // This allows us to see which fields get default values
106106- const partialSchema = schema.partial();
148148+ const partialSchema = getPartialSchema(schema);
107149 const result = partialSchema.safeParse({});
108108-150150+109151 if (result instanceof Promise) {
110152 // Cannot extract defaults from async schemas
111153 return {};
112154 }
113113-155155+114156 // If successful, the result contains all fields that have defaults
115157 // Only include fields that were actually added (have values)
116158 if (!result.success) {
117159 return {};
118160 }
119119-161161+120162 // Filter to only include fields that got values from defaults
121163 // (not undefined, which indicates no default)
122164 const defaults: Record<string, unknown> = {};
123165 const data = result.data as Record<string, unknown>;
124124-166166+125167 for (const [key, value] of Object.entries(data)) {
126168 if (value !== undefined) {
127169 defaults[key] = value;
128170 }
129171 }
130130-172172+ defaultsCache.set(schema, defaults as Partial<Infer<Schema>>);
131173 return defaults as Partial<Infer<T>>;
132174 } catch {
133175 return {};
···137179/**
138180 * Get all field paths mentioned in an update filter object
139181 * This includes fields in $set, $unset, $inc, $push, etc.
140140- *
182182+ *
141183 * @param update - MongoDB update filter
142184 * @returns Set of field paths that are being modified
143185 */
144186function getModifiedFields(update: UpdateFilter<Document>): Set<string> {
145187 const fields = new Set<string>();
146146-147147- // Operators that modify fields
148148- const operators = [
149149- '$set', '$unset', '$inc', '$mul', '$rename', '$min', '$max',
150150- '$currentDate', '$push', '$pull', '$addToSet', '$pop', '$bit',
151151- '$setOnInsert',
152152- ];
153153-154154- for (const op of operators) {
155155- if (update[op] && typeof update[op] === 'object') {
188188+189189+ for (const op of updateOperators) {
190190+ if (update[op] && typeof update[op] === "object") {
156191 // Add all field names from this operator
157192 for (const field of Object.keys(update[op] as Document)) {
158193 fields.add(field);
159194 }
160195 }
161196 }
162162-197197+163198 return fields;
164199}
165200···209244210245/**
211246 * Apply schema defaults to an update operation using $setOnInsert
212212- *
247247+ *
213248 * This is used for upsert operations to ensure defaults are applied when
214249 * a new document is created, but not when updating an existing document.
215215- *
250250+ *
216251 * For each default field:
217252 * - If the field is NOT mentioned in any update operator ($set, $inc, etc.)
218253 * - If the field is NOT fixed by an equality clause in the query filter
219254 * - Add it to $setOnInsert so it's only applied on insert
220220- *
255255+ *
221256 * @param schema - Zod schema with defaults
222257 * @param query - MongoDB query filter
223258 * @param update - MongoDB update filter
···226261export function applyDefaultsForUpsert<T extends Schema>(
227262 schema: T,
228263 query: Filter<Infer<T>>,
229229- update: UpdateFilter<Infer<T>>
264264+ update: UpdateFilter<Infer<T>>,
230265): UpdateFilter<Infer<T>> {
231266 // Extract defaults from schema
232267 const defaults = extractDefaults(schema);
233233-268268+234269 // If no defaults, return update unchanged
235270 if (Object.keys(defaults).length === 0) {
236271 return update;
237272 }
238238-273273+239274 // Get fields that are already being modified
240275 const modifiedFields = getModifiedFields(update as UpdateFilter<Document>);
241276 const filterEqualityFields = getEqualityFields(query as Filter<Document>);
242242-277277+243278 // Build $setOnInsert with defaults for unmodified fields
244279 const setOnInsert: Partial<Infer<T>> = {};
245245-280280+246281 for (const [field, value] of Object.entries(defaults)) {
247282 // Only add default if field is not already being modified or fixed by filter equality
248283 if (!modifiedFields.has(field) && !filterEqualityFields.has(field)) {
249284 setOnInsert[field as keyof Infer<T>] = value as Infer<T>[keyof Infer<T>];
250285 }
251286 }
252252-287287+253288 // If there are defaults to add, merge them into $setOnInsert
254289 if (Object.keys(setOnInsert).length > 0) {
255290 return {
256291 ...update,
257292 $setOnInsert: {
258293 ...(update.$setOnInsert || {}),
259259- ...setOnInsert
260260- } as Partial<Infer<T>>
294294+ ...setOnInsert,
295295+ } as Partial<Infer<T>>,
261296 };
262297 }
263263-298298+264299 return update;
265300}
+42-37
tests/connection_test.ts
···11import { assert, assertEquals, assertExists } from "@std/assert";
22-import { connect, disconnect, healthCheck, type ConnectOptions } from "../mod.ts";
22+import {
33+ connect,
44+ type ConnectOptions,
55+ disconnect,
66+ healthCheck,
77+} from "../mod.ts";
38import { MongoMemoryServer } from "mongodb-memory-server-core";
49510let mongoServer: MongoMemoryServer | null = null;
···2732 async fn() {
2833 const uri = await setupTestServer();
2934 const connection = await connect(uri, "test_db");
3030-3535+3136 assert(connection);
3237 assert(connection.client);
3338 assert(connection.db);
···4752 maxIdleTimeMS: 30000,
4853 connectTimeoutMS: 5000,
4954 };
5050-5555+5156 const connection = await connect(uri, "test_db", options);
5252-5757+5358 assert(connection);
5459 assert(connection.client);
5560 assert(connection.db);
5656-6161+5762 // Verify connection is working
5863 const adminDb = connection.db.admin();
5964 const serverStatus = await adminDb.serverStatus();
···6772 name: "Connection: Singleton - should reuse existing connection",
6873 async fn() {
6974 const uri = await setupTestServer();
7070-7575+7176 const connection1 = await connect(uri, "test_db");
7277 const connection2 = await connect(uri, "test_db");
7373-7878+7479 // Should return the same connection instance
7580 assertEquals(connection1, connection2);
7681 assertEquals(connection1.client, connection2.client);
···8489 name: "Connection: Disconnect - should disconnect and allow reconnection",
8590 async fn() {
8691 const uri = await setupTestServer();
8787-9292+8893 const connection1 = await connect(uri, "test_db");
8994 assert(connection1);
9090-9595+9196 await disconnect();
9292-9797+9398 // Should be able to reconnect
9499 const connection2 = await connect(uri, "test_db");
95100 assert(connection2);
9696-101101+97102 // Should be a new connection instance
98103 assert(connection1 !== connection2);
99104 },
···108113 const options: ConnectOptions = {
109114 maxPoolSize: 5,
110115 };
111111-116116+112117 const connection = await connect(uri, "test_db", options);
113113-118118+114119 // Verify connection works with custom pool size
115120 const collections = await connection.db.listCollections().toArray();
116121 assert(Array.isArray(collections));
···120125});
121126122127Deno.test({
123123- name: "Connection: Multiple Databases - should handle different database names",
128128+ name:
129129+ "Connection: Multiple Databases - should handle different database names",
124130 async fn() {
125131 const uri = await setupTestServer();
126126-132132+127133 // Connect to first database
128134 const connection1 = await connect(uri, "db1");
129135 assertEquals(connection1.db.databaseName, "db1");
130130-136136+131137 // Disconnect first
132138 await disconnect();
133133-139139+134140 // Connect to second database
135141 const connection2 = await connect(uri, "db2");
136142 assertEquals(connection2.db.databaseName, "db2");
···143149 name: "Health Check: should return unhealthy when not connected",
144150 async fn() {
145151 const result = await healthCheck();
146146-152152+147153 assertEquals(result.healthy, false);
148154 assertEquals(result.connected, false);
149155 assertExists(result.error);
···160166 async fn() {
161167 const uri = await setupTestServer();
162168 await connect(uri, "test_db");
163163-169169+164170 const result = await healthCheck();
165165-171171+166172 assertEquals(result.healthy, true);
167173 assertEquals(result.connected, true);
168174 assertExists(result.responseTimeMs);
···179185 async fn() {
180186 const uri = await setupTestServer();
181187 await connect(uri, "test_db");
182182-188188+183189 const result = await healthCheck();
184184-190190+185191 assertEquals(result.healthy, true);
186192 assertExists(result.responseTimeMs);
187193 // Response time should be reasonable (less than 1 second for in-memory MongoDB)
···196202 async fn() {
197203 const uri = await setupTestServer();
198204 await connect(uri, "test_db");
199199-205205+200206 // Run health check multiple times
201207 const results = await Promise.all([
202208 healthCheck(),
203209 healthCheck(),
204210 healthCheck(),
205211 ]);
206206-212212+207213 // All should be healthy
208214 for (const result of results) {
209215 assertEquals(result.healthy, true);
···220226 async fn() {
221227 const uri = await setupTestServer();
222228 await connect(uri, "test_db");
223223-229229+224230 // First check should be healthy
225231 let result = await healthCheck();
226232 assertEquals(result.healthy, true);
227227-233233+228234 // Disconnect
229235 await disconnect();
230230-236236+231237 // Second check should be unhealthy
232238 result = await healthCheck();
233239 assertEquals(result.healthy, false);
···247253 serverSelectionTimeoutMS: 5000,
248254 connectTimeoutMS: 5000,
249255 };
250250-256256+251257 const connection = await connect(uri, "test_db", options);
252252-258258+253259 assert(connection);
254260 assert(connection.client);
255261 assert(connection.db);
256256-262262+257263 // Verify connection works with retry options
258264 const collections = await connection.db.listCollections().toArray();
259265 assert(Array.isArray(collections));
···270276 // Pooling
271277 maxPoolSize: 10,
272278 minPoolSize: 2,
273273-279279+274280 // Retry logic
275281 retryReads: true,
276282 retryWrites: true,
277277-283283+278284 // Timeouts
279285 connectTimeoutMS: 10000,
280286 socketTimeoutMS: 45000,
281287 serverSelectionTimeoutMS: 10000,
282282-288288+283289 // Resilience
284290 maxIdleTimeMS: 30000,
285291 heartbeatFrequencyMS: 10000,
286292 };
287287-293293+288294 const connection = await connect(uri, "test_db", options);
289289-295295+290296 assert(connection);
291291-297297+292298 // Verify connection is working
293299 const adminDb = connection.db.admin();
294300 const serverStatus = await adminDb.serverStatus();
···297303 sanitizeResources: false,
298304 sanitizeOps: false,
299305});
300300-
+1-8
tests/crud_test.ts
···14141515Deno.test.beforeAll(async () => {
1616 await setupTestDb();
1717- UserModel = createUserModel();
1717+ UserModel = createUserModel("users_crud");
1818});
19192020Deno.test.beforeEach(async () => {
···2828Deno.test({
2929 name: "CRUD: Insert - should insert a new user successfully",
3030 async fn() {
3131-3231 const newUser: UserInsert = {
3332 name: "Test User",
3433 email: "test@example.com",
···4746Deno.test({
4847 name: "CRUD: Find - should find the inserted user",
4948 async fn() {
5050-5149 // First insert a user for this test
5250 const newUser: UserInsert = {
5351 name: "Find Test User",
···7371Deno.test({
7472 name: "CRUD: Update - should update user data",
7573 async fn() {
7676-7774 // Insert a user for this test
7875 const newUser: UserInsert = {
7976 name: "Update Test User",
···106103Deno.test({
107104 name: "CRUD: Delete - should delete user successfully",
108105 async fn() {
109109-110106 // Insert a user for this test
111107 const newUser: UserInsert = {
112108 name: "Delete Test User",
···137133Deno.test({
138134 name: "CRUD: Find Multiple - should find multiple users",
139135 async fn() {
140140-141136 // Insert multiple users
142137 const users: UserInsert[] = [
143138 { name: "User 1", email: "user1@example.com", age: 20 },
···157152 sanitizeResources: false,
158153 sanitizeOps: false,
159154});
160160-161161-
···13131414let mongoServer: MongoMemoryServer | null = null;
1515let isSetup = false;
1616+let setupRefCount = 0;
1717+let activeDbName: string | null = null;
16181717-export async function setupTestDb() {
1818- if (!isSetup) {
1919- // Start MongoDB Memory Server
1919+export async function setupTestDb(dbName = "test_db") {
2020+ setupRefCount++;
2121+2222+ // If we're already connected, just share the same database
2323+ if (isSetup) {
2424+ if (activeDbName !== dbName) {
2525+ throw new Error(
2626+ `Test DB already initialized for ${activeDbName}, requested ${dbName}`,
2727+ );
2828+ }
2929+ return;
3030+ }
3131+3232+ try {
2033 mongoServer = await MongoMemoryServer.create();
2134 const uri = mongoServer.getUri();
2222-2323- // Connect to the in-memory database
2424- await connect(uri, "test_db");
3535+3636+ await connect(uri, dbName);
3737+ activeDbName = dbName;
2538 isSetup = true;
3939+ } catch (error) {
4040+ // Roll back refcount if setup failed so future attempts can retry
4141+ setupRefCount = Math.max(0, setupRefCount - 1);
4242+ throw error;
2643 }
2744}
28452946export async function teardownTestDb() {
3030- if (isSetup) {
4747+ if (setupRefCount === 0) {
4848+ return;
4949+ }
5050+5151+ setupRefCount = Math.max(0, setupRefCount - 1);
5252+5353+ if (isSetup && setupRefCount === 0) {
3154 await disconnect();
3255 if (mongoServer) {
3356 await mongoServer.stop();
3457 mongoServer = null;
3558 }
5959+ activeDbName = null;
3660 isSetup = false;
3761 }
3862}
39634040-export function createUserModel(): Model<typeof userSchema> {
4141- return new Model("users", userSchema);
6464+export function createUserModel(
6565+ collectionName = "users",
6666+): Model<typeof userSchema> {
6767+ return new Model(collectionName, userSchema);
4268}
43694470export async function cleanupCollection(model: Model<typeof userSchema>) {
4571 await model.delete({});
4672}
4747-
+1-8
tests/validation_test.ts
···14141515Deno.test.beforeAll(async () => {
1616 await setupTestDb();
1717- UserModel = createUserModel();
1717+ UserModel = createUserModel("users_validation");
1818});
19192020Deno.test.beforeEach(async () => {
···2828Deno.test({
2929 name: "Validation: Schema - should validate user data on insert",
3030 async fn() {
3131-3231 const invalidUser = {
3332 name: "Invalid User",
3433 email: "not-an-email", // Invalid email
···5049Deno.test({
5150 name: "Validation: Update - should reject invalid email in update",
5251 async fn() {
5353-5452 // Insert a user for this test
5553 const newUser: UserInsert = {
5654 name: "Validation Test User",
···7977Deno.test({
8078 name: "Validation: Update - should reject negative age in update",
8179 async fn() {
8282-8380 // Insert a user for this test
8481 const newUser: UserInsert = {
8582 name: "Age Validation Test User",
···108105Deno.test({
109106 name: "Validation: Update - should reject invalid name type in update",
110107 async fn() {
111111-112108 // Insert a user for this test
113109 const newUser: UserInsert = {
114110 name: "Type Validation Test User",
···137133Deno.test({
138134 name: "Validation: Update - should accept valid partial updates",
139135 async fn() {
140140-141136 // Insert a user for this test
142137 const newUser: UserInsert = {
143138 name: "Valid Update Test User",
···168163 sanitizeResources: false,
169164 sanitizeOps: false,
170165});
171171-172172-
+5-6
types.ts
···11import type { z } from "@zod/zod";
22-import type { Document, ObjectId, IndexDescription } from "mongodb";
22+import type { Document, IndexDescription, ObjectId } from "mongodb";
3344/**
55 * Type alias for Zod schema objects
···1111 */
1212export type Infer<T extends Schema> = z.infer<T> & Document;
13131414-1514/**
1615 * Infer the model type from a Zod schema, including MongoDB Document and ObjectId
1716 */
1817export type InferModel<T extends Schema> = Infer<T> & {
1919- _id?: ObjectId;
2020- };
1818+ _id?: ObjectId;
1919+};
21202221/**
2322 * Infer the input type for a Zod schema (handles defaults)
···31303231/**
3332 * Complete definition of a model, including schema and indexes
3434- *
3333+ *
3534 * @example
3635 * ```ts
3736 * const userDef: ModelDef<typeof userSchema> = {
···4645export type ModelDef<T extends Schema> = {
4746 schema: T;
4847 indexes?: Indexes;
4949-};4848+};