@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.
- package/CHANGELOG.md +32 -0
- package/MIGRATION.md +213 -0
- package/README.md +58 -0
- package/dist/index.d.ts +114 -2
- package/dist/index.js +293 -12
- package/dist/index.js.map +1 -1
- package/jest.config.js +8 -0
- package/package.json +3 -2
- package/src/index.ts +342 -11
- package/test/index.test.ts +258 -1
- package/test/integration.test.ts +8 -0
- package/test/queryast.test.ts +322 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/index.ts
CHANGED
|
@@ -1,7 +1,60 @@
|
|
|
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
|
+
|
|
1
9
|
import { Driver } from '@objectql/types';
|
|
10
|
+
import { DriverInterface, QueryAST, FilterNode, SortNode } from '@objectstack/spec';
|
|
2
11
|
import { MongoClient, Db, Filter, ObjectId, FindOptions } from 'mongodb';
|
|
3
12
|
|
|
4
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Command interface for executeCommand method
|
|
15
|
+
*/
|
|
16
|
+
export interface Command {
|
|
17
|
+
type: 'create' | 'update' | 'delete' | 'bulkCreate' | 'bulkUpdate' | 'bulkDelete';
|
|
18
|
+
object: string;
|
|
19
|
+
data?: any;
|
|
20
|
+
id?: string | number;
|
|
21
|
+
ids?: Array<string | number>;
|
|
22
|
+
records?: any[];
|
|
23
|
+
updates?: Array<{id: string | number, data: any}>;
|
|
24
|
+
options?: any;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Command result interface
|
|
29
|
+
*/
|
|
30
|
+
export interface CommandResult {
|
|
31
|
+
success: boolean;
|
|
32
|
+
data?: any;
|
|
33
|
+
affected: number;
|
|
34
|
+
error?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* MongoDB Driver for ObjectQL
|
|
39
|
+
*
|
|
40
|
+
* Implements both the legacy Driver interface from @objectql/types and
|
|
41
|
+
* the standard DriverInterface from @objectstack/spec for compatibility
|
|
42
|
+
* with the new kernel-based plugin system.
|
|
43
|
+
*
|
|
44
|
+
* The driver internally converts QueryAST format to MongoDB query format.
|
|
45
|
+
*/
|
|
46
|
+
export class MongoDriver implements Driver, DriverInterface {
|
|
47
|
+
// Driver metadata (ObjectStack-compatible)
|
|
48
|
+
public readonly name = 'MongoDriver';
|
|
49
|
+
public readonly version = '3.0.1';
|
|
50
|
+
public readonly supports = {
|
|
51
|
+
transactions: true,
|
|
52
|
+
joins: false,
|
|
53
|
+
fullTextSearch: true,
|
|
54
|
+
jsonFields: true,
|
|
55
|
+
arrayFields: true
|
|
56
|
+
};
|
|
57
|
+
|
|
5
58
|
private client: MongoClient;
|
|
6
59
|
private db?: Db;
|
|
7
60
|
private config: any;
|
|
@@ -10,14 +63,39 @@ export class MongoDriver implements Driver {
|
|
|
10
63
|
constructor(config: { url: string, dbName?: string }) {
|
|
11
64
|
this.config = config;
|
|
12
65
|
this.client = new MongoClient(config.url);
|
|
13
|
-
this.connected = this.
|
|
66
|
+
this.connected = this.internalConnect();
|
|
14
67
|
}
|
|
15
68
|
|
|
16
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Internal connect method used in constructor
|
|
71
|
+
*/
|
|
72
|
+
private async internalConnect() {
|
|
17
73
|
await this.client.connect();
|
|
18
74
|
this.db = this.client.db(this.config.dbName);
|
|
19
75
|
}
|
|
20
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Connect to the database (for DriverInterface compatibility)
|
|
79
|
+
* This method ensures the connection is established.
|
|
80
|
+
*/
|
|
81
|
+
async connect(): Promise<void> {
|
|
82
|
+
await this.connected;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Check database connection health
|
|
87
|
+
*/
|
|
88
|
+
async checkHealth(): Promise<boolean> {
|
|
89
|
+
try {
|
|
90
|
+
await this.connected;
|
|
91
|
+
if (!this.db) return false;
|
|
92
|
+
await this.db.admin().ping();
|
|
93
|
+
return true;
|
|
94
|
+
} catch (error) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
21
99
|
private async getCollection(objectName: string) {
|
|
22
100
|
await this.connected;
|
|
23
101
|
if (!this.db) throw new Error("Database not initialized");
|
|
@@ -162,29 +240,79 @@ export class MongoDriver implements Driver {
|
|
|
162
240
|
return mongoCondition;
|
|
163
241
|
}
|
|
164
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Normalizes query format to support both legacy UnifiedQuery and QueryAST formats.
|
|
245
|
+
* This ensures backward compatibility while supporting the new @objectstack/spec interface.
|
|
246
|
+
*
|
|
247
|
+
* QueryAST format uses 'top' for limit, while UnifiedQuery uses 'limit'.
|
|
248
|
+
* QueryAST sort is array of {field, order}, while UnifiedQuery is array of [field, order].
|
|
249
|
+
* QueryAST uses 'aggregations', while legacy uses 'aggregate'.
|
|
250
|
+
*/
|
|
251
|
+
private normalizeQuery(query: any): any {
|
|
252
|
+
if (!query) return {};
|
|
253
|
+
|
|
254
|
+
const normalized: any = { ...query };
|
|
255
|
+
|
|
256
|
+
// Normalize limit/top
|
|
257
|
+
if (normalized.top !== undefined && normalized.limit === undefined) {
|
|
258
|
+
normalized.limit = normalized.top;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Normalize aggregations/aggregate
|
|
262
|
+
if (normalized.aggregations !== undefined && normalized.aggregate === undefined) {
|
|
263
|
+
// Convert QueryAST aggregations format to legacy aggregate format
|
|
264
|
+
normalized.aggregate = normalized.aggregations.map((agg: any) => ({
|
|
265
|
+
func: agg.function || agg.func,
|
|
266
|
+
field: agg.field,
|
|
267
|
+
alias: agg.alias
|
|
268
|
+
}));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Normalize sort format
|
|
272
|
+
if (normalized.sort && Array.isArray(normalized.sort)) {
|
|
273
|
+
// Check if it's already in the array format [field, order]
|
|
274
|
+
const firstSort = normalized.sort[0];
|
|
275
|
+
if (firstSort && typeof firstSort === 'object' && !Array.isArray(firstSort)) {
|
|
276
|
+
// Convert from QueryAST format {field, order} to internal format [field, order]
|
|
277
|
+
normalized.sort = normalized.sort.map((item: any) => [
|
|
278
|
+
item.field,
|
|
279
|
+
item.order || item.direction || item.dir || 'asc'
|
|
280
|
+
]);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return normalized;
|
|
285
|
+
}
|
|
286
|
+
|
|
165
287
|
async find(objectName: string, query: any, options?: any): Promise<any[]> {
|
|
288
|
+
const normalizedQuery = this.normalizeQuery(query);
|
|
166
289
|
const collection = await this.getCollection(objectName);
|
|
167
|
-
const filter = this.mapFilters(
|
|
290
|
+
const filter = this.mapFilters(normalizedQuery.filters);
|
|
168
291
|
|
|
169
292
|
const findOptions: FindOptions = {};
|
|
170
|
-
if (
|
|
171
|
-
if (
|
|
172
|
-
if (
|
|
293
|
+
if (normalizedQuery.skip) findOptions.skip = normalizedQuery.skip;
|
|
294
|
+
if (normalizedQuery.limit) findOptions.limit = normalizedQuery.limit;
|
|
295
|
+
if (normalizedQuery.sort) {
|
|
173
296
|
// map [['field', 'desc']] to { field: -1 }
|
|
174
297
|
findOptions.sort = {};
|
|
175
|
-
for (const [field, order] of
|
|
298
|
+
for (const [field, order] of normalizedQuery.sort) {
|
|
176
299
|
// Map both 'id' and '_id' to '_id' for backward compatibility
|
|
177
300
|
const dbField = (field === 'id' || field === '_id') ? '_id' : field;
|
|
178
301
|
(findOptions.sort as any)[dbField] = order === 'desc' ? -1 : 1;
|
|
179
302
|
}
|
|
180
303
|
}
|
|
181
|
-
if (
|
|
304
|
+
if (normalizedQuery.fields && normalizedQuery.fields.length > 0) {
|
|
182
305
|
findOptions.projection = {};
|
|
183
|
-
for (const field of
|
|
306
|
+
for (const field of normalizedQuery.fields) {
|
|
184
307
|
// Map both 'id' and '_id' to '_id' for backward compatibility
|
|
185
308
|
const dbField = (field === 'id' || field === '_id') ? '_id' : field;
|
|
186
309
|
(findOptions.projection as any)[dbField] = 1;
|
|
187
310
|
}
|
|
311
|
+
// Explicitly exclude _id if 'id' is not in the requested fields
|
|
312
|
+
const hasIdField = normalizedQuery.fields.some((f: string) => f === 'id' || f === '_id');
|
|
313
|
+
if (!hasIdField) {
|
|
314
|
+
(findOptions.projection as any)._id = 0;
|
|
315
|
+
}
|
|
188
316
|
}
|
|
189
317
|
|
|
190
318
|
const results = await collection.find(filter, findOptions).toArray();
|
|
@@ -245,7 +373,10 @@ export class MongoDriver implements Driver {
|
|
|
245
373
|
|
|
246
374
|
async count(objectName: string, filters: any, options?: any): Promise<number> {
|
|
247
375
|
const collection = await this.getCollection(objectName);
|
|
248
|
-
|
|
376
|
+
// Normalize to support both filter arrays and full query objects
|
|
377
|
+
const normalizedQuery = this.normalizeQuery(filters);
|
|
378
|
+
const actualFilters = normalizedQuery.filters || filters;
|
|
379
|
+
const filter = this.mapFilters(actualFilters);
|
|
249
380
|
return await collection.countDocuments(filter);
|
|
250
381
|
}
|
|
251
382
|
|
|
@@ -299,5 +430,205 @@ export class MongoDriver implements Driver {
|
|
|
299
430
|
await this.client.close();
|
|
300
431
|
}
|
|
301
432
|
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Execute a query using QueryAST (DriverInterface v4.0 method)
|
|
436
|
+
*
|
|
437
|
+
* This is the new standard method for query execution using the
|
|
438
|
+
* ObjectStack QueryAST format.
|
|
439
|
+
*
|
|
440
|
+
* @param ast - The QueryAST representing the query
|
|
441
|
+
* @param options - Optional execution options
|
|
442
|
+
* @returns Query results with value and count
|
|
443
|
+
*/
|
|
444
|
+
async executeQuery(ast: QueryAST, options?: any): Promise<{ value: any[]; count?: number }> {
|
|
445
|
+
const objectName = ast.object || '';
|
|
446
|
+
|
|
447
|
+
// Convert QueryAST to legacy query format
|
|
448
|
+
const legacyQuery: any = {
|
|
449
|
+
fields: ast.fields,
|
|
450
|
+
filters: this.convertFilterNodeToLegacy(ast.filters),
|
|
451
|
+
sort: ast.sort?.map((s: SortNode) => [s.field, s.order]),
|
|
452
|
+
limit: ast.top,
|
|
453
|
+
skip: ast.skip,
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
// Use existing find method
|
|
457
|
+
const results = await this.find(objectName, legacyQuery, options);
|
|
458
|
+
|
|
459
|
+
return {
|
|
460
|
+
value: results,
|
|
461
|
+
count: results.length
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Execute a command (DriverInterface v4.0 method)
|
|
467
|
+
*
|
|
468
|
+
* This method handles all mutation operations (create, update, delete)
|
|
469
|
+
* using a unified command interface.
|
|
470
|
+
*
|
|
471
|
+
* @param command - The command to execute
|
|
472
|
+
* @param options - Optional execution options
|
|
473
|
+
* @returns Command execution result
|
|
474
|
+
*/
|
|
475
|
+
async executeCommand(command: Command, options?: any): Promise<CommandResult> {
|
|
476
|
+
try {
|
|
477
|
+
const cmdOptions = { ...options, ...command.options };
|
|
478
|
+
|
|
479
|
+
switch (command.type) {
|
|
480
|
+
case 'create':
|
|
481
|
+
if (!command.data) {
|
|
482
|
+
throw new Error('Create command requires data');
|
|
483
|
+
}
|
|
484
|
+
const created = await this.create(command.object, command.data, cmdOptions);
|
|
485
|
+
return {
|
|
486
|
+
success: true,
|
|
487
|
+
data: created,
|
|
488
|
+
affected: 1
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
case 'update':
|
|
492
|
+
if (!command.id || !command.data) {
|
|
493
|
+
throw new Error('Update command requires id and data');
|
|
494
|
+
}
|
|
495
|
+
const updated = await this.update(command.object, command.id, command.data, cmdOptions);
|
|
496
|
+
return {
|
|
497
|
+
success: true,
|
|
498
|
+
data: updated,
|
|
499
|
+
affected: updated ? 1 : 0
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
case 'delete':
|
|
503
|
+
if (!command.id) {
|
|
504
|
+
throw new Error('Delete command requires id');
|
|
505
|
+
}
|
|
506
|
+
const deleteCount = await this.delete(command.object, command.id, cmdOptions);
|
|
507
|
+
return {
|
|
508
|
+
success: true,
|
|
509
|
+
affected: deleteCount
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
case 'bulkCreate':
|
|
513
|
+
if (!command.records || !Array.isArray(command.records)) {
|
|
514
|
+
throw new Error('BulkCreate command requires records array');
|
|
515
|
+
}
|
|
516
|
+
const bulkCreated = await this.createMany(command.object, command.records, cmdOptions);
|
|
517
|
+
return {
|
|
518
|
+
success: true,
|
|
519
|
+
data: bulkCreated,
|
|
520
|
+
affected: command.records.length
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
case 'bulkUpdate':
|
|
524
|
+
if (!command.updates || !Array.isArray(command.updates)) {
|
|
525
|
+
throw new Error('BulkUpdate command requires updates array');
|
|
526
|
+
}
|
|
527
|
+
let updateCount = 0;
|
|
528
|
+
const updateResults = [];
|
|
529
|
+
for (const update of command.updates) {
|
|
530
|
+
const result = await this.update(command.object, update.id, update.data, cmdOptions);
|
|
531
|
+
updateResults.push(result);
|
|
532
|
+
if (result) updateCount++;
|
|
533
|
+
}
|
|
534
|
+
return {
|
|
535
|
+
success: true,
|
|
536
|
+
data: updateResults,
|
|
537
|
+
affected: updateCount
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
case 'bulkDelete':
|
|
541
|
+
if (!command.ids || !Array.isArray(command.ids)) {
|
|
542
|
+
throw new Error('BulkDelete command requires ids array');
|
|
543
|
+
}
|
|
544
|
+
let deleted = 0;
|
|
545
|
+
for (const id of command.ids) {
|
|
546
|
+
const result = await this.delete(command.object, id, cmdOptions);
|
|
547
|
+
deleted += result;
|
|
548
|
+
}
|
|
549
|
+
return {
|
|
550
|
+
success: true,
|
|
551
|
+
affected: deleted
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
default:
|
|
555
|
+
const validTypes = ['create', 'update', 'delete', 'bulkCreate', 'bulkUpdate', 'bulkDelete'];
|
|
556
|
+
throw new Error(`Unknown command type: ${(command as any).type}. Valid types are: ${validTypes.join(', ')}`);
|
|
557
|
+
}
|
|
558
|
+
} catch (error: any) {
|
|
559
|
+
return {
|
|
560
|
+
success: false,
|
|
561
|
+
error: error.message || 'Command execution failed',
|
|
562
|
+
affected: 0
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Convert FilterNode (QueryAST format) to legacy filter array format
|
|
569
|
+
* This allows reuse of existing filter logic while supporting new QueryAST
|
|
570
|
+
*
|
|
571
|
+
* @private
|
|
572
|
+
*/
|
|
573
|
+
private convertFilterNodeToLegacy(node?: FilterNode): any {
|
|
574
|
+
if (!node) return undefined;
|
|
575
|
+
|
|
576
|
+
switch (node.type) {
|
|
577
|
+
case 'comparison':
|
|
578
|
+
// Convert comparison node to [field, operator, value] format
|
|
579
|
+
const operator = node.operator || '=';
|
|
580
|
+
return [[node.field, operator, node.value]];
|
|
581
|
+
|
|
582
|
+
case 'and':
|
|
583
|
+
case 'or':
|
|
584
|
+
// Convert AND/OR node to array with separator
|
|
585
|
+
if (!node.children || node.children.length === 0) return undefined;
|
|
586
|
+
const results: any[] = [];
|
|
587
|
+
const separator = node.type; // 'and' or 'or'
|
|
588
|
+
|
|
589
|
+
for (const child of node.children) {
|
|
590
|
+
const converted = this.convertFilterNodeToLegacy(child);
|
|
591
|
+
if (converted) {
|
|
592
|
+
if (results.length > 0) {
|
|
593
|
+
results.push(separator);
|
|
594
|
+
}
|
|
595
|
+
results.push(...(Array.isArray(converted) ? converted : [converted]));
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return results.length > 0 ? results : undefined;
|
|
599
|
+
|
|
600
|
+
case 'not':
|
|
601
|
+
// NOT is not directly supported in the legacy filter format
|
|
602
|
+
// MongoDB supports $not, but legacy array format doesn't have a NOT operator
|
|
603
|
+
// Use native MongoDB queries with $not instead:
|
|
604
|
+
// Example: { field: { $not: { $eq: value } } }
|
|
605
|
+
throw new Error(
|
|
606
|
+
'NOT filters are not supported in legacy filter format. ' +
|
|
607
|
+
'Use native MongoDB queries with $not operator instead. ' +
|
|
608
|
+
'Example: { field: { $not: { $eq: value } } }'
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
default:
|
|
612
|
+
return undefined;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Execute command (alternative signature for compatibility)
|
|
618
|
+
*
|
|
619
|
+
* @param command - Command string or object
|
|
620
|
+
* @param parameters - Command parameters
|
|
621
|
+
* @param options - Execution options
|
|
622
|
+
*/
|
|
623
|
+
async execute(command: any, parameters?: any[], options?: any): Promise<any> {
|
|
624
|
+
// MongoDB driver doesn't support raw command execution in the traditional SQL sense
|
|
625
|
+
// Use executeCommand() instead for mutations (create/update/delete)
|
|
626
|
+
// Example: await driver.executeCommand({ type: 'create', object: 'users', data: {...} })
|
|
627
|
+
throw new Error(
|
|
628
|
+
'MongoDB driver does not support raw command execution. ' +
|
|
629
|
+
'Use executeCommand() for mutations or aggregate() for complex queries. ' +
|
|
630
|
+
'Example: driver.executeCommand({ type: "create", object: "users", data: {...} })'
|
|
631
|
+
);
|
|
632
|
+
}
|
|
302
633
|
}
|
|
303
634
|
|
package/test/index.test.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
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
|
+
|
|
1
9
|
import { MongoDriver } from '../src';
|
|
2
10
|
import { MongoClient } from 'mongodb';
|
|
3
11
|
|
|
@@ -9,6 +17,10 @@ const mockCollection = {
|
|
|
9
17
|
toArray: jest.fn().mockResolvedValue([]),
|
|
10
18
|
findOne: jest.fn().mockResolvedValue(null),
|
|
11
19
|
insertOne: jest.fn().mockResolvedValue({ insertedId: '123' }),
|
|
20
|
+
insertMany: jest.fn().mockResolvedValue({
|
|
21
|
+
insertedIds: { 0: 'id1', 1: 'id2' },
|
|
22
|
+
insertedCount: 2
|
|
23
|
+
}),
|
|
12
24
|
updateOne: jest.fn().mockResolvedValue({ modifiedCount: 1 }),
|
|
13
25
|
deleteOne: jest.fn().mockResolvedValue({ deletedCount: 1 }),
|
|
14
26
|
countDocuments: jest.fn().mockResolvedValue(10)
|
|
@@ -26,7 +38,10 @@ const mockClient = {
|
|
|
26
38
|
jest.mock('mongodb', () => {
|
|
27
39
|
return {
|
|
28
40
|
MongoClient: jest.fn().mockImplementation(() => mockClient),
|
|
29
|
-
ObjectId: jest.fn(id =>
|
|
41
|
+
ObjectId: jest.fn().mockImplementation((id?: string) => ({
|
|
42
|
+
toHexString: () => id || 'generated-object-id',
|
|
43
|
+
toString: () => id || 'generated-object-id'
|
|
44
|
+
}))
|
|
30
45
|
};
|
|
31
46
|
});
|
|
32
47
|
|
|
@@ -320,4 +335,246 @@ describe('MongoDriver', () => {
|
|
|
320
335
|
);
|
|
321
336
|
});
|
|
322
337
|
|
|
338
|
+
describe('DriverInterface v4.0 methods', () => {
|
|
339
|
+
describe('executeQuery', () => {
|
|
340
|
+
it('should execute a simple QueryAST query', async () => {
|
|
341
|
+
const ast = {
|
|
342
|
+
object: 'users',
|
|
343
|
+
fields: ['name', 'email'],
|
|
344
|
+
filters: {
|
|
345
|
+
type: 'comparison' as const,
|
|
346
|
+
field: 'status',
|
|
347
|
+
operator: '=',
|
|
348
|
+
value: 'active'
|
|
349
|
+
},
|
|
350
|
+
top: 10,
|
|
351
|
+
skip: 0
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
mockCollection.toArray.mockResolvedValue([
|
|
355
|
+
{ name: 'User 1', email: 'user1@example.com' },
|
|
356
|
+
{ name: 'User 2', email: 'user2@example.com' }
|
|
357
|
+
]);
|
|
358
|
+
|
|
359
|
+
const result = await driver.executeQuery(ast);
|
|
360
|
+
|
|
361
|
+
expect(result.value).toHaveLength(2);
|
|
362
|
+
expect(result.count).toBe(2);
|
|
363
|
+
expect(mockCollection.find).toHaveBeenCalled();
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('should handle complex QueryAST with AND filters', async () => {
|
|
367
|
+
const ast = {
|
|
368
|
+
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
|
+
}
|
|
384
|
+
]
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
mockCollection.toArray.mockResolvedValue([]);
|
|
389
|
+
|
|
390
|
+
const result = await driver.executeQuery(ast);
|
|
391
|
+
|
|
392
|
+
expect(result.value).toEqual([]);
|
|
393
|
+
expect(mockCollection.find).toHaveBeenCalled();
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('should handle QueryAST with sort', async () => {
|
|
397
|
+
const ast = {
|
|
398
|
+
object: 'users',
|
|
399
|
+
sort: [
|
|
400
|
+
{ field: 'name', order: 'asc' as const }
|
|
401
|
+
]
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
mockCollection.toArray.mockResolvedValue([]);
|
|
405
|
+
|
|
406
|
+
await driver.executeQuery(ast);
|
|
407
|
+
|
|
408
|
+
expect(mockCollection.find).toHaveBeenCalledWith(
|
|
409
|
+
{},
|
|
410
|
+
expect.objectContaining({
|
|
411
|
+
sort: { name: 1 }
|
|
412
|
+
})
|
|
413
|
+
);
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
describe('executeCommand', () => {
|
|
418
|
+
it('should execute create command', async () => {
|
|
419
|
+
const command = {
|
|
420
|
+
type: 'create' as const,
|
|
421
|
+
object: 'users',
|
|
422
|
+
data: { name: 'New User', email: 'new@example.com' }
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
mockCollection.insertOne.mockResolvedValue({
|
|
426
|
+
insertedId: 'new123',
|
|
427
|
+
acknowledged: true
|
|
428
|
+
} as any);
|
|
429
|
+
|
|
430
|
+
const result = await driver.executeCommand(command);
|
|
431
|
+
|
|
432
|
+
expect(result.success).toBe(true);
|
|
433
|
+
expect(result.affected).toBe(1);
|
|
434
|
+
expect(result.data).toBeDefined();
|
|
435
|
+
expect(mockCollection.insertOne).toHaveBeenCalled();
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('should execute update command', async () => {
|
|
439
|
+
const command = {
|
|
440
|
+
type: 'update' as const,
|
|
441
|
+
object: 'users',
|
|
442
|
+
id: '123',
|
|
443
|
+
data: { name: 'Updated User' }
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
mockCollection.updateOne.mockResolvedValue({
|
|
447
|
+
modifiedCount: 1,
|
|
448
|
+
acknowledged: true
|
|
449
|
+
} as any);
|
|
450
|
+
mockCollection.findOne.mockResolvedValue({
|
|
451
|
+
_id: '123',
|
|
452
|
+
name: 'Updated User'
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
const result = await driver.executeCommand(command);
|
|
456
|
+
|
|
457
|
+
expect(result.success).toBe(true);
|
|
458
|
+
expect(result.affected).toBe(1);
|
|
459
|
+
expect(mockCollection.updateOne).toHaveBeenCalled();
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('should execute delete command', async () => {
|
|
463
|
+
const command = {
|
|
464
|
+
type: 'delete' as const,
|
|
465
|
+
object: 'users',
|
|
466
|
+
id: '123'
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
mockCollection.deleteOne.mockResolvedValue({
|
|
470
|
+
deletedCount: 1,
|
|
471
|
+
acknowledged: true
|
|
472
|
+
} as any);
|
|
473
|
+
|
|
474
|
+
const result = await driver.executeCommand(command);
|
|
475
|
+
|
|
476
|
+
expect(result.success).toBe(true);
|
|
477
|
+
expect(result.affected).toBe(1);
|
|
478
|
+
expect(mockCollection.deleteOne).toHaveBeenCalled();
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it('should execute bulkCreate command', async () => {
|
|
482
|
+
const command = {
|
|
483
|
+
type: 'bulkCreate' as const,
|
|
484
|
+
object: 'users',
|
|
485
|
+
records: [
|
|
486
|
+
{ name: 'User 1', email: 'user1@example.com' },
|
|
487
|
+
{ name: 'User 2', email: 'user2@example.com' }
|
|
488
|
+
]
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
mockCollection.insertOne.mockResolvedValue({
|
|
492
|
+
insertedId: 'id1',
|
|
493
|
+
acknowledged: true
|
|
494
|
+
} as any);
|
|
495
|
+
|
|
496
|
+
const result = await driver.executeCommand(command);
|
|
497
|
+
|
|
498
|
+
expect(result.success).toBe(true);
|
|
499
|
+
expect(result.affected).toBe(2);
|
|
500
|
+
expect(result.data).toHaveLength(2);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it('should execute bulkUpdate command', async () => {
|
|
504
|
+
const command = {
|
|
505
|
+
type: 'bulkUpdate' as const,
|
|
506
|
+
object: 'users',
|
|
507
|
+
updates: [
|
|
508
|
+
{ id: '1', data: { name: 'Updated 1' } },
|
|
509
|
+
{ id: '2', data: { name: 'Updated 2' } }
|
|
510
|
+
]
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
mockCollection.updateOne.mockResolvedValue({
|
|
514
|
+
modifiedCount: 1,
|
|
515
|
+
acknowledged: true
|
|
516
|
+
} as any);
|
|
517
|
+
mockCollection.findOne.mockResolvedValue({ _id: '1', name: 'Updated 1' });
|
|
518
|
+
|
|
519
|
+
const result = await driver.executeCommand(command);
|
|
520
|
+
|
|
521
|
+
expect(result.success).toBe(true);
|
|
522
|
+
expect(result.affected).toBe(2);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it('should execute bulkDelete command', async () => {
|
|
526
|
+
const command = {
|
|
527
|
+
type: 'bulkDelete' as const,
|
|
528
|
+
object: 'users',
|
|
529
|
+
ids: ['1', '2', '3']
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
mockCollection.deleteOne.mockResolvedValue({
|
|
533
|
+
deletedCount: 1,
|
|
534
|
+
acknowledged: true
|
|
535
|
+
} as any);
|
|
536
|
+
|
|
537
|
+
const result = await driver.executeCommand(command);
|
|
538
|
+
|
|
539
|
+
expect(result.success).toBe(true);
|
|
540
|
+
expect(result.affected).toBe(3);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it('should handle command errors gracefully', async () => {
|
|
544
|
+
const command = {
|
|
545
|
+
type: 'create' as const,
|
|
546
|
+
object: 'users',
|
|
547
|
+
data: undefined // Invalid data
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
const result = await driver.executeCommand(command);
|
|
551
|
+
|
|
552
|
+
expect(result.success).toBe(false);
|
|
553
|
+
expect(result.error).toBeDefined();
|
|
554
|
+
expect(result.affected).toBe(0);
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it('should reject unknown command types', async () => {
|
|
558
|
+
const command = {
|
|
559
|
+
type: 'invalidCommand' as any,
|
|
560
|
+
object: 'users'
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
const result = await driver.executeCommand(command);
|
|
564
|
+
|
|
565
|
+
expect(result.success).toBe(false);
|
|
566
|
+
expect(result.error).toContain('Unknown command type');
|
|
567
|
+
expect(result.error).toContain('Valid types are');
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
describe('execute', () => {
|
|
572
|
+
it('should throw error as MongoDB does not support raw command execution', async () => {
|
|
573
|
+
await expect(driver.execute('SELECT * FROM users')).rejects.toThrow(
|
|
574
|
+
'MongoDB driver does not support raw command execution'
|
|
575
|
+
);
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
|
|
323
580
|
});
|
package/test/integration.test.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
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
|
+
|
|
1
9
|
import { MongoDriver } from '../src';
|
|
2
10
|
import { MongoClient } from 'mongodb';
|
|
3
11
|
import { MongoMemoryServer } from 'mongodb-memory-server';
|