@objectql/driver-mongo 4.0.2 → 4.0.4

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.
@@ -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
 
@@ -295,12 +299,12 @@ describe('MongoDriver Integration Tests', () => {
295
299
  await driver.create('users', { name: 'Bob', status: 'pending' });
296
300
  await driver.create('users', { name: 'Charlie', status: 'active' });
297
301
 
298
- const modifiedCount = await driver.updateMany('users',
302
+ const result = await driver.updateMany('users',
299
303
  [['status', '=', 'pending']],
300
304
  { status: 'active' }
301
305
  );
302
306
 
303
- expect(modifiedCount).toBe(2);
307
+ expect(result.modifiedCount).toBe(2);
304
308
 
305
309
  const results = await driver.find('users', {
306
310
  filters: [['status', '=', 'active']]
@@ -314,12 +318,12 @@ describe('MongoDriver Integration Tests', () => {
314
318
  await driver.create('users', { name: 'Bob', score: 20, active: true });
315
319
  await driver.create('users', { name: 'Charlie', score: 30, active: false });
316
320
 
317
- const modifiedCount = await driver.updateMany('users',
321
+ const result = await driver.updateMany('users',
318
322
  [['active', '=', true]],
319
323
  { $inc: { score: 5 } }
320
324
  );
321
325
 
322
- expect(modifiedCount).toBe(2);
326
+ expect(result.modifiedCount).toBe(2);
323
327
 
324
328
  const alice = await driver.findOne('users', null as any, {
325
329
  filters: [['name', '=', 'Alice']]
@@ -333,11 +337,11 @@ describe('MongoDriver Integration Tests', () => {
333
337
  await driver.create('users', { name: 'Bob', status: 'inactive' });
334
338
  await driver.create('users', { name: 'Charlie', status: 'active' });
335
339
 
336
- const deletedCount = await driver.deleteMany('users',
340
+ const result = await driver.deleteMany('users',
337
341
  [['status', '=', 'inactive']]
338
342
  );
339
343
 
340
- expect(deletedCount).toBe(2);
344
+ expect(result.deletedCount).toBe(2);
341
345
 
342
346
  const remaining = await driver.count('users', []);
343
347
  expect(remaining).toBe(1);
@@ -348,16 +352,16 @@ describe('MongoDriver Integration Tests', () => {
348
352
  const result = await driver.createMany('users', []);
349
353
  expect(result).toBeDefined();
350
354
 
351
- const updated = await driver.updateMany('users',
355
+ const updateResult = await driver.updateMany('users',
352
356
  [['name', '=', 'nonexistent']],
353
357
  { status: 'updated' }
354
358
  );
355
- expect(updated).toBe(0);
359
+ expect(updateResult.modifiedCount).toBe(0);
356
360
 
357
- const deleted = await driver.deleteMany('users',
361
+ const deleteResult = await driver.deleteMany('users',
358
362
  [['name', '=', 'nonexistent']]
359
363
  );
360
- expect(deleted).toBe(0);
364
+ expect(deleteResult.deletedCount).toBe(0);
361
365
  });
362
366
  });
363
367
 
@@ -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
  });
@@ -0,0 +1,73 @@
1
+ /**
2
+ * ObjectQL MongoDB Driver TCK Tests
3
+ * Copyright (c) 2026-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ /**
10
+ * MongoDB Driver TCK (Technology Compatibility Kit) Tests
11
+ *
12
+ * This test suite verifies that the MongoDB driver passes all TCK requirements.
13
+ */
14
+
15
+ import { runDriverTCK } from '@objectql/driver-tck';
16
+ import { MongoDriver } from '../src';
17
+ import { MongoMemoryReplSet } from 'mongodb-memory-server';
18
+
19
+ describe('MongoDriver TCK Compliance', () => {
20
+ let mongoServer: MongoMemoryReplSet;
21
+ let driver: MongoDriver;
22
+
23
+ beforeAll(async () => {
24
+ // Start MongoDB Memory Server with replica set (required for transactions)
25
+ mongoServer = await MongoMemoryReplSet.create({
26
+ replSet: { count: 1, storageEngine: 'wiredTiger' }
27
+ });
28
+ }, 120000);
29
+
30
+ afterAll(async () => {
31
+ if (driver) {
32
+ await driver.disconnect();
33
+ }
34
+ if (mongoServer) {
35
+ await mongoServer.stop();
36
+ }
37
+ }, 60000);
38
+
39
+ runDriverTCK(
40
+ () => {
41
+ const uri = mongoServer.getUri();
42
+ driver = new MongoDriver({
43
+ url: uri,
44
+ dbName: 'tck_test'
45
+ });
46
+ return driver;
47
+ },
48
+ {
49
+ // MongoDB supports most features
50
+ skip: {
51
+ // No features to skip - MongoDB now supports transactions
52
+ },
53
+ timeout: 30000,
54
+ hooks: {
55
+ beforeEach: async () => {
56
+ // Wait for driver to connect
57
+ await driver['connected'];
58
+
59
+ // Clear all collections
60
+ if (driver && driver['db']) {
61
+ const collections = await driver['db'].listCollections().toArray();
62
+ for (const collection of collections) {
63
+ await driver['db'].collection(collection.name).deleteMany({});
64
+ }
65
+ }
66
+ },
67
+ afterEach: async () => {
68
+ // Cleanup handled in beforeEach
69
+ }
70
+ }
71
+ }
72
+ );
73
+ });