Thin MongoDB ODM built for Standard Schema
mongodb zod deno
1import { 2 type Db, 3 type MongoClientOptions, 4 type ClientSession, 5 type TransactionOptions, 6 MongoClient 7} from "mongodb"; 8import { ConnectionError } from "./errors.ts"; 9 10interface Connection { 11 client: MongoClient; 12 db: Db; 13} 14 15let connection: Connection | null = null; 16 17export interface ConnectOptions extends MongoClientOptions {}; 18 19/** 20 * Health check details of the MongoDB connection 21 * 22 * @property healthy - Overall health status of the connection 23 * @property connected - Whether a connection is established 24 * @property responseTimeMs - Response time in milliseconds (if connection is healthy) 25 * @property error - Error message if health check failed 26 * @property timestamp - Timestamp when health check was performed 27 */ 28export interface HealthCheckResult { 29 healthy: boolean; 30 connected: boolean; 31 responseTimeMs?: number; 32 error?: string; 33 timestamp: Date; 34} 35 36/** 37 * Connect to MongoDB with connection pooling, retry logic, and resilience options 38 * 39 * The MongoDB driver handles connection pooling and automatic retries. 40 * Retry logic is enabled by default for both reads and writes in MongoDB 4.2+. 41 * 42 * @param uri - MongoDB connection string 43 * @param dbName - Name of the database to connect to 44 * @param options - Connection options (pooling, retries, timeouts, etc.) 45 * 46 * @example 47 * Basic connection with pooling: 48 * ```ts 49 * await connect("mongodb://localhost:27017", "mydb", { 50 * maxPoolSize: 10, 51 * minPoolSize: 2, 52 * maxIdleTimeMS: 30000, 53 * connectTimeoutMS: 10000, 54 * socketTimeoutMS: 45000, 55 * }); 56 * ``` 57 * 58 * @example 59 * Production-ready connection with retry logic and resilience: 60 * ```ts 61 * await connect("mongodb://localhost:27017", "mydb", { 62 * // Connection pooling 63 * maxPoolSize: 10, 64 * minPoolSize: 2, 65 * 66 * // Automatic retry logic (enabled by default) 67 * retryReads: true, // Retry failed read operations 68 * retryWrites: true, // Retry failed write operations 69 * 70 * // Timeouts 71 * connectTimeoutMS: 10000, // Initial connection timeout 72 * socketTimeoutMS: 45000, // Socket operation timeout 73 * serverSelectionTimeoutMS: 10000, // Server selection timeout 74 * 75 * // Connection resilience 76 * maxIdleTimeMS: 30000, // Close idle connections 77 * heartbeatFrequencyMS: 10000, // Server health check interval 78 * 79 * // Optional: Compression for reduced bandwidth 80 * compressors: ['snappy', 'zlib'], 81 * }); 82 * ``` 83 */ 84export async function connect( 85 uri: string, 86 dbName: string, 87 options?: ConnectOptions, 88): Promise<Connection> { 89 if (connection) { 90 return connection; 91 } 92 93 try { 94 const client = new MongoClient(uri, options); 95 await client.connect(); 96 const db = client.db(dbName); 97 98 connection = { client, db }; 99 return connection; 100 } catch (error) { 101 throw new ConnectionError( 102 `Failed to connect to MongoDB: ${error instanceof Error ? error.message : String(error)}`, 103 uri 104 ); 105 } 106} 107 108export async function disconnect(): Promise<void> { 109 if (connection) { 110 await connection.client.close(); 111 connection = null; 112 } 113} 114 115/** 116 * Start a new client session for transactions 117 * 118 * Sessions must be ended when done using `endSession()` 119 * 120 * @example 121 * ```ts 122 * const session = await startSession(); 123 * try { 124 * // use session 125 * } finally { 126 * await endSession(session); 127 * } 128 * ``` 129 */ 130export function startSession(): ClientSession { 131 if (!connection) { 132 throw new ConnectionError("MongoDB not connected. Call connect() first."); 133 } 134 return connection.client.startSession(); 135} 136 137/** 138 * End a client session 139 * 140 * @param session - The session to end 141 */ 142export async function endSession(session: ClientSession): Promise<void> { 143 await session.endSession(); 144} 145 146/** 147 * Execute a function within a transaction 148 * 149 * Automatically handles session creation, transaction start/commit/abort, and cleanup. 150 * If the callback throws an error, the transaction is automatically aborted. 151 * 152 * @param callback - Async function to execute within the transaction. Receives the session as parameter. 153 * @param options - Optional transaction options (read/write concern, etc.) 154 * @returns The result from the callback function 155 * 156 * @example 157 * ```ts 158 * const result = await withTransaction(async (session) => { 159 * await UserModel.insertOne({ name: "Alice" }, { session }); 160 * await OrderModel.insertOne({ userId: "123", total: 100 }, { session }); 161 * return { success: true }; 162 * }); 163 * ``` 164 */ 165export async function withTransaction<T>( 166 callback: (session: ClientSession) => Promise<T>, 167 options?: TransactionOptions 168): Promise<T> { 169 const session = await startSession(); 170 171 try { 172 let result: T; 173 174 await session.withTransaction(async () => { 175 result = await callback(session); 176 }, options); 177 178 return result!; 179 } finally { 180 await endSession(session); 181 } 182} 183 184export function getDb(): Db { 185 if (!connection) { 186 throw new ConnectionError("MongoDB not connected. Call connect() first."); 187 } 188 return connection.db; 189} 190 191/** 192 * Check the health of the MongoDB connection 193 * 194 * Performs a ping operation to verify the database is responsive 195 * and returns detailed health information including response time. 196 * 197 * @example 198 * ```ts 199 * const health = await healthCheck(); 200 * if (health.healthy) { 201 * console.log(`Database healthy (${health.responseTimeMs}ms)`); 202 * } else { 203 * console.error(`Database unhealthy: ${health.error}`); 204 * } 205 * ``` 206 */ 207export async function healthCheck(): Promise<HealthCheckResult> { 208 const timestamp = new Date(); 209 210 // Check if connection exists 211 if (!connection) { 212 return { 213 healthy: false, 214 connected: false, 215 error: "No active connection. Call connect() first.", 216 timestamp, 217 }; 218 } 219 220 try { 221 // Measure ping response time 222 const startTime = performance.now(); 223 await connection.db.admin().ping(); 224 const endTime = performance.now(); 225 const responseTimeMs = Math.round(endTime - startTime); 226 227 return { 228 healthy: true, 229 connected: true, 230 responseTimeMs, 231 timestamp, 232 }; 233 } catch (error) { 234 return { 235 healthy: false, 236 connected: true, 237 error: error instanceof Error ? error.message : String(error), 238 timestamp, 239 }; 240 } 241}