Thin MongoDB ODM built for Standard Schema
mongodb
zod
deno
1import { assertEquals, assertExists, assertRejects } from "@std/assert";
2import {
3 connect,
4 disconnect,
5 endSession,
6 Model,
7 startSession,
8 withTransaction,
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: 1,
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:
163 "Transactions: withTransaction - should support read and write operations",
164 async fn() {
165 const uri = await setupTestReplSet();
166 await connect(uri, "test_db");
167
168 const UserModel = new Model("users", userSchema);
169
170 // Insert initial user
171 const initialUser = await UserModel.insertOne({
172 name: "Charlie",
173 email: "charlie@example.com",
174 balance: 100,
175 });
176
177 const result = await withTransaction(async (session) => {
178 // Read
179 const user = await UserModel.findById(initialUser.insertedId, {
180 session,
181 });
182 assertExists(user);
183
184 // Update
185 await UserModel.updateOne(
186 { _id: initialUser.insertedId },
187 { balance: 150 },
188 { session },
189 );
190
191 // Read again
192 const updatedUser = await UserModel.findById(initialUser.insertedId, {
193 session,
194 });
195
196 return updatedUser?.balance;
197 });
198
199 assertEquals(result, 150);
200 },
201 sanitizeResources: false,
202 sanitizeOps: false,
203});
204
205Deno.test({
206 name: "Transactions: withTransaction - should handle validation errors",
207 async fn() {
208 const uri = await setupTestReplSet();
209 await connect(uri, "test_db");
210
211 const UserModel = new Model("users", userSchema);
212
213 await assertRejects(
214 async () => {
215 await withTransaction(async (session) => {
216 // Valid insert
217 await UserModel.insertOne(
218 { name: "Valid", email: "valid@example.com" },
219 { session },
220 );
221
222 // Invalid insert (will throw ValidationError)
223 await UserModel.insertOne(
224 { name: "", email: "invalid" },
225 { session },
226 );
227 });
228 },
229 Error, // ValidationError
230 );
231
232 // Transaction should have been aborted, no data should exist
233 const users = await UserModel.find({});
234 assertEquals(users.length, 0);
235 },
236 sanitizeResources: false,
237 sanitizeOps: false,
238});
239
240Deno.test({
241 name:
242 "Transactions: Manual session - should work with manual session management",
243 async fn() {
244 const uri = await setupTestReplSet();
245 await connect(uri, "test_db");
246
247 const UserModel = new Model("users", userSchema);
248
249 const session = startSession();
250
251 try {
252 await session.withTransaction(async () => {
253 await UserModel.insertOne(
254 { name: "Dave", email: "dave@example.com" },
255 { session },
256 );
257 await UserModel.insertOne(
258 { name: "Eve", email: "eve@example.com" },
259 { session },
260 );
261 });
262 } finally {
263 await endSession(session);
264 }
265
266 // Verify both users were created
267 const users = await UserModel.find({});
268 assertEquals(users.length, 2);
269 },
270 sanitizeResources: false,
271 sanitizeOps: false,
272});
273
274Deno.test({
275 name: "Transactions: withTransaction - should support delete operations",
276 async fn() {
277 const uri = await setupTestReplSet();
278 await connect(uri, "test_db");
279
280 const UserModel = new Model("users", userSchema);
281
282 // Insert initial users
283 await UserModel.insertMany([
284 { name: "User1", email: "user1@example.com" },
285 { name: "User2", email: "user2@example.com" },
286 { name: "User3", email: "user3@example.com" },
287 ]);
288
289 await withTransaction(async (session) => {
290 // Delete one user
291 await UserModel.deleteOne({ name: "User1" }, { session });
292
293 // Delete multiple users
294 await UserModel.delete({ name: { $in: ["User2", "User3"] } }, {
295 session,
296 });
297 });
298
299 // Verify all were deleted
300 const users = await UserModel.find({});
301 assertEquals(users.length, 0);
302 },
303 sanitizeResources: false,
304 sanitizeOps: false,
305});
306
307Deno.test({
308 name: "Transactions: withTransaction - should handle transaction options",
309 async fn() {
310 const uri = await setupTestReplSet();
311 await connect(uri, "test_db");
312
313 const UserModel = new Model("users", userSchema);
314
315 const result = await withTransaction(
316 async (session) => {
317 await UserModel.insertOne(
318 { name: "Frank", email: "frank@example.com" },
319 { session },
320 );
321 return "success";
322 },
323 {
324 readPreference: "primary",
325 readConcern: { level: "snapshot" },
326 writeConcern: { w: "majority" },
327 },
328 );
329
330 assertEquals(result, "success");
331
332 const users = await UserModel.find({});
333 assertEquals(users.length, 1);
334 },
335 sanitizeResources: false,
336 sanitizeOps: false,
337});