@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.
- package/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +58 -0
- package/dist/index.d.ts +118 -1
- package/dist/index.js +266 -11
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +320 -14
- package/test/index.test.ts +15 -13
- package/test/integration.test.ts +260 -15
- package/test/tck.test.ts +73 -0
- package/tsconfig.tsbuildinfo +1 -1
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
|
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
355
|
+
const updateResult = await driver.updateMany('users',
|
|
352
356
|
[['name', '=', 'nonexistent']],
|
|
353
357
|
{ status: 'updated' }
|
|
354
358
|
);
|
|
355
|
-
expect(
|
|
359
|
+
expect(updateResult.modifiedCount).toBe(0);
|
|
356
360
|
|
|
357
|
-
const
|
|
361
|
+
const deleteResult = await driver.deleteMany('users',
|
|
358
362
|
[['name', '=', 'nonexistent']]
|
|
359
363
|
);
|
|
360
|
-
expect(
|
|
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
|
});
|
package/test/tck.test.ts
ADDED
|
@@ -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
|
+
});
|