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});