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}