Thin MongoDB ODM built for Standard Schema
mongodb zod deno

transactions v0

knotbin.com 413c7a59 95663b8a

verified
+593 -132
+3
.vscode/settings.json
··· 1 + { 2 + "git.enabled": false 3 + }
+70 -30
PRODUCTION_READINESS_ASSESSMENT.md
··· 35 35 36 36 ## ❌ Critical Missing Features for Production 37 37 38 - ### 1. **Transactions** 🔴 CRITICAL 39 - **Status:** Not implemented 40 - 41 - **Impact:** Cannot perform multi-document atomic operations 42 - 43 - **Mongoose Equivalent:** 44 - ```javascript 45 - const session = await mongoose.startSession(); 46 - session.startTransaction(); 47 - try { 48 - await UserModel.updateOne({...}, {...}, { session }); 49 - await OrderModel.create([...], { session }); 50 - await session.commitTransaction(); 51 - } catch (error) { 52 - await session.abortTransaction(); 53 - } 54 - ``` 55 - 56 - **Required for:** 57 - - Financial operations 58 - - Multi-collection updates 59 - - Data consistency guarantees 60 - - Rollback capabilities 38 + ### 1. **Transactions** ✅ IMPLEMENTED 39 + **Status:** ✅ **FULLY IMPLEMENTED** - Complete transaction support with MongoDB driver 40 + 41 + **Current Features:** 42 + - ✅ `withTransaction()` helper for automatic transaction management 43 + - ✅ `startSession()` and `endSession()` for manual session management 44 + - ✅ All Model methods accept `session` option 45 + - ✅ Automatic commit on success, abort on error 46 + - ✅ Support for TransactionOptions (read/write concern, etc.) 47 + - ✅ Clean API matching MongoDB best practices 48 + - ✅ Comprehensive documentation and examples 49 + 50 + **Nozzle API:** 51 + ```typescript 52 + // Automatic transaction management 53 + const result = await withTransaction(async (session) => { 54 + await UserModel.insertOne({ name: "Alice" }, { session }); 55 + await OrderModel.insertOne({ userId: "123" }, { session }); 56 + return { success: true }; 57 + }); 58 + 59 + // Manual session management 60 + const session = startSession(); 61 + try { 62 + await session.withTransaction(async () => { 63 + await UserModel.updateOne({...}, {...}, { session }); 64 + }); 65 + } finally { 66 + await endSession(session); 67 + } 68 + ``` 69 + 70 + **Supported Operations:** 71 + - ✅ Insert (insertOne, insertMany) 72 + - ✅ Find (find, findOne, findById) 73 + - ✅ Update (update, updateOne, replaceOne) 74 + - ✅ Delete (delete, deleteOne) 75 + - ✅ Aggregate 76 + - ✅ Count 77 + 78 + **Requirements:** 79 + - Requires MongoDB 4.0+ with Replica Set or MongoDB 4.2+ with Sharded Cluster 80 + - All operations must pass the session parameter 61 81 62 82 --- 63 83 ··· 395 415 | Basic CRUD | ✅ | ✅ | ✅ | 396 416 | Type Safety | ✅✅ | ✅ | ✅ | 397 417 | Schema Validation | ✅ | ✅✅ | ✅ | 398 - | Transactions | ❌ | ✅ | 🔴 | 418 + | Transactions | ✅ | ✅ | 🔴 | 399 419 | Middleware/Hooks | ❌ | ✅ | 🔴 | 400 420 | Index Management | ✅ | ✅ | 🟡 | 401 421 | Update Validation | ✅ | ✅ | 🟡 | ··· 442 462 443 463 If you want to make Nozzle production-ready: 444 464 445 - **Phase 1: Critical (Must Have)** 446 - 1. ❌ Implement transactions 465 + **Phase 1: Critical (Must Have)** ✅ **ALL COMPLETED** 466 + 1. ✅ **COMPLETED** - Implement transactions 447 467 2. ✅ **COMPLETED** - Add connection retry logic 448 468 3. ✅ **COMPLETED** - Improve error handling 449 469 4. ✅ **COMPLETED** - Add update validation ··· 473 493 | Type Safety | 9/10 | 15% | 1.35 | 474 494 | Error Handling | 8/10 | 15% | 1.2 | 475 495 | Connection Management | 7/10 | 15% | 1.05 | 476 - | Advanced Features | 2/10 | 20% | 0.4 | 496 + | Advanced Features | 5/10 | 20% | 1.0 | 477 497 | Testing & Docs | 7/10 | 10% | 0.7 | 478 498 | Production Features | 5/10 | 5% | 0.25 | 479 499 480 - **Overall Score: 6.55/10** (Significantly Improved - Approaching Production Ready) 500 + **Overall Score: 7.15/10** (Production Ready for Most Use Cases) 481 501 482 502 **Mongoose Equivalent Score: ~8.5/10** 483 503 ··· 510 530 511 531 ## 🆕 Recent Improvements 512 532 513 - 1. ✅ **Structured Error Handling Implemented** (errors.ts) 533 + 1. ✅ **Transaction Support Implemented** (client.ts, model.ts) 534 + - `withTransaction()` helper for automatic transaction management 535 + - `startSession()` and `endSession()` for manual control 536 + - All Model methods accept session options 537 + - Automatic commit/abort handling 538 + - Support for TransactionOptions 539 + - Clean API matching MongoDB best practices 540 + - Comprehensive documentation with examples 541 + - Works with MongoDB 4.0+ replica sets and 4.2+ sharded clusters 542 + 543 + 2. ✅ **Structured Error Handling Implemented** (errors.ts) 514 544 - Custom error class hierarchy with `NozzleError` base class 515 545 - `ValidationError` with Zod issue integration and field grouping 516 546 - `ConnectionError` with URI context ··· 571 601 572 602 ## 📋 Changelog 573 603 574 - ### Version 0.4.0 (Latest) 604 + ### Version 0.5.0 (Latest) 605 + - ✅ **TRANSACTIONS IMPLEMENTED** - Full transaction support 606 + - ✅ `withTransaction()` helper for automatic transaction management 607 + - ✅ All Model methods accept session options 608 + - ✅ Automatic commit/abort handling 609 + - ✅ Phase 1 Critical Features: **ALL COMPLETED** 🎉 610 + - Updated scores (7.15/10, up from 6.55/10) 611 + - Advanced Features upgraded from 2/10 to 5/10 612 + - **Production Ready** status achieved for most use cases 613 + 614 + ### Version 0.4.0 575 615 - ✅ Structured error handling implemented (custom error classes) 576 616 - ✅ `ValidationError` with field-specific error grouping 577 617 - ✅ `ConnectionError`, `ConfigurationError`, and other error types
+35 -1
README.md
··· 202 202 { key: { createdAt: 1 }, name: "created_at_idx" }, 203 203 ]); 204 204 205 + // Transactions (Requires MongoDB Replica Set or Sharded Cluster) 206 + import { withTransaction } from "@nozzle/nozzle"; 207 + 208 + // Automatic transaction management with withTransaction 209 + const result = await withTransaction(async (session) => { 210 + // All operations in this callback are part of the same transaction 211 + const user = await UserModel.insertOne( 212 + { name: "Alice", email: "alice@example.com" }, 213 + { session } // Pass session to each operation 214 + ); 215 + 216 + const order = await OrderModel.insertOne( 217 + { userId: user.insertedId, total: 100 }, 218 + { session } 219 + ); 220 + 221 + // If any operation fails, the entire transaction is automatically aborted 222 + // If callback succeeds, transaction is automatically committed 223 + return { user, order }; 224 + }); 225 + 226 + // Manual session management (for advanced use cases) 227 + import { startSession, endSession } from "@nozzle/nozzle"; 228 + 229 + const session = startSession(); 230 + try { 231 + await session.withTransaction(async () => { 232 + await UserModel.insertOne({ name: "Bob", email: "bob@example.com" }, { session }); 233 + await UserModel.updateOne({ name: "Alice" }, { balance: 50 }, { session }); 234 + }); 235 + } finally { 236 + await endSession(session); 237 + } 238 + 205 239 // Error Handling 206 240 import { ValidationError, ConnectionError } from "@nozzle/nozzle"; 207 241 ··· 227 261 ## 🗺️ Roadmap 228 262 229 263 ### 🔴 Critical (Must Have) 230 - - [ ] Transactions support 264 + - [x] Transactions support 231 265 - [x] Connection retry logic 232 266 - [x] Improved error handling 233 267 - [x] Connection health checks
+76 -1
client.ts
··· 1 - import { type Db, type MongoClientOptions, MongoClient } from "mongodb"; 1 + import { 2 + type Db, 3 + type MongoClientOptions, 4 + type ClientSession, 5 + type TransactionOptions, 6 + MongoClient 7 + } from "mongodb"; 2 8 import { ConnectionError } from "./errors.ts"; 3 9 4 10 interface Connection { ··· 103 109 if (connection) { 104 110 await connection.client.close(); 105 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 + */ 130 + export 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 + */ 142 + export 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 + */ 165 + export 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); 106 181 } 107 182 } 108 183
examples/.github/workflows/publish.yml

This is a binary file and will not be displayed.

-81
examples/user.ts
··· 1 - import { z } from "@zod/zod"; 2 - import { ObjectId } from "mongodb"; 3 - import { 4 - connect, 5 - disconnect, 6 - type InferModel, 7 - type Input, 8 - Model, 9 - } from "../mod.ts"; 10 - 11 - // 1. Define your schema using Zod 12 - const userSchema = z.object({ 13 - name: z.string(), 14 - email: z.email(), 15 - age: z.number().int().positive().optional(), 16 - createdAt: z.date().default(() => new Date()), 17 - }); 18 - 19 - // Infer the TypeScript type from the Zod schema 20 - type User = InferModel<typeof userSchema>; 21 - type UserInsert = Input<typeof userSchema>; 22 - 23 - async function runExample() { 24 - try { 25 - // 3. Connect to MongoDB 26 - await connect("mongodb://localhost:27017", "nozzle_example"); 27 - console.log("Connected to MongoDB"); 28 - 29 - // 2. Create a Model for your collection 30 - const UserModel = new Model("users", userSchema); 31 - 32 - // Clean up previous data 33 - await UserModel.delete({}); 34 - 35 - // 4. Insert a new document 36 - const newUser: UserInsert = { 37 - name: "Alice Smith", 38 - email: "alice@example.com", 39 - age: 30, 40 - }; 41 - const insertResult = await UserModel.insertOne(newUser); 42 - console.log("Inserted user:", insertResult.insertedId); 43 - 44 - // 5. Find documents 45 - const users = await UserModel.find({ name: "Alice Smith" }); 46 - console.log("Found users:", users); 47 - 48 - // 6. Find one document 49 - const foundUser = await UserModel.findOne({ 50 - _id: new ObjectId(insertResult.insertedId), 51 - }); 52 - console.log("Found one user:", foundUser); 53 - 54 - // 7. Update a document 55 - const updateResult = await UserModel.update( 56 - { _id: new ObjectId(insertResult.insertedId) }, 57 - { age: 31 }, 58 - ); 59 - console.log("Updated user count:", updateResult.modifiedCount); 60 - 61 - const updatedUser = await UserModel.findOne({ 62 - _id: new ObjectId(insertResult.insertedId), 63 - }); 64 - console.log("Updated user data:", updatedUser); 65 - 66 - // 8. Delete documents 67 - const deleteResult = await UserModel.delete({ name: "Alice Smith" }); 68 - console.log("Deleted user count:", deleteResult.deletedCount); 69 - } catch (error) { 70 - console.error("Error during example run:", error); 71 - } finally { 72 - // 9. Disconnect from MongoDB 73 - await disconnect(); 74 - console.log("Disconnected from MongoDB"); 75 - } 76 - } 77 - 78 - // Only run the example if this is the main module 79 - if (import.meta.main) { 80 - runExample(); 81 - }
+13 -1
mod.ts
··· 1 1 export { type InferModel, type Input } from "./schema.ts"; 2 - export { connect, disconnect, healthCheck, type ConnectOptions, type HealthCheckResult } from "./client.ts"; 2 + export { 3 + connect, 4 + disconnect, 5 + healthCheck, 6 + startSession, 7 + endSession, 8 + withTransaction, 9 + type ConnectOptions, 10 + type HealthCheckResult 11 + } from "./client.ts"; 3 12 export { Model } from "./model.ts"; 4 13 export { 5 14 NozzleError, ··· 10 19 OperationError, 11 20 AsyncValidationError, 12 21 } from "./errors.ts"; 22 + 23 + // Re-export MongoDB types that users might need 24 + export type { ClientSession, TransactionOptions } from "mongodb";
+67 -18
model.ts
··· 10 10 IndexSpecification, 11 11 InsertManyResult, 12 12 InsertOneResult, 13 + InsertOneOptions, 14 + FindOptions, 15 + UpdateOptions, 16 + ReplaceOptions, 17 + DeleteOptions, 18 + CountDocumentsOptions, 19 + AggregateOptions, 13 20 ListIndexesOptions, 14 21 OptionalUnlessRequiredId, 15 22 UpdateResult, 16 23 WithId, 24 + BulkWriteOptions, 17 25 } from "mongodb"; 18 26 import { ObjectId } from "mongodb"; 19 27 import { getDb } from "./client.ts"; ··· 81 89 this.schema = schema; 82 90 } 83 91 84 - async insertOne(data: Input<T>): Promise<InsertOneResult<Infer<T>>> { 92 + async insertOne( 93 + data: Input<T>, 94 + options?: InsertOneOptions 95 + ): Promise<InsertOneResult<Infer<T>>> { 85 96 const validatedData = parse(this.schema, data); 86 97 return await this.collection.insertOne( 87 98 validatedData as OptionalUnlessRequiredId<Infer<T>>, 99 + options 88 100 ); 89 101 } 90 102 91 - async insertMany(data: Input<T>[]): Promise<InsertManyResult<Infer<T>>> { 103 + async insertMany( 104 + data: Input<T>[], 105 + options?: BulkWriteOptions 106 + ): Promise<InsertManyResult<Infer<T>>> { 92 107 const validatedData = data.map((item) => parse(this.schema, item)); 93 108 return await this.collection.insertMany( 94 109 validatedData as OptionalUnlessRequiredId<Infer<T>>[], 110 + options 95 111 ); 96 112 } 97 113 98 - async find(query: Filter<Infer<T>>): Promise<(WithId<Infer<T>>)[]> { 99 - return await this.collection.find(query).toArray(); 114 + async find( 115 + query: Filter<Infer<T>>, 116 + options?: FindOptions 117 + ): Promise<(WithId<Infer<T>>)[]> { 118 + return await this.collection.find(query, options).toArray(); 100 119 } 101 120 102 - async findOne(query: Filter<Infer<T>>): Promise<WithId<Infer<T>> | null> { 103 - return await this.collection.findOne(query); 121 + async findOne( 122 + query: Filter<Infer<T>>, 123 + options?: FindOptions 124 + ): Promise<WithId<Infer<T>> | null> { 125 + return await this.collection.findOne(query, options); 104 126 } 105 127 106 - async findById(id: string | ObjectId): Promise<WithId<Infer<T>> | null> { 128 + async findById( 129 + id: string | ObjectId, 130 + options?: FindOptions 131 + ): Promise<WithId<Infer<T>> | null> { 107 132 const objectId = typeof id === "string" ? new ObjectId(id) : id; 108 - return await this.findOne({ _id: objectId } as Filter<Infer<T>>); 133 + return await this.findOne({ _id: objectId } as Filter<Infer<T>>, options); 109 134 } 110 135 111 136 async update( 112 137 query: Filter<Infer<T>>, 113 138 data: Partial<z.infer<T>>, 139 + options?: UpdateOptions 114 140 ): Promise<UpdateResult<Infer<T>>> { 115 141 const validatedData = parsePartial(this.schema, data); 116 - return await this.collection.updateMany(query, { $set: validatedData as Partial<Infer<T>> }); 142 + return await this.collection.updateMany( 143 + query, 144 + { $set: validatedData as Partial<Infer<T>> }, 145 + options 146 + ); 117 147 } 118 148 119 149 async updateOne( 120 150 query: Filter<Infer<T>>, 121 151 data: Partial<z.infer<T>>, 152 + options?: UpdateOptions 122 153 ): Promise<UpdateResult<Infer<T>>> { 123 154 const validatedData = parsePartial(this.schema, data); 124 - return await this.collection.updateOne(query, { $set: validatedData as Partial<Infer<T>> }); 155 + return await this.collection.updateOne( 156 + query, 157 + { $set: validatedData as Partial<Infer<T>> }, 158 + options 159 + ); 125 160 } 126 161 127 162 async replaceOne( 128 163 query: Filter<Infer<T>>, 129 164 data: Input<T>, 165 + options?: ReplaceOptions 130 166 ): Promise<UpdateResult<Infer<T>>> { 131 167 const validatedData = parseReplace(this.schema, data); 132 168 // Remove _id from validatedData for replaceOne (it will use the query's _id) ··· 134 170 return await this.collection.replaceOne( 135 171 query, 136 172 withoutId as Infer<T>, 173 + options 137 174 ); 138 175 } 139 176 140 - async delete(query: Filter<Infer<T>>): Promise<DeleteResult> { 141 - return await this.collection.deleteMany(query); 177 + async delete( 178 + query: Filter<Infer<T>>, 179 + options?: DeleteOptions 180 + ): Promise<DeleteResult> { 181 + return await this.collection.deleteMany(query, options); 142 182 } 143 183 144 - async deleteOne(query: Filter<Infer<T>>): Promise<DeleteResult> { 145 - return await this.collection.deleteOne(query); 184 + async deleteOne( 185 + query: Filter<Infer<T>>, 186 + options?: DeleteOptions 187 + ): Promise<DeleteResult> { 188 + return await this.collection.deleteOne(query, options); 146 189 } 147 190 148 - async count(query: Filter<Infer<T>>): Promise<number> { 149 - return await this.collection.countDocuments(query); 191 + async count( 192 + query: Filter<Infer<T>>, 193 + options?: CountDocumentsOptions 194 + ): Promise<number> { 195 + return await this.collection.countDocuments(query, options); 150 196 } 151 197 152 - async aggregate(pipeline: Document[]): Promise<Document[]> { 153 - return await this.collection.aggregate(pipeline).toArray(); 198 + async aggregate( 199 + pipeline: Document[], 200 + options?: AggregateOptions 201 + ): Promise<Document[]> { 202 + return await this.collection.aggregate(pipeline, options).toArray(); 154 203 } 155 204 156 205 // Pagination support for find
+329
tests/transactions_test.ts
··· 1 + import { assertEquals, assertExists, assertRejects } from "@std/assert"; 2 + import { 3 + connect, 4 + disconnect, 5 + Model, 6 + withTransaction, 7 + startSession, 8 + endSession, 9 + } from "../mod.ts"; 10 + import { z } from "@zod/zod"; 11 + import { MongoMemoryReplSet } from "mongodb-memory-server-core"; 12 + 13 + let replSet: MongoMemoryReplSet | null = null; 14 + 15 + async 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 + 27 + Deno.test.afterEach(async () => { 28 + // Clean up database 29 + if (replSet) { 30 + try { 31 + const { getDb } = await import("../client.ts"); 32 + const db = getDb(); 33 + await db.dropDatabase(); 34 + } catch { 35 + // Ignore if not connected 36 + } 37 + } 38 + await disconnect(); 39 + }); 40 + 41 + Deno.test.afterAll(async () => { 42 + if (replSet) { 43 + await replSet.stop(); 44 + replSet = null; 45 + } 46 + }); 47 + 48 + // Test schemas 49 + const userSchema = z.object({ 50 + name: z.string().min(1), 51 + email: z.string().email(), 52 + balance: z.number().nonnegative().default(0), 53 + }); 54 + 55 + const orderSchema = z.object({ 56 + userId: z.string(), 57 + amount: z.number().positive(), 58 + status: z.enum(["pending", "completed", "failed"]).default("pending"), 59 + }); 60 + 61 + Deno.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 + 97 + Deno.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 + 129 + Deno.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 + 161 + Deno.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 + 200 + Deno.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 + 235 + Deno.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 + 268 + Deno.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 + 299 + Deno.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 + });