@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.
@@ -62,23 +62,16 @@ describe('MongoDriver', () => {
62
62
 
63
63
  it('should find objects with query', async () => {
64
64
  const query = {
65
- filters: [['age', '>', 18]],
66
- sort: [['name', 'asc']],
67
- skip: 10,
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
- filters: [['age', '>', 18], 'or', ['role', '=', 'admin']]
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
- filters: [['id', '=', '12345']]
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
- sort: [['id', 'desc']]
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
- filters: [['_id', '=', '12345']]
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
- sort: [['_id', 'asc']]
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
- filters: [
241
- [
242
- ['status', '=', 'completed'],
243
- 'and',
244
- ['amount', '>', 100]
245
- ],
246
- 'or',
247
- [
248
- ['customer', '=', 'Alice'],
249
- 'and',
250
- ['status', '=', 'pending']
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
- filters: [
275
- [
276
- [
277
- ['age', '>', 22],
278
- 'and',
279
- ['status', '=', 'active']
280
- ],
281
- 'or',
282
- ['role', '=', 'admin']
283
- ],
284
- 'and',
285
- ['name', '!=', 'Bob']
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
- filters: [
317
- [
318
- ['status', '=', 'active'],
319
- ['role', '=', 'admin']
320
- ],
321
- ['age', '>', 25]
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
- filters: {
345
- type: 'comparison' as const,
346
- field: 'status',
347
- operator: '=',
348
- value: 'active'
352
+ where: {
353
+ status: 'active'
349
354
  },
350
- top: 10,
351
- skip: 0
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
- filters: {
370
- type: 'and' as const,
371
- children: [
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
- sort: [
393
+ orderBy: [
400
394
  { field: 'name', order: 'asc' as const }
401
395
  ]
402
396
  };
@@ -8,7 +8,7 @@
8
8
 
9
9
  import { MongoDriver } from '../src';
10
10
  import { MongoClient } from 'mongodb';
11
- import { MongoMemoryServer } from 'mongodb-memory-server';
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: MongoMemoryServer;
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. implementation in CI services)
42
- // Otherwise start an in-memory instance
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
- mongod = await MongoMemoryServer.create();
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
  });