Thin MongoDB ODM built for Standard Schema
mongodb zod deno

transactions v0

knotbin.com 413c7a59 95663b8a

verified
+3
.vscode/settings.json
···
+
{
+
"git.enabled": false
+
}
+70 -30
PRODUCTION_READINESS_ASSESSMENT.md
···
## ❌ Critical Missing Features for Production
-
### 1. **Transactions** 🔴 CRITICAL
-
**Status:** Not implemented
-
-
**Impact:** Cannot perform multi-document atomic operations
-
-
**Mongoose Equivalent:**
-
```javascript
-
const session = await mongoose.startSession();
-
session.startTransaction();
-
try {
-
await UserModel.updateOne({...}, {...}, { session });
-
await OrderModel.create([...], { session });
-
await session.commitTransaction();
-
} catch (error) {
-
await session.abortTransaction();
-
}
-
```
-
-
**Required for:**
-
- Financial operations
-
- Multi-collection updates
-
- Data consistency guarantees
-
- Rollback capabilities
+
### 1. **Transactions** ✅ IMPLEMENTED
+
**Status:** ✅ **FULLY IMPLEMENTED** - Complete transaction support with MongoDB driver
+
+
**Current Features:**
+
- ✅ `withTransaction()` helper for automatic transaction management
+
- ✅ `startSession()` and `endSession()` for manual session management
+
- ✅ All Model methods accept `session` option
+
- ✅ Automatic commit on success, abort on error
+
- ✅ Support for TransactionOptions (read/write concern, etc.)
+
- ✅ Clean API matching MongoDB best practices
+
- ✅ Comprehensive documentation and examples
+
+
**Nozzle API:**
+
```typescript
+
// Automatic transaction management
+
const result = await withTransaction(async (session) => {
+
await UserModel.insertOne({ name: "Alice" }, { session });
+
await OrderModel.insertOne({ userId: "123" }, { session });
+
return { success: true };
+
});
+
+
// Manual session management
+
const session = startSession();
+
try {
+
await session.withTransaction(async () => {
+
await UserModel.updateOne({...}, {...}, { session });
+
});
+
} finally {
+
await endSession(session);
+
}
+
```
+
+
**Supported Operations:**
+
- ✅ Insert (insertOne, insertMany)
+
- ✅ Find (find, findOne, findById)
+
- ✅ Update (update, updateOne, replaceOne)
+
- ✅ Delete (delete, deleteOne)
+
- ✅ Aggregate
+
- ✅ Count
+
+
**Requirements:**
+
- Requires MongoDB 4.0+ with Replica Set or MongoDB 4.2+ with Sharded Cluster
+
- All operations must pass the session parameter
---
···
| Basic CRUD | ✅ | ✅ | ✅ |
| Type Safety | ✅✅ | ✅ | ✅ |
| Schema Validation | ✅ | ✅✅ | ✅ |
-
| Transactions | ❌ | ✅ | 🔴 |
+
| Transactions | ✅ | ✅ | 🔴 |
| Middleware/Hooks | ❌ | ✅ | 🔴 |
| Index Management | ✅ | ✅ | 🟡 |
| Update Validation | ✅ | ✅ | 🟡 |
···
If you want to make Nozzle production-ready:
-
**Phase 1: Critical (Must Have)**
-
1. ❌ Implement transactions
+
**Phase 1: Critical (Must Have)** ✅ **ALL COMPLETED**
+
1. ✅ **COMPLETED** - Implement transactions
2. ✅ **COMPLETED** - Add connection retry logic
3. ✅ **COMPLETED** - Improve error handling
4. ✅ **COMPLETED** - Add update validation
···
| Type Safety | 9/10 | 15% | 1.35 |
| Error Handling | 8/10 | 15% | 1.2 |
| Connection Management | 7/10 | 15% | 1.05 |
-
| Advanced Features | 2/10 | 20% | 0.4 |
+
| Advanced Features | 5/10 | 20% | 1.0 |
| Testing & Docs | 7/10 | 10% | 0.7 |
| Production Features | 5/10 | 5% | 0.25 |
-
**Overall Score: 6.55/10** (Significantly Improved - Approaching Production Ready)
+
**Overall Score: 7.15/10** (Production Ready for Most Use Cases)
**Mongoose Equivalent Score: ~8.5/10**
···
## 🆕 Recent Improvements
-
1. ✅ **Structured Error Handling Implemented** (errors.ts)
+
1. ✅ **Transaction Support Implemented** (client.ts, model.ts)
+
- `withTransaction()` helper for automatic transaction management
+
- `startSession()` and `endSession()` for manual control
+
- All Model methods accept session options
+
- Automatic commit/abort handling
+
- Support for TransactionOptions
+
- Clean API matching MongoDB best practices
+
- Comprehensive documentation with examples
+
- Works with MongoDB 4.0+ replica sets and 4.2+ sharded clusters
+
+
2. ✅ **Structured Error Handling Implemented** (errors.ts)
- Custom error class hierarchy with `NozzleError` base class
- `ValidationError` with Zod issue integration and field grouping
- `ConnectionError` with URI context
···
## 📋 Changelog
-
### Version 0.4.0 (Latest)
+
### Version 0.5.0 (Latest)
+
- ✅ **TRANSACTIONS IMPLEMENTED** - Full transaction support
+
- ✅ `withTransaction()` helper for automatic transaction management
+
- ✅ All Model methods accept session options
+
- ✅ Automatic commit/abort handling
+
- ✅ Phase 1 Critical Features: **ALL COMPLETED** 🎉
+
- Updated scores (7.15/10, up from 6.55/10)
+
- Advanced Features upgraded from 2/10 to 5/10
+
- **Production Ready** status achieved for most use cases
+
+
### Version 0.4.0
- ✅ Structured error handling implemented (custom error classes)
- ✅ `ValidationError` with field-specific error grouping
- ✅ `ConnectionError`, `ConfigurationError`, and other error types
+35 -1
README.md
···
{ key: { createdAt: 1 }, name: "created_at_idx" },
]);
+
// Transactions (Requires MongoDB Replica Set or Sharded Cluster)
+
import { withTransaction } from "@nozzle/nozzle";
+
+
// Automatic transaction management with withTransaction
+
const result = await withTransaction(async (session) => {
+
// All operations in this callback are part of the same transaction
+
const user = await UserModel.insertOne(
+
{ name: "Alice", email: "alice@example.com" },
+
{ session } // Pass session to each operation
+
);
+
+
const order = await OrderModel.insertOne(
+
{ userId: user.insertedId, total: 100 },
+
{ session }
+
);
+
+
// If any operation fails, the entire transaction is automatically aborted
+
// If callback succeeds, transaction is automatically committed
+
return { user, order };
+
});
+
+
// Manual session management (for advanced use cases)
+
import { startSession, endSession } from "@nozzle/nozzle";
+
+
const session = startSession();
+
try {
+
await session.withTransaction(async () => {
+
await UserModel.insertOne({ name: "Bob", email: "bob@example.com" }, { session });
+
await UserModel.updateOne({ name: "Alice" }, { balance: 50 }, { session });
+
});
+
} finally {
+
await endSession(session);
+
}
+
// Error Handling
import { ValidationError, ConnectionError } from "@nozzle/nozzle";
···
## 🗺️ Roadmap
### 🔴 Critical (Must Have)
-
- [ ] Transactions support
+
- [x] Transactions support
- [x] Connection retry logic
- [x] Improved error handling
- [x] Connection health checks
+76 -1
client.ts
···
-
import { type Db, type MongoClientOptions, MongoClient } from "mongodb";
+
import {
+
type Db,
+
type MongoClientOptions,
+
type ClientSession,
+
type TransactionOptions,
+
MongoClient
+
} from "mongodb";
import { ConnectionError } from "./errors.ts";
interface Connection {
···
if (connection) {
await connection.client.close();
connection = null;
+
}
+
}
+
+
/**
+
* Start a new client session for transactions
+
*
+
* Sessions must be ended when done using `endSession()`
+
*
+
* @example
+
* ```ts
+
* const session = await startSession();
+
* try {
+
* // use session
+
* } finally {
+
* await endSession(session);
+
* }
+
* ```
+
*/
+
export function startSession(): ClientSession {
+
if (!connection) {
+
throw new ConnectionError("MongoDB not connected. Call connect() first.");
+
}
+
return connection.client.startSession();
+
}
+
+
/**
+
* End a client session
+
*
+
* @param session - The session to end
+
*/
+
export async function endSession(session: ClientSession): Promise<void> {
+
await session.endSession();
+
}
+
+
/**
+
* Execute a function within a transaction
+
*
+
* Automatically handles session creation, transaction start/commit/abort, and cleanup.
+
* If the callback throws an error, the transaction is automatically aborted.
+
*
+
* @param callback - Async function to execute within the transaction. Receives the session as parameter.
+
* @param options - Optional transaction options (read/write concern, etc.)
+
* @returns The result from the callback function
+
*
+
* @example
+
* ```ts
+
* const result = await withTransaction(async (session) => {
+
* await UserModel.insertOne({ name: "Alice" }, { session });
+
* await OrderModel.insertOne({ userId: "123", total: 100 }, { session });
+
* return { success: true };
+
* });
+
* ```
+
*/
+
export async function withTransaction<T>(
+
callback: (session: ClientSession) => Promise<T>,
+
options?: TransactionOptions
+
): Promise<T> {
+
const session = await startSession();
+
+
try {
+
let result: T;
+
+
await session.withTransaction(async () => {
+
result = await callback(session);
+
}, options);
+
+
return result!;
+
} finally {
+
await endSession(session);
}
}
examples/.github/workflows/publish.yml

This is a binary file and will not be displayed.

-81
examples/user.ts
···
-
import { z } from "@zod/zod";
-
import { ObjectId } from "mongodb";
-
import {
-
connect,
-
disconnect,
-
type InferModel,
-
type Input,
-
Model,
-
} from "../mod.ts";
-
-
// 1. Define your schema using Zod
-
const userSchema = z.object({
-
name: z.string(),
-
email: z.email(),
-
age: z.number().int().positive().optional(),
-
createdAt: z.date().default(() => new Date()),
-
});
-
-
// Infer the TypeScript type from the Zod schema
-
type User = InferModel<typeof userSchema>;
-
type UserInsert = Input<typeof userSchema>;
-
-
async function runExample() {
-
try {
-
// 3. Connect to MongoDB
-
await connect("mongodb://localhost:27017", "nozzle_example");
-
console.log("Connected to MongoDB");
-
-
// 2. Create a Model for your collection
-
const UserModel = new Model("users", userSchema);
-
-
// Clean up previous data
-
await UserModel.delete({});
-
-
// 4. Insert a new document
-
const newUser: UserInsert = {
-
name: "Alice Smith",
-
email: "alice@example.com",
-
age: 30,
-
};
-
const insertResult = await UserModel.insertOne(newUser);
-
console.log("Inserted user:", insertResult.insertedId);
-
-
// 5. Find documents
-
const users = await UserModel.find({ name: "Alice Smith" });
-
console.log("Found users:", users);
-
-
// 6. Find one document
-
const foundUser = await UserModel.findOne({
-
_id: new ObjectId(insertResult.insertedId),
-
});
-
console.log("Found one user:", foundUser);
-
-
// 7. Update a document
-
const updateResult = await UserModel.update(
-
{ _id: new ObjectId(insertResult.insertedId) },
-
{ age: 31 },
-
);
-
console.log("Updated user count:", updateResult.modifiedCount);
-
-
const updatedUser = await UserModel.findOne({
-
_id: new ObjectId(insertResult.insertedId),
-
});
-
console.log("Updated user data:", updatedUser);
-
-
// 8. Delete documents
-
const deleteResult = await UserModel.delete({ name: "Alice Smith" });
-
console.log("Deleted user count:", deleteResult.deletedCount);
-
} catch (error) {
-
console.error("Error during example run:", error);
-
} finally {
-
// 9. Disconnect from MongoDB
-
await disconnect();
-
console.log("Disconnected from MongoDB");
-
}
-
}
-
-
// Only run the example if this is the main module
-
if (import.meta.main) {
-
runExample();
-
}
+13 -1
mod.ts
···
export { type InferModel, type Input } from "./schema.ts";
-
export { connect, disconnect, healthCheck, type ConnectOptions, type HealthCheckResult } from "./client.ts";
+
export {
+
connect,
+
disconnect,
+
healthCheck,
+
startSession,
+
endSession,
+
withTransaction,
+
type ConnectOptions,
+
type HealthCheckResult
+
} from "./client.ts";
export { Model } from "./model.ts";
export {
NozzleError,
···
OperationError,
AsyncValidationError,
} from "./errors.ts";
+
+
// Re-export MongoDB types that users might need
+
export type { ClientSession, TransactionOptions } from "mongodb";
+67 -18
model.ts
···
IndexSpecification,
InsertManyResult,
InsertOneResult,
+
InsertOneOptions,
+
FindOptions,
+
UpdateOptions,
+
ReplaceOptions,
+
DeleteOptions,
+
CountDocumentsOptions,
+
AggregateOptions,
ListIndexesOptions,
OptionalUnlessRequiredId,
UpdateResult,
WithId,
+
BulkWriteOptions,
} from "mongodb";
import { ObjectId } from "mongodb";
import { getDb } from "./client.ts";
···
this.schema = schema;
}
-
async insertOne(data: Input<T>): Promise<InsertOneResult<Infer<T>>> {
+
async insertOne(
+
data: Input<T>,
+
options?: InsertOneOptions
+
): Promise<InsertOneResult<Infer<T>>> {
const validatedData = parse(this.schema, data);
return await this.collection.insertOne(
validatedData as OptionalUnlessRequiredId<Infer<T>>,
+
options
);
}
-
async insertMany(data: Input<T>[]): Promise<InsertManyResult<Infer<T>>> {
+
async insertMany(
+
data: Input<T>[],
+
options?: BulkWriteOptions
+
): Promise<InsertManyResult<Infer<T>>> {
const validatedData = data.map((item) => parse(this.schema, item));
return await this.collection.insertMany(
validatedData as OptionalUnlessRequiredId<Infer<T>>[],
+
options
);
}
-
async find(query: Filter<Infer<T>>): Promise<(WithId<Infer<T>>)[]> {
-
return await this.collection.find(query).toArray();
+
async find(
+
query: Filter<Infer<T>>,
+
options?: FindOptions
+
): Promise<(WithId<Infer<T>>)[]> {
+
return await this.collection.find(query, options).toArray();
}
-
async findOne(query: Filter<Infer<T>>): Promise<WithId<Infer<T>> | null> {
-
return await this.collection.findOne(query);
+
async findOne(
+
query: Filter<Infer<T>>,
+
options?: FindOptions
+
): Promise<WithId<Infer<T>> | null> {
+
return await this.collection.findOne(query, options);
}
-
async findById(id: string | ObjectId): Promise<WithId<Infer<T>> | null> {
+
async findById(
+
id: string | ObjectId,
+
options?: FindOptions
+
): Promise<WithId<Infer<T>> | null> {
const objectId = typeof id === "string" ? new ObjectId(id) : id;
-
return await this.findOne({ _id: objectId } as Filter<Infer<T>>);
+
return await this.findOne({ _id: objectId } as Filter<Infer<T>>, options);
}
async update(
query: Filter<Infer<T>>,
data: Partial<z.infer<T>>,
+
options?: UpdateOptions
): Promise<UpdateResult<Infer<T>>> {
const validatedData = parsePartial(this.schema, data);
-
return await this.collection.updateMany(query, { $set: validatedData as Partial<Infer<T>> });
+
return await this.collection.updateMany(
+
query,
+
{ $set: validatedData as Partial<Infer<T>> },
+
options
+
);
}
async updateOne(
query: Filter<Infer<T>>,
data: Partial<z.infer<T>>,
+
options?: UpdateOptions
): Promise<UpdateResult<Infer<T>>> {
const validatedData = parsePartial(this.schema, data);
-
return await this.collection.updateOne(query, { $set: validatedData as Partial<Infer<T>> });
+
return await this.collection.updateOne(
+
query,
+
{ $set: validatedData as Partial<Infer<T>> },
+
options
+
);
}
async replaceOne(
query: Filter<Infer<T>>,
data: Input<T>,
+
options?: ReplaceOptions
): Promise<UpdateResult<Infer<T>>> {
const validatedData = parseReplace(this.schema, data);
// Remove _id from validatedData for replaceOne (it will use the query's _id)
···
return await this.collection.replaceOne(
query,
withoutId as Infer<T>,
+
options
);
}
-
async delete(query: Filter<Infer<T>>): Promise<DeleteResult> {
-
return await this.collection.deleteMany(query);
+
async delete(
+
query: Filter<Infer<T>>,
+
options?: DeleteOptions
+
): Promise<DeleteResult> {
+
return await this.collection.deleteMany(query, options);
}
-
async deleteOne(query: Filter<Infer<T>>): Promise<DeleteResult> {
-
return await this.collection.deleteOne(query);
+
async deleteOne(
+
query: Filter<Infer<T>>,
+
options?: DeleteOptions
+
): Promise<DeleteResult> {
+
return await this.collection.deleteOne(query, options);
}
-
async count(query: Filter<Infer<T>>): Promise<number> {
-
return await this.collection.countDocuments(query);
+
async count(
+
query: Filter<Infer<T>>,
+
options?: CountDocumentsOptions
+
): Promise<number> {
+
return await this.collection.countDocuments(query, options);
}
-
async aggregate(pipeline: Document[]): Promise<Document[]> {
-
return await this.collection.aggregate(pipeline).toArray();
+
async aggregate(
+
pipeline: Document[],
+
options?: AggregateOptions
+
): Promise<Document[]> {
+
return await this.collection.aggregate(pipeline, options).toArray();
}
// Pagination support for find
+329
tests/transactions_test.ts
···
+
import { assertEquals, assertExists, assertRejects } from "@std/assert";
+
import {
+
connect,
+
disconnect,
+
Model,
+
withTransaction,
+
startSession,
+
endSession,
+
} from "../mod.ts";
+
import { z } from "@zod/zod";
+
import { MongoMemoryReplSet } from "mongodb-memory-server-core";
+
+
let replSet: MongoMemoryReplSet | null = null;
+
+
async function setupTestReplSet() {
+
if (!replSet) {
+
replSet = await MongoMemoryReplSet.create({
+
replSet: {
+
count: 3,
+
storageEngine: 'wiredTiger' // Required for transactions
+
},
+
});
+
}
+
return replSet.getUri();
+
}
+
+
Deno.test.afterEach(async () => {
+
// Clean up database
+
if (replSet) {
+
try {
+
const { getDb } = await import("../client.ts");
+
const db = getDb();
+
await db.dropDatabase();
+
} catch {
+
// Ignore if not connected
+
}
+
}
+
await disconnect();
+
});
+
+
Deno.test.afterAll(async () => {
+
if (replSet) {
+
await replSet.stop();
+
replSet = null;
+
}
+
});
+
+
// Test schemas
+
const userSchema = z.object({
+
name: z.string().min(1),
+
email: z.string().email(),
+
balance: z.number().nonnegative().default(0),
+
});
+
+
const orderSchema = z.object({
+
userId: z.string(),
+
amount: z.number().positive(),
+
status: z.enum(["pending", "completed", "failed"]).default("pending"),
+
});
+
+
Deno.test({
+
name: "Transactions: withTransaction - should commit successful operations",
+
async fn() {
+
const uri = await setupTestReplSet();
+
await connect(uri, "test_db");
+
+
const UserModel = new Model("users", userSchema);
+
const OrderModel = new Model("orders", orderSchema);
+
+
const result = await withTransaction(async (session) => {
+
const user = await UserModel.insertOne(
+
{ name: "Alice", email: "alice@example.com", balance: 100 },
+
{ session }
+
);
+
+
const order = await OrderModel.insertOne(
+
{ userId: user.insertedId.toString(), amount: 50 },
+
{ session }
+
);
+
+
return { userId: user.insertedId, orderId: order.insertedId };
+
});
+
+
assertExists(result.userId);
+
assertExists(result.orderId);
+
+
// Verify data was committed
+
const users = await UserModel.find({});
+
const orders = await OrderModel.find({});
+
assertEquals(users.length, 1);
+
assertEquals(orders.length, 1);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Transactions: withTransaction - should abort on error",
+
async fn() {
+
const uri = await setupTestReplSet();
+
await connect(uri, "test_db");
+
+
const UserModel = new Model("users", userSchema);
+
+
await assertRejects(
+
async () => {
+
await withTransaction(async (session) => {
+
await UserModel.insertOne(
+
{ name: "Bob", email: "bob@example.com" },
+
{ session }
+
);
+
+
// This will fail and abort the transaction
+
throw new Error("Simulated error");
+
});
+
},
+
Error,
+
"Simulated error"
+
);
+
+
// Verify no data was committed
+
const users = await UserModel.find({});
+
assertEquals(users.length, 0);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Transactions: withTransaction - should handle multiple operations",
+
async fn() {
+
const uri = await setupTestReplSet();
+
await connect(uri, "test_db");
+
+
const UserModel = new Model("users", userSchema);
+
+
const result = await withTransaction(async (session) => {
+
const users = [];
+
+
for (let i = 0; i < 5; i++) {
+
const user = await UserModel.insertOne(
+
{ name: `User${i}`, email: `user${i}@example.com` },
+
{ session }
+
);
+
users.push(user.insertedId);
+
}
+
+
return users;
+
});
+
+
assertEquals(result.length, 5);
+
+
// Verify all users were created
+
const users = await UserModel.find({});
+
assertEquals(users.length, 5);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Transactions: withTransaction - should support read and write operations",
+
async fn() {
+
const uri = await setupTestReplSet();
+
await connect(uri, "test_db");
+
+
const UserModel = new Model("users", userSchema);
+
+
// Insert initial user
+
const initialUser = await UserModel.insertOne({
+
name: "Charlie",
+
email: "charlie@example.com",
+
balance: 100,
+
});
+
+
const result = await withTransaction(async (session) => {
+
// Read
+
const user = await UserModel.findById(initialUser.insertedId, { session });
+
assertExists(user);
+
+
// Update
+
await UserModel.updateOne(
+
{ _id: initialUser.insertedId },
+
{ balance: 150 },
+
{ session }
+
);
+
+
// Read again
+
const updatedUser = await UserModel.findById(initialUser.insertedId, { session });
+
+
return updatedUser?.balance;
+
});
+
+
assertEquals(result, 150);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Transactions: withTransaction - should handle validation errors",
+
async fn() {
+
const uri = await setupTestReplSet();
+
await connect(uri, "test_db");
+
+
const UserModel = new Model("users", userSchema);
+
+
await assertRejects(
+
async () => {
+
await withTransaction(async (session) => {
+
// Valid insert
+
await UserModel.insertOne(
+
{ name: "Valid", email: "valid@example.com" },
+
{ session }
+
);
+
+
// Invalid insert (will throw ValidationError)
+
await UserModel.insertOne(
+
{ name: "", email: "invalid" },
+
{ session }
+
);
+
});
+
},
+
Error // ValidationError
+
);
+
+
// Transaction should have been aborted, no data should exist
+
const users = await UserModel.find({});
+
assertEquals(users.length, 0);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Transactions: Manual session - should work with manual session management",
+
async fn() {
+
const uri = await setupTestReplSet();
+
await connect(uri, "test_db");
+
+
const UserModel = new Model("users", userSchema);
+
+
const session = startSession();
+
+
try {
+
await session.withTransaction(async () => {
+
await UserModel.insertOne(
+
{ name: "Dave", email: "dave@example.com" },
+
{ session }
+
);
+
await UserModel.insertOne(
+
{ name: "Eve", email: "eve@example.com" },
+
{ session }
+
);
+
});
+
} finally {
+
await endSession(session);
+
}
+
+
// Verify both users were created
+
const users = await UserModel.find({});
+
assertEquals(users.length, 2);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Transactions: withTransaction - should support delete operations",
+
async fn() {
+
const uri = await setupTestReplSet();
+
await connect(uri, "test_db");
+
+
const UserModel = new Model("users", userSchema);
+
+
// Insert initial users
+
await UserModel.insertMany([
+
{ name: "User1", email: "user1@example.com" },
+
{ name: "User2", email: "user2@example.com" },
+
{ name: "User3", email: "user3@example.com" },
+
]);
+
+
await withTransaction(async (session) => {
+
// Delete one user
+
await UserModel.deleteOne({ name: "User1" }, { session });
+
+
// Delete multiple users
+
await UserModel.delete({ name: { $in: ["User2", "User3"] } }, { session });
+
});
+
+
// Verify all were deleted
+
const users = await UserModel.find({});
+
assertEquals(users.length, 0);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});
+
+
Deno.test({
+
name: "Transactions: withTransaction - should handle transaction options",
+
async fn() {
+
const uri = await setupTestReplSet();
+
await connect(uri, "test_db");
+
+
const UserModel = new Model("users", userSchema);
+
+
const result = await withTransaction(
+
async (session) => {
+
await UserModel.insertOne(
+
{ name: "Frank", email: "frank@example.com" },
+
{ session }
+
);
+
return "success";
+
},
+
{
+
readPreference: "primary",
+
readConcern: { level: "snapshot" },
+
writeConcern: { w: "majority" },
+
}
+
);
+
+
assertEquals(result, "success");
+
+
const users = await UserModel.find({});
+
assertEquals(users.length, 1);
+
},
+
sanitizeResources: false,
+
sanitizeOps: false,
+
});