Thin MongoDB ODM built for Standard Schema
mongodb zod deno
1import { assertEquals, assertExists, assertRejects } from "@std/assert"; 2import { 3 connect, 4 disconnect, 5 Model, 6 withTransaction, 7 startSession, 8 endSession, 9} from "../mod.ts"; 10import { z } from "@zod/zod"; 11import { MongoMemoryReplSet } from "mongodb-memory-server-core"; 12 13let replSet: MongoMemoryReplSet | null = null; 14 15async function setupTestReplSet() { 16 if (!replSet) { 17 replSet = await MongoMemoryReplSet.create({ 18 replSet: { 19 count: 3, 20 storageEngine: 'wiredTiger' // Required for transactions 21 }, 22 }); 23 } 24 return replSet.getUri(); 25} 26 27Deno.test.afterEach(async () => { 28 // Clean up database 29 if (replSet) { 30 try { 31 const { getDb } = await import("../client/connection.ts"); 32 const db = getDb(); 33 await db.dropDatabase(); 34 } catch { 35 // Ignore if not connected 36 } 37 } 38 await disconnect(); 39}); 40 41Deno.test.afterAll(async () => { 42 if (replSet) { 43 await replSet.stop(); 44 replSet = null; 45 } 46}); 47 48// Test schemas 49const userSchema = z.object({ 50 name: z.string().min(1), 51 email: z.string().email(), 52 balance: z.number().nonnegative().default(0), 53}); 54 55const orderSchema = z.object({ 56 userId: z.string(), 57 amount: z.number().positive(), 58 status: z.enum(["pending", "completed", "failed"]).default("pending"), 59}); 60 61Deno.test({ 62 name: "Transactions: withTransaction - should commit successful operations", 63 async fn() { 64 const uri = await setupTestReplSet(); 65 await connect(uri, "test_db"); 66 67 const UserModel = new Model("users", userSchema); 68 const OrderModel = new Model("orders", orderSchema); 69 70 const result = await withTransaction(async (session) => { 71 const user = await UserModel.insertOne( 72 { name: "Alice", email: "alice@example.com", balance: 100 }, 73 { session } 74 ); 75 76 const order = await OrderModel.insertOne( 77 { userId: user.insertedId.toString(), amount: 50 }, 78 { session } 79 ); 80 81 return { userId: user.insertedId, orderId: order.insertedId }; 82 }); 83 84 assertExists(result.userId); 85 assertExists(result.orderId); 86 87 // Verify data was committed 88 const users = await UserModel.find({}); 89 const orders = await OrderModel.find({}); 90 assertEquals(users.length, 1); 91 assertEquals(orders.length, 1); 92 }, 93 sanitizeResources: false, 94 sanitizeOps: false, 95}); 96 97Deno.test({ 98 name: "Transactions: withTransaction - should abort on error", 99 async fn() { 100 const uri = await setupTestReplSet(); 101 await connect(uri, "test_db"); 102 103 const UserModel = new Model("users", userSchema); 104 105 await assertRejects( 106 async () => { 107 await withTransaction(async (session) => { 108 await UserModel.insertOne( 109 { name: "Bob", email: "bob@example.com" }, 110 { session } 111 ); 112 113 // This will fail and abort the transaction 114 throw new Error("Simulated error"); 115 }); 116 }, 117 Error, 118 "Simulated error" 119 ); 120 121 // Verify no data was committed 122 const users = await UserModel.find({}); 123 assertEquals(users.length, 0); 124 }, 125 sanitizeResources: false, 126 sanitizeOps: false, 127}); 128 129Deno.test({ 130 name: "Transactions: withTransaction - should handle multiple operations", 131 async fn() { 132 const uri = await setupTestReplSet(); 133 await connect(uri, "test_db"); 134 135 const UserModel = new Model("users", userSchema); 136 137 const result = await withTransaction(async (session) => { 138 const users = []; 139 140 for (let i = 0; i < 5; i++) { 141 const user = await UserModel.insertOne( 142 { name: `User${i}`, email: `user${i}@example.com` }, 143 { session } 144 ); 145 users.push(user.insertedId); 146 } 147 148 return users; 149 }); 150 151 assertEquals(result.length, 5); 152 153 // Verify all users were created 154 const users = await UserModel.find({}); 155 assertEquals(users.length, 5); 156 }, 157 sanitizeResources: false, 158 sanitizeOps: false, 159}); 160 161Deno.test({ 162 name: "Transactions: withTransaction - should support read and write operations", 163 async fn() { 164 const uri = await setupTestReplSet(); 165 await connect(uri, "test_db"); 166 167 const UserModel = new Model("users", userSchema); 168 169 // Insert initial user 170 const initialUser = await UserModel.insertOne({ 171 name: "Charlie", 172 email: "charlie@example.com", 173 balance: 100, 174 }); 175 176 const result = await withTransaction(async (session) => { 177 // Read 178 const user = await UserModel.findById(initialUser.insertedId, { session }); 179 assertExists(user); 180 181 // Update 182 await UserModel.updateOne( 183 { _id: initialUser.insertedId }, 184 { balance: 150 }, 185 { session } 186 ); 187 188 // Read again 189 const updatedUser = await UserModel.findById(initialUser.insertedId, { session }); 190 191 return updatedUser?.balance; 192 }); 193 194 assertEquals(result, 150); 195 }, 196 sanitizeResources: false, 197 sanitizeOps: false, 198}); 199 200Deno.test({ 201 name: "Transactions: withTransaction - should handle validation errors", 202 async fn() { 203 const uri = await setupTestReplSet(); 204 await connect(uri, "test_db"); 205 206 const UserModel = new Model("users", userSchema); 207 208 await assertRejects( 209 async () => { 210 await withTransaction(async (session) => { 211 // Valid insert 212 await UserModel.insertOne( 213 { name: "Valid", email: "valid@example.com" }, 214 { session } 215 ); 216 217 // Invalid insert (will throw ValidationError) 218 await UserModel.insertOne( 219 { name: "", email: "invalid" }, 220 { session } 221 ); 222 }); 223 }, 224 Error // ValidationError 225 ); 226 227 // Transaction should have been aborted, no data should exist 228 const users = await UserModel.find({}); 229 assertEquals(users.length, 0); 230 }, 231 sanitizeResources: false, 232 sanitizeOps: false, 233}); 234 235Deno.test({ 236 name: "Transactions: Manual session - should work with manual session management", 237 async fn() { 238 const uri = await setupTestReplSet(); 239 await connect(uri, "test_db"); 240 241 const UserModel = new Model("users", userSchema); 242 243 const session = startSession(); 244 245 try { 246 await session.withTransaction(async () => { 247 await UserModel.insertOne( 248 { name: "Dave", email: "dave@example.com" }, 249 { session } 250 ); 251 await UserModel.insertOne( 252 { name: "Eve", email: "eve@example.com" }, 253 { session } 254 ); 255 }); 256 } finally { 257 await endSession(session); 258 } 259 260 // Verify both users were created 261 const users = await UserModel.find({}); 262 assertEquals(users.length, 2); 263 }, 264 sanitizeResources: false, 265 sanitizeOps: false, 266}); 267 268Deno.test({ 269 name: "Transactions: withTransaction - should support delete operations", 270 async fn() { 271 const uri = await setupTestReplSet(); 272 await connect(uri, "test_db"); 273 274 const UserModel = new Model("users", userSchema); 275 276 // Insert initial users 277 await UserModel.insertMany([ 278 { name: "User1", email: "user1@example.com" }, 279 { name: "User2", email: "user2@example.com" }, 280 { name: "User3", email: "user3@example.com" }, 281 ]); 282 283 await withTransaction(async (session) => { 284 // Delete one user 285 await UserModel.deleteOne({ name: "User1" }, { session }); 286 287 // Delete multiple users 288 await UserModel.delete({ name: { $in: ["User2", "User3"] } }, { session }); 289 }); 290 291 // Verify all were deleted 292 const users = await UserModel.find({}); 293 assertEquals(users.length, 0); 294 }, 295 sanitizeResources: false, 296 sanitizeOps: false, 297}); 298 299Deno.test({ 300 name: "Transactions: withTransaction - should handle transaction options", 301 async fn() { 302 const uri = await setupTestReplSet(); 303 await connect(uri, "test_db"); 304 305 const UserModel = new Model("users", userSchema); 306 307 const result = await withTransaction( 308 async (session) => { 309 await UserModel.insertOne( 310 { name: "Frank", email: "frank@example.com" }, 311 { session } 312 ); 313 return "success"; 314 }, 315 { 316 readPreference: "primary", 317 readConcern: { level: "snapshot" }, 318 writeConcern: { w: "majority" }, 319 } 320 ); 321 322 assertEquals(result, "success"); 323 324 const users = await UserModel.find({}); 325 assertEquals(users.length, 1); 326 }, 327 sanitizeResources: false, 328 sanitizeOps: false, 329});