@objectql/driver-mongo 3.0.0 → 4.0.0

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,322 @@
1
+ /**
2
+ * ObjectQL
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
+ import { MongoDriver } from '../src';
10
+ import { MongoClient } from 'mongodb';
11
+
12
+ // Mock data
13
+ const products = [
14
+ { _id: '1', name: 'Laptop', price: 1200, category: 'Electronics' },
15
+ { _id: '2', name: 'Mouse', price: 25, category: 'Electronics' },
16
+ { _id: '3', name: 'Desk', price: 350, category: 'Furniture' },
17
+ { _id: '4', name: 'Chair', price: 200, category: 'Furniture' },
18
+ { _id: '5', name: 'Monitor', price: 400, category: 'Electronics' }
19
+ ];
20
+
21
+ const mockCollection = {
22
+ find: jest.fn().mockReturnThis(),
23
+ sort: jest.fn().mockReturnThis(),
24
+ skip: jest.fn().mockReturnThis(),
25
+ limit: jest.fn().mockReturnThis(),
26
+ toArray: jest.fn().mockResolvedValue([]),
27
+ findOne: jest.fn().mockResolvedValue(null),
28
+ insertOne: jest.fn().mockResolvedValue({ insertedId: '123' }),
29
+ updateOne: jest.fn().mockResolvedValue({ modifiedCount: 1 }),
30
+ deleteOne: jest.fn().mockResolvedValue({ deletedCount: 1 }),
31
+ countDocuments: jest.fn().mockResolvedValue(0)
32
+ };
33
+
34
+ const mockDb = {
35
+ collection: jest.fn().mockReturnValue(mockCollection),
36
+ admin: jest.fn().mockReturnValue({
37
+ ping: jest.fn().mockResolvedValue({})
38
+ })
39
+ };
40
+
41
+ const mockClient = {
42
+ connect: jest.fn().mockResolvedValue(undefined),
43
+ db: jest.fn().mockReturnValue(mockDb),
44
+ close: jest.fn().mockResolvedValue(undefined)
45
+ };
46
+
47
+ jest.mock('mongodb', () => {
48
+ return {
49
+ MongoClient: jest.fn().mockImplementation(() => mockClient),
50
+ ObjectId: jest.fn(id => id)
51
+ };
52
+ });
53
+
54
+ /**
55
+ * QueryAST format tests
56
+ *
57
+ * Tests the driver's compatibility with @objectstack/spec QueryAST format
58
+ * which uses:
59
+ * - 'top' instead of 'limit'
60
+ * - 'aggregations' instead of 'aggregate'
61
+ * - sort as array of {field, order} objects
62
+ */
63
+ describe('MongoDriver (QueryAST Format)', () => {
64
+ let driver: MongoDriver;
65
+
66
+ beforeEach(async () => {
67
+ driver = new MongoDriver({ url: 'mongodb://localhost:27017', dbName: 'test' });
68
+ await new Promise(process.nextTick);
69
+
70
+ // Reset mocks
71
+ jest.clearAllMocks();
72
+ });
73
+
74
+ afterEach(async () => {
75
+ // Don't actually disconnect since it's mocked
76
+ });
77
+
78
+ describe('Driver Metadata', () => {
79
+ it('should expose driver metadata for ObjectStack compatibility', () => {
80
+ expect(driver.name).toBe('MongoDriver');
81
+ expect(driver.version).toBeDefined();
82
+ expect(driver.supports).toBeDefined();
83
+ expect(driver.supports.transactions).toBe(true);
84
+ expect(driver.supports.joins).toBe(false);
85
+ expect(driver.supports.fullTextSearch).toBe(true);
86
+ expect(driver.supports.jsonFields).toBe(true);
87
+ expect(driver.supports.arrayFields).toBe(true);
88
+ });
89
+ });
90
+
91
+ describe('Lifecycle Methods', () => {
92
+ it('should support connect method', async () => {
93
+ const testDriver = new MongoDriver({ url: 'mongodb://localhost:27017', dbName: 'test2' });
94
+ await expect(testDriver.connect()).resolves.toBeUndefined();
95
+ });
96
+
97
+ it('should support checkHealth method', async () => {
98
+ const healthy = await driver.checkHealth();
99
+ expect(healthy).toBe(true);
100
+ expect(mockDb.admin).toHaveBeenCalled();
101
+ });
102
+
103
+ it('should support disconnect method', async () => {
104
+ await expect(driver.disconnect()).resolves.toBeUndefined();
105
+ expect(mockClient.close).toHaveBeenCalled();
106
+ });
107
+ });
108
+
109
+ describe('QueryAST Format Support', () => {
110
+ it('should support QueryAST with "top" instead of "limit"', async () => {
111
+ mockCollection.toArray.mockResolvedValueOnce([
112
+ { _id: '2', name: 'Mouse', price: 25 },
113
+ { _id: '4', name: 'Chair', price: 200 }
114
+ ]);
115
+
116
+ const query = {
117
+ fields: ['name', 'price'],
118
+ top: 2,
119
+ sort: [{ field: 'price', order: 'asc' as const }]
120
+ };
121
+ const results = await driver.find('products', query);
122
+
123
+ expect(mockCollection.find).toHaveBeenCalledWith(
124
+ {},
125
+ expect.objectContaining({
126
+ limit: 2,
127
+ sort: { price: 1 },
128
+ projection: { _id: 0, name: 1, price: 1 }
129
+ })
130
+ );
131
+ expect(results.length).toBe(2);
132
+ expect(results[0].id).toBe('2');
133
+ expect(results[0].name).toBe('Mouse');
134
+ });
135
+
136
+ it('should support QueryAST sort format with object notation', async () => {
137
+ mockCollection.toArray.mockResolvedValueOnce(products);
138
+
139
+ const query = {
140
+ fields: ['name', 'category', 'price'],
141
+ sort: [
142
+ { field: 'category', order: 'asc' as const },
143
+ { field: 'price', order: 'desc' as const }
144
+ ]
145
+ };
146
+ await driver.find('products', query);
147
+
148
+ expect(mockCollection.find).toHaveBeenCalledWith(
149
+ {},
150
+ expect.objectContaining({
151
+ sort: { category: 1, price: -1 }
152
+ })
153
+ );
154
+ });
155
+
156
+ it('should support QueryAST with filters and pagination', async () => {
157
+ mockCollection.toArray.mockResolvedValueOnce([
158
+ { _id: '5', name: 'Monitor', price: 400 }
159
+ ]);
160
+
161
+ const query = {
162
+ filters: [['category', '=', 'Electronics']],
163
+ skip: 1,
164
+ top: 1,
165
+ sort: [{ field: 'price', order: 'asc' as const }]
166
+ };
167
+ await driver.find('products', query);
168
+
169
+ expect(mockCollection.find).toHaveBeenCalledWith(
170
+ { category: { $eq: 'Electronics' } },
171
+ expect.objectContaining({
172
+ skip: 1,
173
+ limit: 1,
174
+ sort: { price: 1 }
175
+ })
176
+ );
177
+ });
178
+
179
+ it('should support count with QueryAST format', async () => {
180
+ mockCollection.countDocuments.mockResolvedValueOnce(3);
181
+
182
+ const query = {
183
+ filters: [['price', '>', 300]]
184
+ };
185
+ const count = await driver.count('products', query);
186
+
187
+ expect(mockCollection.countDocuments).toHaveBeenCalledWith(
188
+ { price: { $gt: 300 } }
189
+ );
190
+ expect(count).toBe(3);
191
+ });
192
+ });
193
+
194
+ describe('Backward Compatibility', () => {
195
+ it('should still support legacy UnifiedQuery format with "limit"', async () => {
196
+ mockCollection.toArray.mockResolvedValueOnce([
197
+ { _id: '2', name: 'Mouse' },
198
+ { _id: '4', name: 'Chair' }
199
+ ]);
200
+
201
+ const query = {
202
+ fields: ['name'],
203
+ limit: 2,
204
+ sort: [['price', 'asc']]
205
+ };
206
+ await driver.find('products', query);
207
+
208
+ expect(mockCollection.find).toHaveBeenCalledWith(
209
+ {},
210
+ expect.objectContaining({
211
+ limit: 2,
212
+ sort: { price: 1 }
213
+ })
214
+ );
215
+ });
216
+
217
+ it('should support legacy sort format with arrays', async () => {
218
+ mockCollection.toArray.mockResolvedValueOnce(products);
219
+
220
+ const query = {
221
+ fields: ['name'],
222
+ sort: [['price', 'desc']],
223
+ limit: 3
224
+ };
225
+ await driver.find('products', query);
226
+
227
+ expect(mockCollection.find).toHaveBeenCalledWith(
228
+ {},
229
+ expect.objectContaining({
230
+ limit: 3,
231
+ sort: { price: -1 }
232
+ })
233
+ );
234
+ });
235
+ });
236
+
237
+ describe('Mixed Format Support', () => {
238
+ it('should handle query with both top and skip', async () => {
239
+ mockCollection.toArray.mockResolvedValueOnce(products.slice(2, 5));
240
+
241
+ const query = {
242
+ top: 3,
243
+ skip: 2,
244
+ sort: [{ field: 'name', order: 'asc' as const }]
245
+ };
246
+ await driver.find('products', query);
247
+
248
+ expect(mockCollection.find).toHaveBeenCalledWith(
249
+ {},
250
+ expect.objectContaining({
251
+ skip: 2,
252
+ limit: 3,
253
+ sort: { name: 1 }
254
+ })
255
+ );
256
+ });
257
+
258
+ it('should support filtering with object sort format', async () => {
259
+ mockCollection.toArray.mockResolvedValueOnce([
260
+ { _id: '1', name: 'Laptop' },
261
+ { _id: '5', name: 'Monitor' }
262
+ ]);
263
+
264
+ const query = {
265
+ filters: [['category', '=', 'Electronics']],
266
+ sort: [{ field: 'price', order: 'desc' as const }],
267
+ top: 2
268
+ };
269
+ await driver.find('products', query);
270
+
271
+ expect(mockCollection.find).toHaveBeenCalledWith(
272
+ { category: { $eq: 'Electronics' } },
273
+ expect.objectContaining({
274
+ limit: 2,
275
+ sort: { price: -1 }
276
+ })
277
+ );
278
+ });
279
+ });
280
+
281
+ describe('Field Mapping', () => {
282
+ it('should support querying with id field in QueryAST format', async () => {
283
+ mockCollection.toArray.mockResolvedValueOnce([
284
+ { _id: '1', name: 'Laptop' }
285
+ ]);
286
+
287
+ const query = {
288
+ filters: [['id', '=', '1']],
289
+ fields: ['id', 'name']
290
+ };
291
+ const results = await driver.find('products', query);
292
+
293
+ expect(mockCollection.find).toHaveBeenCalledWith(
294
+ { _id: '1' },
295
+ expect.objectContaining({
296
+ projection: { _id: 1, name: 1 }
297
+ })
298
+ );
299
+ });
300
+
301
+ it('should support sorting by id field', async () => {
302
+ mockCollection.toArray.mockResolvedValueOnce([
303
+ { _id: '5', name: 'Monitor' },
304
+ { _id: '4', name: 'Chair' }
305
+ ]);
306
+
307
+ const query = {
308
+ sort: [{ field: 'id', order: 'desc' as const }],
309
+ top: 2
310
+ };
311
+ await driver.find('products', query);
312
+
313
+ expect(mockCollection.find).toHaveBeenCalledWith(
314
+ {},
315
+ expect.objectContaining({
316
+ limit: 2,
317
+ sort: { _id: -1 }
318
+ })
319
+ );
320
+ });
321
+ });
322
+ });