@objectql/driver-mongo 4.0.1 → 4.0.3
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.
- package/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +30 -0
- package/dist/index.d.ts +80 -5
- package/dist/index.js +221 -99
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +267 -112
- package/test/index.test.ts +64 -70
- package/test/integration.test.ts +250 -5
- package/tsconfig.tsbuildinfo +1 -1
package/test/index.test.ts
CHANGED
|
@@ -62,23 +62,16 @@ describe('MongoDriver', () => {
|
|
|
62
62
|
|
|
63
63
|
it('should find objects with query', async () => {
|
|
64
64
|
const query = {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
where: { age: { $gt: 18 } },
|
|
66
|
+
orderBy: [{ field: 'name', order: 'asc' as const }],
|
|
67
|
+
offset: 10,
|
|
68
68
|
limit: 5
|
|
69
69
|
};
|
|
70
70
|
|
|
71
71
|
await driver.find('users', query);
|
|
72
72
|
|
|
73
|
-
// Debugging what was actually called
|
|
74
|
-
// console.log('Find calls:', mockCollection.find.mock.calls);
|
|
75
|
-
|
|
76
73
|
expect(mockDb.collection).toHaveBeenCalledWith('users');
|
|
77
74
|
|
|
78
|
-
// We expect: find(filter, options)
|
|
79
|
-
// filter = { $and: [{ age: { $gt: 18 } }] }
|
|
80
|
-
// options = { limit: 5, skip: 10, sort: { name: 1 } }
|
|
81
|
-
|
|
82
75
|
expect(mockCollection.find).toHaveBeenCalledWith(
|
|
83
76
|
{ age: { $gt: 18 } },
|
|
84
77
|
expect.objectContaining({
|
|
@@ -92,7 +85,12 @@ describe('MongoDriver', () => {
|
|
|
92
85
|
|
|
93
86
|
it('should handle OR filters', async () => {
|
|
94
87
|
const query = {
|
|
95
|
-
|
|
88
|
+
where: {
|
|
89
|
+
$or: [
|
|
90
|
+
{ age: { $gt: 18 } },
|
|
91
|
+
{ role: 'admin' }
|
|
92
|
+
]
|
|
93
|
+
}
|
|
96
94
|
};
|
|
97
95
|
await driver.find('users', query);
|
|
98
96
|
|
|
@@ -109,7 +107,7 @@ describe('MongoDriver', () => {
|
|
|
109
107
|
|
|
110
108
|
it('should map "id" field to "_id" in filters', async () => {
|
|
111
109
|
const query = {
|
|
112
|
-
|
|
110
|
+
where: { id: '12345' }
|
|
113
111
|
};
|
|
114
112
|
await driver.find('users', query);
|
|
115
113
|
|
|
@@ -121,7 +119,7 @@ describe('MongoDriver', () => {
|
|
|
121
119
|
|
|
122
120
|
it('should map "id" to "_id" in sorting', async () => {
|
|
123
121
|
const query = {
|
|
124
|
-
|
|
122
|
+
orderBy: [{ field: 'id', order: 'desc' as const }]
|
|
125
123
|
};
|
|
126
124
|
await driver.find('users', query);
|
|
127
125
|
|
|
@@ -192,7 +190,7 @@ describe('MongoDriver', () => {
|
|
|
192
190
|
// Backward compatibility tests for legacy '_id' usage
|
|
193
191
|
it('should accept "_id" field in filters for backward compatibility', async () => {
|
|
194
192
|
const query = {
|
|
195
|
-
|
|
193
|
+
where: { _id: '12345' }
|
|
196
194
|
};
|
|
197
195
|
await driver.find('users', query);
|
|
198
196
|
|
|
@@ -204,7 +202,7 @@ describe('MongoDriver', () => {
|
|
|
204
202
|
|
|
205
203
|
it('should accept "_id" in sorting for backward compatibility', async () => {
|
|
206
204
|
const query = {
|
|
207
|
-
|
|
205
|
+
orderBy: [{ field: '_id', order: 'asc' as const }]
|
|
208
206
|
};
|
|
209
207
|
await driver.find('users', query);
|
|
210
208
|
|
|
@@ -237,19 +235,22 @@ describe('MongoDriver', () => {
|
|
|
237
235
|
|
|
238
236
|
it('should handle nested filter groups', async () => {
|
|
239
237
|
const query = {
|
|
240
|
-
|
|
241
|
-
[
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
238
|
+
where: {
|
|
239
|
+
$or: [
|
|
240
|
+
{
|
|
241
|
+
$and: [
|
|
242
|
+
{ status: 'completed' },
|
|
243
|
+
{ amount: { $gt: 100 } }
|
|
244
|
+
]
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
$and: [
|
|
248
|
+
{ customer: 'Alice' },
|
|
249
|
+
{ status: 'pending' }
|
|
250
|
+
]
|
|
251
|
+
}
|
|
251
252
|
]
|
|
252
|
-
|
|
253
|
+
}
|
|
253
254
|
};
|
|
254
255
|
await driver.find('orders', query);
|
|
255
256
|
|
|
@@ -271,19 +272,22 @@ describe('MongoDriver', () => {
|
|
|
271
272
|
|
|
272
273
|
it('should handle deeply nested filter groups', async () => {
|
|
273
274
|
const query = {
|
|
274
|
-
|
|
275
|
-
[
|
|
276
|
-
|
|
277
|
-
[
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
275
|
+
where: {
|
|
276
|
+
$and: [
|
|
277
|
+
{
|
|
278
|
+
$or: [
|
|
279
|
+
{
|
|
280
|
+
$and: [
|
|
281
|
+
{ age: { $gt: 22 } },
|
|
282
|
+
{ status: 'active' }
|
|
283
|
+
]
|
|
284
|
+
},
|
|
285
|
+
{ role: 'admin' }
|
|
286
|
+
]
|
|
287
|
+
},
|
|
288
|
+
{ name: { $ne: 'Bob' } }
|
|
289
|
+
]
|
|
290
|
+
}
|
|
287
291
|
};
|
|
288
292
|
await driver.find('users', query);
|
|
289
293
|
|
|
@@ -313,13 +317,17 @@ describe('MongoDriver', () => {
|
|
|
313
317
|
|
|
314
318
|
it('should handle nested groups with implicit AND', async () => {
|
|
315
319
|
const query = {
|
|
316
|
-
|
|
317
|
-
[
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
320
|
+
where: {
|
|
321
|
+
$and: [
|
|
322
|
+
{
|
|
323
|
+
$and: [
|
|
324
|
+
{ status: 'active' },
|
|
325
|
+
{ role: 'admin' }
|
|
326
|
+
]
|
|
327
|
+
},
|
|
328
|
+
{ age: { $gt: 25 } }
|
|
329
|
+
]
|
|
330
|
+
}
|
|
323
331
|
};
|
|
324
332
|
await driver.find('users', query);
|
|
325
333
|
|
|
@@ -341,14 +349,11 @@ describe('MongoDriver', () => {
|
|
|
341
349
|
const ast = {
|
|
342
350
|
object: 'users',
|
|
343
351
|
fields: ['name', 'email'],
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
field: 'status',
|
|
347
|
-
operator: '=',
|
|
348
|
-
value: 'active'
|
|
352
|
+
where: {
|
|
353
|
+
status: 'active'
|
|
349
354
|
},
|
|
350
|
-
|
|
351
|
-
|
|
355
|
+
limit: 10,
|
|
356
|
+
offset: 0
|
|
352
357
|
};
|
|
353
358
|
|
|
354
359
|
mockCollection.toArray.mockResolvedValue([
|
|
@@ -366,21 +371,10 @@ describe('MongoDriver', () => {
|
|
|
366
371
|
it('should handle complex QueryAST with AND filters', async () => {
|
|
367
372
|
const ast = {
|
|
368
373
|
object: 'users',
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
{
|
|
373
|
-
type: 'comparison' as const,
|
|
374
|
-
field: 'status',
|
|
375
|
-
operator: '=',
|
|
376
|
-
value: 'active'
|
|
377
|
-
},
|
|
378
|
-
{
|
|
379
|
-
type: 'comparison' as const,
|
|
380
|
-
field: 'age',
|
|
381
|
-
operator: '>',
|
|
382
|
-
value: 18
|
|
383
|
-
}
|
|
374
|
+
where: {
|
|
375
|
+
$and: [
|
|
376
|
+
{ status: 'active' },
|
|
377
|
+
{ age: { $gt: 18 } }
|
|
384
378
|
]
|
|
385
379
|
}
|
|
386
380
|
};
|
|
@@ -396,7 +390,7 @@ describe('MongoDriver', () => {
|
|
|
396
390
|
it('should handle QueryAST with sort', async () => {
|
|
397
391
|
const ast = {
|
|
398
392
|
object: 'users',
|
|
399
|
-
|
|
393
|
+
orderBy: [
|
|
400
394
|
{ field: 'name', order: 'asc' as const }
|
|
401
395
|
]
|
|
402
396
|
};
|
package/test/integration.test.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { MongoDriver } from '../src';
|
|
10
10
|
import { MongoClient } from 'mongodb';
|
|
11
|
-
import {
|
|
11
|
+
import { MongoMemoryReplSet } from 'mongodb-memory-server';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Integration tests for MongoDriver with real MongoDB operations.
|
|
@@ -32,18 +32,22 @@ const skipIfMongoUnavailable = () => {
|
|
|
32
32
|
describe('MongoDriver Integration Tests', () => {
|
|
33
33
|
let driver: MongoDriver;
|
|
34
34
|
let client: MongoClient;
|
|
35
|
-
let mongod:
|
|
35
|
+
let mongod: MongoMemoryReplSet;
|
|
36
36
|
let mongoUrl: string;
|
|
37
37
|
let dbName: string;
|
|
38
38
|
|
|
39
39
|
beforeAll(async () => {
|
|
40
40
|
try {
|
|
41
|
-
// Use existing MONGO_URL if provided (e.g.
|
|
42
|
-
// Otherwise start an in-memory
|
|
41
|
+
// Use existing MONGO_URL if provided (e.g. in CI services)
|
|
42
|
+
// Otherwise start an in-memory replica set
|
|
43
43
|
if (process.env.MONGO_URL) {
|
|
44
44
|
mongoUrl = process.env.MONGO_URL;
|
|
45
45
|
} else {
|
|
46
|
-
|
|
46
|
+
// Create MongoDB Memory Replica Set
|
|
47
|
+
// This is required for change streams to work
|
|
48
|
+
mongod = await MongoMemoryReplSet.create({
|
|
49
|
+
replSet: { count: 1, storageEngine: 'wiredTiger' }
|
|
50
|
+
});
|
|
47
51
|
mongoUrl = mongod.getUri();
|
|
48
52
|
}
|
|
49
53
|
|
|
@@ -716,4 +720,245 @@ describe('MongoDriver Integration Tests', () => {
|
|
|
716
720
|
expect(results.length).toBe(2);
|
|
717
721
|
});
|
|
718
722
|
});
|
|
723
|
+
|
|
724
|
+
describe('Distinct Method', () => {
|
|
725
|
+
beforeEach(async () => {
|
|
726
|
+
if (skipIfMongoUnavailable()) return;
|
|
727
|
+
// Create test data
|
|
728
|
+
await driver.create('products', { name: 'Laptop', category: 'Electronics', price: 1200 });
|
|
729
|
+
await driver.create('products', { name: 'Mouse', category: 'Electronics', price: 25 });
|
|
730
|
+
await driver.create('products', { name: 'Desk', category: 'Furniture', price: 500 });
|
|
731
|
+
await driver.create('products', { name: 'Chair', category: 'Furniture', price: 300 });
|
|
732
|
+
await driver.create('products', { name: 'Monitor', category: 'Electronics', price: 400 });
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
test('should get distinct values for a field', async () => {
|
|
736
|
+
if (skipIfMongoUnavailable()) return;
|
|
737
|
+
|
|
738
|
+
const categories = await driver.distinct('products', 'category');
|
|
739
|
+
|
|
740
|
+
expect(categories).toBeDefined();
|
|
741
|
+
expect(Array.isArray(categories)).toBe(true);
|
|
742
|
+
expect(categories.sort()).toEqual(['Electronics', 'Furniture'].sort());
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
test('should get distinct values with filters', async () => {
|
|
746
|
+
if (skipIfMongoUnavailable()) return;
|
|
747
|
+
|
|
748
|
+
const names = await driver.distinct('products', 'name', {
|
|
749
|
+
category: 'Electronics'
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
expect(names).toBeDefined();
|
|
753
|
+
expect(names.length).toBe(3);
|
|
754
|
+
expect(names).toContain('Laptop');
|
|
755
|
+
expect(names).toContain('Mouse');
|
|
756
|
+
expect(names).toContain('Monitor');
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
test('should return empty array for non-matching filters', async () => {
|
|
760
|
+
if (skipIfMongoUnavailable()) return;
|
|
761
|
+
|
|
762
|
+
const values = await driver.distinct('products', 'name', {
|
|
763
|
+
category: 'NonExistent'
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
expect(values).toEqual([]);
|
|
767
|
+
});
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
describe('FindOneAndUpdate Method', () => {
|
|
771
|
+
beforeEach(async () => {
|
|
772
|
+
if (skipIfMongoUnavailable()) return;
|
|
773
|
+
// Create test data
|
|
774
|
+
await driver.create('users', { name: 'Alice', email: 'alice@example.com', status: 'active', points: 100 });
|
|
775
|
+
await driver.create('users', { name: 'Bob', email: 'bob@example.com', status: 'inactive', points: 50 });
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
test('should find and update a document', async () => {
|
|
779
|
+
if (skipIfMongoUnavailable()) return;
|
|
780
|
+
|
|
781
|
+
const result = await driver.findOneAndUpdate(
|
|
782
|
+
'users',
|
|
783
|
+
{ email: 'alice@example.com' },
|
|
784
|
+
{ $set: { status: 'premium' } },
|
|
785
|
+
{ returnDocument: 'after' }
|
|
786
|
+
);
|
|
787
|
+
|
|
788
|
+
expect(result).toBeDefined();
|
|
789
|
+
expect(result.email).toBe('alice@example.com');
|
|
790
|
+
expect(result.status).toBe('premium');
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
test('should return document before update', async () => {
|
|
794
|
+
if (skipIfMongoUnavailable()) return;
|
|
795
|
+
|
|
796
|
+
const result = await driver.findOneAndUpdate(
|
|
797
|
+
'users',
|
|
798
|
+
{ email: 'bob@example.com' },
|
|
799
|
+
{ $set: { status: 'active' } },
|
|
800
|
+
{ returnDocument: 'before' }
|
|
801
|
+
);
|
|
802
|
+
|
|
803
|
+
expect(result).toBeDefined();
|
|
804
|
+
expect(result.status).toBe('inactive'); // Should be the old value
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
test('should increment a field atomically', async () => {
|
|
808
|
+
if (skipIfMongoUnavailable()) return;
|
|
809
|
+
|
|
810
|
+
const result = await driver.findOneAndUpdate(
|
|
811
|
+
'users',
|
|
812
|
+
{ email: 'alice@example.com' },
|
|
813
|
+
{ $inc: { points: 50 } },
|
|
814
|
+
{ returnDocument: 'after' }
|
|
815
|
+
);
|
|
816
|
+
|
|
817
|
+
expect(result).toBeDefined();
|
|
818
|
+
expect(result.points).toBe(150);
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
test('should upsert if document does not exist', async () => {
|
|
822
|
+
if (skipIfMongoUnavailable()) return;
|
|
823
|
+
|
|
824
|
+
const result = await driver.findOneAndUpdate(
|
|
825
|
+
'users',
|
|
826
|
+
{ email: 'charlie@example.com' },
|
|
827
|
+
{ $set: { name: 'Charlie', email: 'charlie@example.com', status: 'active' } },
|
|
828
|
+
{ returnDocument: 'after', upsert: true }
|
|
829
|
+
);
|
|
830
|
+
|
|
831
|
+
expect(result).toBeDefined();
|
|
832
|
+
expect(result.email).toBe('charlie@example.com');
|
|
833
|
+
expect(result.name).toBe('Charlie');
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
test('should return null if no document matches', async () => {
|
|
837
|
+
if (skipIfMongoUnavailable()) return;
|
|
838
|
+
|
|
839
|
+
const result = await driver.findOneAndUpdate(
|
|
840
|
+
'users',
|
|
841
|
+
{ email: 'nonexistent@example.com' },
|
|
842
|
+
{ $set: { status: 'active' } },
|
|
843
|
+
{ returnDocument: 'after' }
|
|
844
|
+
);
|
|
845
|
+
|
|
846
|
+
expect(result).toBeNull();
|
|
847
|
+
});
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
describe('Change Streams (Watch)', () => {
|
|
851
|
+
test('should watch for insert operations', async () => {
|
|
852
|
+
if (skipIfMongoUnavailable()) return;
|
|
853
|
+
|
|
854
|
+
const changes: any[] = [];
|
|
855
|
+
|
|
856
|
+
// Start watching
|
|
857
|
+
const streamId = await driver.watch('users', async (change) => {
|
|
858
|
+
changes.push(change);
|
|
859
|
+
}, {
|
|
860
|
+
operationTypes: ['insert']
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
expect(streamId).toBeDefined();
|
|
864
|
+
|
|
865
|
+
// Give the change stream time to initialize
|
|
866
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
867
|
+
|
|
868
|
+
// Insert a document
|
|
869
|
+
await driver.create('users', { name: 'ChangeTest', email: 'test@example.com' });
|
|
870
|
+
|
|
871
|
+
// Wait for change to be detected
|
|
872
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
873
|
+
|
|
874
|
+
// Stop watching
|
|
875
|
+
await driver.unwatchChangeStream(streamId);
|
|
876
|
+
|
|
877
|
+
expect(changes.length).toBeGreaterThan(0);
|
|
878
|
+
expect(changes[0].operationType).toBe('insert');
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
test('should watch for update operations', async () => {
|
|
882
|
+
if (skipIfMongoUnavailable()) return;
|
|
883
|
+
|
|
884
|
+
// Create initial document
|
|
885
|
+
const doc = await driver.create('users', { name: 'UpdateTest', email: 'update@example.com' });
|
|
886
|
+
|
|
887
|
+
const changes: any[] = [];
|
|
888
|
+
|
|
889
|
+
// Start watching
|
|
890
|
+
const streamId = await driver.watch('users', async (change) => {
|
|
891
|
+
changes.push(change);
|
|
892
|
+
}, {
|
|
893
|
+
operationTypes: ['update'],
|
|
894
|
+
fullDocument: 'updateLookup'
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
expect(streamId).toBeDefined();
|
|
898
|
+
|
|
899
|
+
// Give the change stream time to initialize
|
|
900
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
901
|
+
|
|
902
|
+
// Update the document
|
|
903
|
+
await driver.update('users', doc.id, { name: 'UpdatedName' });
|
|
904
|
+
|
|
905
|
+
// Wait for change to be detected
|
|
906
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
907
|
+
|
|
908
|
+
// Stop watching
|
|
909
|
+
await driver.unwatchChangeStream(streamId);
|
|
910
|
+
|
|
911
|
+
expect(changes.length).toBeGreaterThan(0);
|
|
912
|
+
expect(changes[0].operationType).toBe('update');
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
test('should return list of active change streams', async () => {
|
|
916
|
+
if (skipIfMongoUnavailable()) return;
|
|
917
|
+
|
|
918
|
+
const streamId1 = await driver.watch('users', async () => {});
|
|
919
|
+
const streamId2 = await driver.watch('products', async () => {});
|
|
920
|
+
|
|
921
|
+
const activeStreams = driver.getActiveChangeStreams();
|
|
922
|
+
|
|
923
|
+
expect(activeStreams).toContain(streamId1);
|
|
924
|
+
expect(activeStreams).toContain(streamId2);
|
|
925
|
+
expect(activeStreams.length).toBe(2);
|
|
926
|
+
|
|
927
|
+
// Cleanup
|
|
928
|
+
await driver.unwatchChangeStream(streamId1);
|
|
929
|
+
await driver.unwatchChangeStream(streamId2);
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
test('should allow multiple watchers on same collection', async () => {
|
|
933
|
+
if (skipIfMongoUnavailable()) return;
|
|
934
|
+
|
|
935
|
+
const changes1: any[] = [];
|
|
936
|
+
const changes2: any[] = [];
|
|
937
|
+
|
|
938
|
+
const streamId1 = await driver.watch('users', async (change) => {
|
|
939
|
+
changes1.push(change);
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
const streamId2 = await driver.watch('users', async (change) => {
|
|
943
|
+
changes2.push(change);
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
// Give streams time to initialize
|
|
947
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
948
|
+
|
|
949
|
+
// Create a document
|
|
950
|
+
await driver.create('users', { name: 'MultiWatch', email: 'multi@example.com' });
|
|
951
|
+
|
|
952
|
+
// Wait for changes to be detected
|
|
953
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
954
|
+
|
|
955
|
+
// Both watchers should have received the change
|
|
956
|
+
expect(changes1.length).toBeGreaterThan(0);
|
|
957
|
+
expect(changes2.length).toBeGreaterThan(0);
|
|
958
|
+
|
|
959
|
+
// Cleanup
|
|
960
|
+
await driver.unwatchChangeStream(streamId1);
|
|
961
|
+
await driver.unwatchChangeStream(streamId2);
|
|
962
|
+
});
|
|
963
|
+
});
|
|
719
964
|
});
|