@objectql/driver-mongo 1.7.0 → 1.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,583 @@
1
+ import { MongoDriver } from '../src';
2
+ import { MongoClient } from 'mongodb';
3
+ import { MongoMemoryServer } from 'mongodb-memory-server';
4
+
5
+ /**
6
+ * Integration tests for MongoDriver with real MongoDB operations.
7
+ * Uses mongodb-memory-server for isolated testing without external dependencies.
8
+ */
9
+
10
+ describe('MongoDriver Integration Tests', () => {
11
+ let driver: MongoDriver;
12
+ let client: MongoClient;
13
+ let mongod: MongoMemoryServer;
14
+ let mongoUrl: string;
15
+ let dbName: string;
16
+
17
+ beforeAll(async () => {
18
+ // Use existing MONGO_URL if provided (e.g. implementation in CI services)
19
+ // Otherwise start an in-memory instance
20
+ if (process.env.MONGO_URL) {
21
+ mongoUrl = process.env.MONGO_URL;
22
+ } else {
23
+ mongod = await MongoMemoryServer.create();
24
+ mongoUrl = mongod.getUri();
25
+ }
26
+
27
+ dbName = 'objectql_test_' + Date.now();
28
+
29
+ // ensure connection works
30
+ client = new MongoClient(mongoUrl);
31
+ await client.connect();
32
+ }, 60000); // startup can take time
33
+
34
+ afterAll(async () => {
35
+ if (client) await client.close();
36
+ if (mongod) await mongod.stop();
37
+ });
38
+
39
+ beforeEach(async () => {
40
+ driver = new MongoDriver({ url: mongoUrl, dbName: dbName });
41
+ // Wait for connection
42
+ await new Promise(resolve => setTimeout(resolve, 100));
43
+ });
44
+
45
+ afterEach(async () => {
46
+ if (driver) {
47
+ await driver.disconnect();
48
+ }
49
+
50
+ try {
51
+ // Clean up test database
52
+ // Reuse the client connected in beforeAll instead of creating a new one every time if possible,
53
+ // but for safety let's use the one we established or just use the driver?
54
+ // Driver doesn't have dropDatabase.
55
+ // Use the client we created in beforeAll
56
+ await client.db(dbName).dropDatabase();
57
+ } catch (e) {
58
+ // Ignore cleanup errors
59
+ }
60
+ });
61
+
62
+ describe('Basic CRUD Operations', () => {
63
+ test('should create a document', async () => {
64
+
65
+ const data = {
66
+ name: 'Alice',
67
+ age: 25,
68
+ email: 'alice@example.com'
69
+ };
70
+
71
+ const result = await driver.create('users', data);
72
+
73
+ expect(result).toBeDefined();
74
+ expect(result.id).toBeDefined();
75
+ expect(result.name).toBe('Alice');
76
+ expect(result.age).toBe(25);
77
+ });
78
+
79
+ test('should create document with custom _id', async () => {
80
+ const data = {
81
+ _id: 'custom-id-123',
82
+ name: 'Bob',
83
+ age: 30
84
+ };
85
+
86
+ const result = await driver.create('users', data);
87
+
88
+ expect(result.id).toBe('custom-id-123');
89
+ expect(result.name).toBe('Bob');
90
+ });
91
+
92
+ test('should find documents with filters', async () => {
93
+ // Insert test data
94
+ await driver.create('users', { name: 'Alice', age: 25, status: 'active' });
95
+ await driver.create('users', { name: 'Bob', age: 30, status: 'active' });
96
+ await driver.create('users', { name: 'Charlie', age: 20, status: 'inactive' });
97
+
98
+ const results = await driver.find('users', {
99
+ filters: [['status', '=', 'active']]
100
+ });
101
+
102
+ expect(results.length).toBe(2);
103
+ expect(results.every(r => r.status === 'active')).toBe(true);
104
+ });
105
+
106
+ test('should find documents with comparison operators', async () => {
107
+ await driver.create('users', { name: 'Alice', age: 25 });
108
+ await driver.create('users', { name: 'Bob', age: 30 });
109
+ await driver.create('users', { name: 'Charlie', age: 20 });
110
+
111
+ const results = await driver.find('users', {
112
+ filters: [['age', '>', 22]]
113
+ });
114
+
115
+ expect(results.length).toBe(2);
116
+ expect(results.every(r => r.age > 22)).toBe(true);
117
+ });
118
+
119
+ test('should find documents with OR filters', async () => {
120
+ await driver.create('users', { name: 'Alice', age: 25 });
121
+ await driver.create('users', { name: 'Bob', age: 30 });
122
+ await driver.create('users', { name: 'Charlie', age: 20 });
123
+
124
+ const results = await driver.find('users', {
125
+ filters: [
126
+ ['age', '=', 25],
127
+ 'or',
128
+ ['name', '=', 'Bob']
129
+ ]
130
+ });
131
+
132
+ expect(results.length).toBe(2);
133
+ });
134
+
135
+ test('should find documents with in filter', async () => {
136
+ await driver.create('users', { name: 'Alice', status: 'active' });
137
+ await driver.create('users', { name: 'Bob', status: 'pending' });
138
+ await driver.create('users', { name: 'Charlie', status: 'inactive' });
139
+
140
+ const results = await driver.find('users', {
141
+ filters: [['status', 'in', ['active', 'pending']]]
142
+ });
143
+
144
+ expect(results.length).toBe(2);
145
+ });
146
+
147
+ test('should find documents with contains filter', async () => {
148
+ await driver.create('users', { name: 'Alice Johnson' });
149
+ await driver.create('users', { name: 'Bob Smith' });
150
+ await driver.create('users', { name: 'Charlie Johnson' });
151
+
152
+ const results = await driver.find('users', {
153
+ filters: [['name', 'contains', 'Johnson']]
154
+ });
155
+
156
+ expect(results.length).toBe(2);
157
+ });
158
+
159
+ test('should find one document by id', async () => {
160
+ const created = await driver.create('users', { name: 'Alice', age: 25 });
161
+
162
+ const found = await driver.findOne('users', created.id);
163
+
164
+ expect(found).toBeDefined();
165
+ expect(found.name).toBe('Alice');
166
+ expect(found.age).toBe(25);
167
+ });
168
+
169
+ test('should find one document by query', async () => {
170
+ await driver.create('users', { name: 'Alice', age: 25 });
171
+ await driver.create('users', { name: 'Bob', age: 30 });
172
+
173
+ const found = await driver.findOne('users', null as any, {
174
+ filters: [['name', '=', 'Bob']]
175
+ });
176
+
177
+ expect(found).toBeDefined();
178
+ expect(found.name).toBe('Bob');
179
+ });
180
+
181
+ test('should update a document', async () => {
182
+ const created = await driver.create('users', { name: 'Alice', age: 25 });
183
+
184
+ await driver.update('users', created.id, { age: 26 });
185
+
186
+ const updated = await driver.findOne('users', created.id);
187
+ expect(updated.age).toBe(26);
188
+ expect(updated.name).toBe('Alice'); // Should not be removed
189
+ });
190
+
191
+ test('should update with atomic operators', async () => {
192
+ const created = await driver.create('users', { name: 'Alice', age: 25, score: 10 });
193
+
194
+ await driver.update('users', created.id, { $inc: { score: 5 } });
195
+
196
+ const updated = await driver.findOne('users', created.id);
197
+ expect(updated.score).toBe(15);
198
+ });
199
+
200
+ test('should delete a document', async () => {
201
+ const created = await driver.create('users', { name: 'Alice', age: 25 });
202
+
203
+ const deleteCount = await driver.delete('users', created.id);
204
+ expect(deleteCount).toBe(1);
205
+
206
+ const found = await driver.findOne('users', created.id);
207
+ expect(found).toBeNull();
208
+ });
209
+
210
+ test('should count documents', async () => {
211
+ await driver.create('users', { name: 'Alice', status: 'active' });
212
+ await driver.create('users', { name: 'Bob', status: 'active' });
213
+ await driver.create('users', { name: 'Charlie', status: 'inactive' });
214
+
215
+ const count = await driver.count('users', [['status', '=', 'active']]);
216
+ expect(count).toBe(2);
217
+ });
218
+
219
+ test('should count all documents', async () => {
220
+ await driver.create('users', { name: 'Alice' });
221
+ await driver.create('users', { name: 'Bob' });
222
+ await driver.create('users', { name: 'Charlie' });
223
+
224
+ const count = await driver.count('users', []);
225
+ expect(count).toBe(3);
226
+ });
227
+ });
228
+
229
+ describe('Bulk Operations', () => {
230
+ test('should create many documents', async () => {
231
+ const data = [
232
+ { name: 'Alice', age: 25 },
233
+ { name: 'Bob', age: 30 },
234
+ { name: 'Charlie', age: 35 }
235
+ ];
236
+
237
+ const result = await driver.createMany('users', data);
238
+
239
+ expect(result).toBeDefined();
240
+ expect(Object.keys(result).length).toBe(3);
241
+
242
+ const count = await driver.count('users', []);
243
+ expect(count).toBe(3);
244
+ });
245
+
246
+ test('should update many documents', async () => {
247
+ await driver.create('users', { name: 'Alice', status: 'pending' });
248
+ await driver.create('users', { name: 'Bob', status: 'pending' });
249
+ await driver.create('users', { name: 'Charlie', status: 'active' });
250
+
251
+ const modifiedCount = await driver.updateMany('users',
252
+ [['status', '=', 'pending']],
253
+ { status: 'active' }
254
+ );
255
+
256
+ expect(modifiedCount).toBe(2);
257
+
258
+ const results = await driver.find('users', {
259
+ filters: [['status', '=', 'active']]
260
+ });
261
+ expect(results.length).toBe(3);
262
+ });
263
+
264
+ test('should update many with atomic operators', async () => {
265
+ await driver.create('users', { name: 'Alice', score: 10, active: true });
266
+ await driver.create('users', { name: 'Bob', score: 20, active: true });
267
+ await driver.create('users', { name: 'Charlie', score: 30, active: false });
268
+
269
+ const modifiedCount = await driver.updateMany('users',
270
+ [['active', '=', true]],
271
+ { $inc: { score: 5 } }
272
+ );
273
+
274
+ expect(modifiedCount).toBe(2);
275
+
276
+ const alice = await driver.findOne('users', null as any, {
277
+ filters: [['name', '=', 'Alice']]
278
+ });
279
+ expect(alice.score).toBe(15);
280
+ });
281
+
282
+ test('should delete many documents', async () => {
283
+ await driver.create('users', { name: 'Alice', status: 'inactive' });
284
+ await driver.create('users', { name: 'Bob', status: 'inactive' });
285
+ await driver.create('users', { name: 'Charlie', status: 'active' });
286
+
287
+ const deletedCount = await driver.deleteMany('users',
288
+ [['status', '=', 'inactive']]
289
+ );
290
+
291
+ expect(deletedCount).toBe(2);
292
+
293
+ const remaining = await driver.count('users', []);
294
+ expect(remaining).toBe(1);
295
+ });
296
+
297
+ test('should handle empty bulk operations', async () => {
298
+ const result = await driver.createMany('users', []);
299
+ expect(result).toBeDefined();
300
+
301
+ const updated = await driver.updateMany('users',
302
+ [['name', '=', 'nonexistent']],
303
+ { status: 'updated' }
304
+ );
305
+ expect(updated).toBe(0);
306
+
307
+ const deleted = await driver.deleteMany('users',
308
+ [['name', '=', 'nonexistent']]
309
+ );
310
+ expect(deleted).toBe(0);
311
+ });
312
+ });
313
+
314
+ describe('Query Options', () => {
315
+ beforeEach(async () => {
316
+
317
+ // Insert ordered test data
318
+ await driver.create('products', { _id: '1', name: 'Laptop', price: 1200, category: 'electronics' });
319
+ await driver.create('products', { _id: '2', name: 'Mouse', price: 25, category: 'electronics' });
320
+ await driver.create('products', { _id: '3', name: 'Desk', price: 350, category: 'furniture' });
321
+ await driver.create('products', { _id: '4', name: 'Chair', price: 200, category: 'furniture' });
322
+ await driver.create('products', { _id: '5', name: 'Monitor', price: 400, category: 'electronics' });
323
+ });
324
+
325
+ test('should sort results ascending', async () => {
326
+ const results = await driver.find('products', {
327
+ sort: [['price', 'asc']]
328
+ });
329
+
330
+ expect(results[0].price).toBe(25);
331
+ expect(results[results.length - 1].price).toBe(1200);
332
+ });
333
+
334
+ test('should sort results descending', async () => {
335
+ const results = await driver.find('products', {
336
+ sort: [['price', 'desc']]
337
+ });
338
+
339
+ expect(results[0].price).toBe(1200);
340
+ expect(results[results.length - 1].price).toBe(25);
341
+ });
342
+
343
+ test('should limit results', async () => {
344
+ const results = await driver.find('products', {
345
+ limit: 2
346
+ });
347
+
348
+ expect(results.length).toBe(2);
349
+ });
350
+
351
+ test('should skip results', async () => {
352
+ const results = await driver.find('products', {
353
+ sort: [['_id', 'asc']],
354
+ skip: 2
355
+ });
356
+
357
+ expect(results.length).toBe(3);
358
+ expect(results[0].id).toBe('3');
359
+ });
360
+
361
+ test('should combine skip and limit for pagination', async () => {
362
+ const page1 = await driver.find('products', {
363
+ sort: [['_id', 'asc']],
364
+ skip: 0,
365
+ limit: 2
366
+ });
367
+
368
+ expect(page1.length).toBe(2);
369
+ expect(page1[0].id).toBe('1');
370
+
371
+ const page2 = await driver.find('products', {
372
+ sort: [['_id', 'asc']],
373
+ skip: 2,
374
+ limit: 2
375
+ });
376
+
377
+ expect(page2.length).toBe(2);
378
+ expect(page2[0].id).toBe('3');
379
+ });
380
+
381
+ test('should select specific fields', async () => {
382
+ const results = await driver.find('products', {
383
+ fields: ['name', 'price']
384
+ });
385
+
386
+ expect(results.length).toBeGreaterThan(0);
387
+ expect(results[0]).toHaveProperty('name');
388
+ expect(results[0]).toHaveProperty('price');
389
+ // _id is always included by MongoDB unless explicitly excluded
390
+ });
391
+
392
+ test('should combine filters, sort, skip, and limit', async () => {
393
+ const results = await driver.find('products', {
394
+ filters: [['category', '=', 'electronics']],
395
+ sort: [['price', 'desc']],
396
+ skip: 1,
397
+ limit: 1
398
+ });
399
+
400
+ expect(results.length).toBe(1);
401
+ expect(results[0].name).toBe('Monitor'); // Second most expensive electronics
402
+ });
403
+ });
404
+
405
+ describe('Aggregate Operations', () => {
406
+ beforeEach(async () => {
407
+
408
+ await driver.create('orders', { customer: 'Alice', amount: 100, status: 'completed' });
409
+ await driver.create('orders', { customer: 'Alice', amount: 200, status: 'completed' });
410
+ await driver.create('orders', { customer: 'Bob', amount: 150, status: 'completed' });
411
+ await driver.create('orders', { customer: 'Bob', amount: 50, status: 'pending' });
412
+ });
413
+
414
+ test('should execute simple aggregation pipeline', async () => {
415
+ const pipeline = [
416
+ { $match: { status: 'completed' } },
417
+ { $group: { _id: '$customer', total: { $sum: '$amount' } } }
418
+ ];
419
+
420
+ const results = await driver.aggregate('orders', pipeline);
421
+
422
+ expect(results.length).toBe(2);
423
+
424
+ const alice = results.find(r => r.id === 'Alice');
425
+ expect(alice.total).toBe(300);
426
+
427
+ const bob = results.find(r => r.id === 'Bob');
428
+ expect(bob.total).toBe(150);
429
+ });
430
+
431
+ test('should count with aggregation', async () => {
432
+ const pipeline = [
433
+ { $group: { _id: '$status', count: { $sum: 1 } } }
434
+ ];
435
+
436
+ const results = await driver.aggregate('orders', pipeline);
437
+
438
+ expect(results.length).toBe(2);
439
+
440
+ const completed = results.find(r => r.id === 'completed');
441
+ expect(completed.count).toBe(3);
442
+
443
+ const pending = results.find(r => r.id === 'pending');
444
+ expect(pending.count).toBe(1);
445
+ });
446
+
447
+ test('should calculate average with aggregation', async () => {
448
+ const pipeline = [
449
+ { $group: { _id: null, avgAmount: { $avg: '$amount' } } }
450
+ ];
451
+
452
+ const results = await driver.aggregate('orders', pipeline);
453
+
454
+ expect(results.length).toBe(1);
455
+ expect(results[0].avgAmount).toBe(125); // (100 + 200 + 150 + 50) / 4
456
+ });
457
+ });
458
+
459
+ describe('Edge Cases', () => {
460
+ test('should handle empty collection', async () => {
461
+ const results = await driver.find('empty_collection', {});
462
+ expect(results.length).toBe(0);
463
+
464
+ const count = await driver.count('empty_collection', []);
465
+ expect(count).toBe(0);
466
+ });
467
+
468
+ test('should handle null values', async () => {
469
+ await driver.create('users', { name: 'Alice', email: null, age: null });
470
+
471
+ const result = await driver.findOne('users', null as any, {
472
+ filters: [['name', '=', 'Alice']]
473
+ });
474
+
475
+ expect(result).toBeDefined();
476
+ expect(result.email).toBeNull();
477
+ expect(result.age).toBeNull();
478
+ });
479
+
480
+ test('should handle nested objects', async () => {
481
+ const data = {
482
+ name: 'Alice',
483
+ address: {
484
+ street: '123 Main St',
485
+ city: 'New York',
486
+ zip: '10001'
487
+ }
488
+ };
489
+
490
+ const created = await driver.create('users', data);
491
+
492
+ const found = await driver.findOne('users', created.id);
493
+ expect(found.address).toEqual(data.address);
494
+ });
495
+
496
+ test('should handle arrays', async () => {
497
+ const data = {
498
+ name: 'Alice',
499
+ tags: ['developer', 'designer'],
500
+ scores: [10, 20, 30]
501
+ };
502
+
503
+ const created = await driver.create('users', data);
504
+
505
+ const found = await driver.findOne('users', created.id);
506
+ expect(found.tags).toEqual(['developer', 'designer']);
507
+ expect(found.scores).toEqual([10, 20, 30]);
508
+ });
509
+
510
+ test('should return null for non-existent document', async () => {
511
+ const found = await driver.findOne('users', 'nonexistent-id');
512
+ expect(found).toBeNull();
513
+ });
514
+
515
+ test('should handle skip beyond total count', async () => {
516
+ await driver.create('users', { name: 'Alice' });
517
+
518
+ const results = await driver.find('users', {
519
+ skip: 100,
520
+ limit: 10
521
+ });
522
+
523
+ expect(results.length).toBe(0);
524
+ });
525
+
526
+ test('should handle complex filter combinations', async () => {
527
+ await driver.create('users', { name: 'Alice', age: 25, status: 'active' });
528
+ await driver.create('users', { name: 'Bob', age: 30, status: 'active' });
529
+ await driver.create('users', { name: 'Charlie', age: 20, status: 'inactive' });
530
+
531
+ const results = await driver.find('users', {
532
+ filters: [
533
+ ['age', '>', 22],
534
+ 'and',
535
+ ['status', '=', 'active']
536
+ ]
537
+ });
538
+
539
+ expect(results.length).toBe(2);
540
+ });
541
+
542
+ test('should handle nin (not in) filter', async () => {
543
+ await driver.create('users', { name: 'Alice', status: 'active' });
544
+ await driver.create('users', { name: 'Bob', status: 'inactive' });
545
+ await driver.create('users', { name: 'Charlie', status: 'pending' });
546
+
547
+ const results = await driver.find('users', {
548
+ filters: [['status', 'nin', ['inactive', 'pending']]]
549
+ });
550
+
551
+ expect(results.length).toBe(1);
552
+ expect(results[0].name).toBe('Alice');
553
+ });
554
+
555
+ test('should handle != operator', async () => {
556
+ await driver.create('users', { name: 'Alice', status: 'active' });
557
+ await driver.create('users', { name: 'Bob', status: 'inactive' });
558
+
559
+ const results = await driver.find('users', {
560
+ filters: [['status', '!=', 'inactive']]
561
+ });
562
+
563
+ expect(results.length).toBe(1);
564
+ expect(results[0].name).toBe('Alice');
565
+ });
566
+
567
+ test('should handle >= and <= operators', async () => {
568
+ await driver.create('users', { name: 'Alice', age: 25 });
569
+ await driver.create('users', { name: 'Bob', age: 30 });
570
+ await driver.create('users', { name: 'Charlie', age: 35 });
571
+
572
+ const results = await driver.find('users', {
573
+ filters: [
574
+ ['age', '>=', 25],
575
+ 'and',
576
+ ['age', '<=', 30]
577
+ ]
578
+ });
579
+
580
+ expect(results.length).toBe(2);
581
+ });
582
+ });
583
+ });