@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.
- package/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +30 -0
- package/dist/index.d.ts +80 -5
- package/dist/index.js +221 -99
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +267 -112
- package/test/index.test.ts +64 -70
- package/test/integration.test.ts +250 -5
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectql/driver-mongo",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.3",
|
|
4
4
|
"description": "MongoDB driver for ObjectQL - Native aggregation pipeline translation for high-performance NoSQL operations",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"objectql",
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
"main": "dist/index.js",
|
|
17
17
|
"types": "dist/index.d.ts",
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@objectstack/spec": "^0.
|
|
19
|
+
"@objectstack/spec": "^0.8.0",
|
|
20
20
|
"mongodb": "^5.9.2",
|
|
21
|
-
"@objectql/types": "4.0.
|
|
21
|
+
"@objectql/types": "4.0.3"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"mongodb-memory-server": "^11.0.1"
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { Data, System } from '@objectstack/spec';
|
|
1
|
+
import { Data, System as SystemSpec } from '@objectstack/spec';
|
|
2
2
|
type QueryAST = Data.QueryAST;
|
|
3
|
-
type FilterNode = Data.FilterNode;
|
|
4
3
|
type SortNode = Data.SortNode;
|
|
5
|
-
type DriverInterface =
|
|
4
|
+
type DriverInterface = Data.DriverInterface;
|
|
6
5
|
/**
|
|
7
6
|
* ObjectQL
|
|
8
7
|
* Copyright (c) 2026-present ObjectStack Inc.
|
|
@@ -12,7 +11,24 @@ type DriverInterface = System.DriverInterface;
|
|
|
12
11
|
*/
|
|
13
12
|
|
|
14
13
|
import { Driver } from '@objectql/types';
|
|
15
|
-
import { MongoClient, Db, Filter, ObjectId, FindOptions } from 'mongodb';
|
|
14
|
+
import { MongoClient, Db, Filter, ObjectId, FindOptions, ChangeStream, ChangeStreamDocument } from 'mongodb';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Change stream event handler callback
|
|
18
|
+
*/
|
|
19
|
+
export type ChangeStreamHandler = (change: ChangeStreamDocument) => void | Promise<void>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Change stream options
|
|
23
|
+
*/
|
|
24
|
+
export interface ChangeStreamOptions {
|
|
25
|
+
/** Filter for specific operation types (insert, update, delete, replace) */
|
|
26
|
+
operationTypes?: ('insert' | 'update' | 'delete' | 'replace')[];
|
|
27
|
+
/** Full document lookup for update operations */
|
|
28
|
+
fullDocument?: 'updateLookup' | 'whenAvailable' | 'required';
|
|
29
|
+
/** Pipeline to filter change events */
|
|
30
|
+
pipeline?: any[];
|
|
31
|
+
}
|
|
16
32
|
|
|
17
33
|
/**
|
|
18
34
|
* Command interface for executeCommand method
|
|
@@ -69,11 +85,13 @@ export class MongoDriver implements Driver {
|
|
|
69
85
|
private db?: Db;
|
|
70
86
|
private config: any;
|
|
71
87
|
private connected: Promise<void>;
|
|
88
|
+
private changeStreams: Map<string, ChangeStream>;
|
|
72
89
|
|
|
73
90
|
constructor(config: { url: string, dbName?: string }) {
|
|
74
91
|
this.config = config;
|
|
75
92
|
this.client = new MongoClient(config.url);
|
|
76
93
|
this.connected = this.internalConnect();
|
|
94
|
+
this.changeStreams = new Map();
|
|
77
95
|
}
|
|
78
96
|
|
|
79
97
|
/**
|
|
@@ -150,12 +168,72 @@ export class MongoDriver implements Driver {
|
|
|
150
168
|
}
|
|
151
169
|
|
|
152
170
|
private mapFilters(filters: any): Filter<any> {
|
|
153
|
-
if (!filters
|
|
171
|
+
if (!filters) return {};
|
|
172
|
+
|
|
173
|
+
// If filters is an object (FilterCondition format), map id fields to _id
|
|
174
|
+
if (typeof filters === 'object' && !Array.isArray(filters)) {
|
|
175
|
+
return this.mapIdFieldsInFilter(filters);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// If filters is an array (legacy format), convert it
|
|
179
|
+
if (Array.isArray(filters) && filters.length === 0) return {};
|
|
154
180
|
|
|
155
181
|
const result = this.buildFilterConditions(filters);
|
|
156
182
|
return result;
|
|
157
183
|
}
|
|
158
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Recursively map 'id' fields to '_id' in FilterCondition objects
|
|
187
|
+
* and normalize primitive values to use $eq operator when inside logical operators
|
|
188
|
+
*/
|
|
189
|
+
private mapIdFieldsInFilter(filter: any, insideLogicalOp: boolean = false): any {
|
|
190
|
+
if (!filter || typeof filter !== 'object') {
|
|
191
|
+
return filter;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const result: any = {};
|
|
195
|
+
|
|
196
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
197
|
+
// Handle logical operators
|
|
198
|
+
if (key === '$and' || key === '$or') {
|
|
199
|
+
if (Array.isArray(value)) {
|
|
200
|
+
// Pass true to indicate we're inside a logical operator
|
|
201
|
+
result[key] = value.map(v => this.mapIdFieldsInFilter(v, true));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// Map 'id' to '_id'
|
|
205
|
+
else if (key === 'id') {
|
|
206
|
+
// If value is already an object with operators, recurse
|
|
207
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
208
|
+
result['_id'] = this.mapIdFieldsInFilter(value, insideLogicalOp);
|
|
209
|
+
} else {
|
|
210
|
+
// Primitive value - wrap with $eq only if inside logical operator
|
|
211
|
+
result['_id'] = insideLogicalOp ? { $eq: value } : value;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Skip MongoDB operator keys (starting with $) - but still recurse for nested mappings
|
|
215
|
+
else if (key.startsWith('$')) {
|
|
216
|
+
// Recursively process to handle nested objects that might contain 'id' fields
|
|
217
|
+
if (typeof value === 'object' && value !== null) {
|
|
218
|
+
result[key] = this.mapIdFieldsInFilter(value, insideLogicalOp);
|
|
219
|
+
} else {
|
|
220
|
+
result[key] = value;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Recursively handle nested objects (already operator-wrapped values)
|
|
224
|
+
else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
225
|
+
result[key] = this.mapIdFieldsInFilter(value, insideLogicalOp);
|
|
226
|
+
}
|
|
227
|
+
// Primitive field values
|
|
228
|
+
else {
|
|
229
|
+
// Wrap with $eq only if inside a logical operator
|
|
230
|
+
result[key] = insideLogicalOp ? { $eq: value } : value;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
|
|
159
237
|
/**
|
|
160
238
|
* Build MongoDB filter conditions from ObjectQL filter array.
|
|
161
239
|
* Supports nested filter groups and logical operators (AND/OR).
|
|
@@ -258,68 +336,45 @@ export class MongoDriver implements Driver {
|
|
|
258
336
|
* QueryAST sort is array of {field, order}, while UnifiedQuery is array of [field, order].
|
|
259
337
|
* QueryAST uses 'aggregations', while legacy uses 'aggregate'.
|
|
260
338
|
*/
|
|
261
|
-
|
|
262
|
-
|
|
339
|
+
async find(objectName: string, query: any, options?: any): Promise<any[]> {
|
|
340
|
+
const collection = await this.getCollection(objectName);
|
|
263
341
|
|
|
264
|
-
|
|
342
|
+
// Handle both new format (where) and legacy format (filters)
|
|
343
|
+
const filterCondition = query.where || query.filters;
|
|
344
|
+
const filter = this.mapFilters(filterCondition);
|
|
265
345
|
|
|
266
|
-
|
|
267
|
-
if (normalized.top !== undefined && normalized.limit === undefined) {
|
|
268
|
-
normalized.limit = normalized.top;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// Normalize aggregations/aggregate
|
|
272
|
-
if (normalized.aggregations !== undefined && normalized.aggregate === undefined) {
|
|
273
|
-
// Convert QueryAST aggregations format to legacy aggregate format
|
|
274
|
-
normalized.aggregate = normalized.aggregations.map((agg: any) => ({
|
|
275
|
-
func: agg.function || agg.func,
|
|
276
|
-
field: agg.field,
|
|
277
|
-
alias: agg.alias
|
|
278
|
-
}));
|
|
279
|
-
}
|
|
346
|
+
const findOptions: FindOptions = {};
|
|
280
347
|
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const firstSort = normalized.sort[0];
|
|
285
|
-
if (firstSort && typeof firstSort === 'object' && !Array.isArray(firstSort)) {
|
|
286
|
-
// Convert from QueryAST format {field, order} to internal format [field, order]
|
|
287
|
-
normalized.sort = normalized.sort.map((item: any) => [
|
|
288
|
-
item.field,
|
|
289
|
-
item.order || item.direction || item.dir || 'asc'
|
|
290
|
-
]);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
348
|
+
// Handle pagination - support both new format (offset/limit) and legacy format (skip/top)
|
|
349
|
+
const offsetValue = query.offset ?? query.skip;
|
|
350
|
+
const limitValue = query.limit ?? query.top;
|
|
293
351
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
async find(objectName: string, query: any, options?: any): Promise<any[]> {
|
|
298
|
-
const normalizedQuery = this.normalizeQuery(query);
|
|
299
|
-
const collection = await this.getCollection(objectName);
|
|
300
|
-
const filter = this.mapFilters(normalizedQuery.filters);
|
|
352
|
+
if (offsetValue !== undefined) findOptions.skip = offsetValue;
|
|
353
|
+
if (limitValue !== undefined) findOptions.limit = limitValue;
|
|
301
354
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
if (
|
|
305
|
-
if (normalizedQuery.sort) {
|
|
306
|
-
// map [['field', 'desc']] to { field: -1 }
|
|
355
|
+
// Handle sort - support both new format (orderBy) and legacy format (sort)
|
|
356
|
+
const sortArray = query.orderBy || query.sort;
|
|
357
|
+
if (sortArray && Array.isArray(sortArray)) {
|
|
307
358
|
findOptions.sort = {};
|
|
308
|
-
for (const
|
|
359
|
+
for (const item of sortArray) {
|
|
360
|
+
// Support both {field, order} object format and [field, order] array format
|
|
361
|
+
const field = item.field || item[0];
|
|
362
|
+
const order = item.order || item[1] || 'asc';
|
|
309
363
|
// Map both 'id' and '_id' to '_id' for backward compatibility
|
|
310
364
|
const dbField = (field === 'id' || field === '_id') ? '_id' : field;
|
|
311
365
|
(findOptions.sort as any)[dbField] = order === 'desc' ? -1 : 1;
|
|
312
366
|
}
|
|
313
367
|
}
|
|
314
|
-
|
|
368
|
+
|
|
369
|
+
if (query.fields && query.fields.length > 0) {
|
|
315
370
|
findOptions.projection = {};
|
|
316
|
-
for (const field of
|
|
371
|
+
for (const field of query.fields) {
|
|
317
372
|
// Map both 'id' and '_id' to '_id' for backward compatibility
|
|
318
373
|
const dbField = (field === 'id' || field === '_id') ? '_id' : field;
|
|
319
374
|
(findOptions.projection as any)[dbField] = 1;
|
|
320
375
|
}
|
|
321
376
|
// Explicitly exclude _id if 'id' is not in the requested fields
|
|
322
|
-
const hasIdField =
|
|
377
|
+
const hasIdField = query.fields.some((f: string) => f === 'id' || f === '_id');
|
|
323
378
|
if (!hasIdField) {
|
|
324
379
|
(findOptions.projection as any)._id = 0;
|
|
325
380
|
}
|
|
@@ -383,9 +438,12 @@ export class MongoDriver implements Driver {
|
|
|
383
438
|
|
|
384
439
|
async count(objectName: string, filters: any, options?: any): Promise<number> {
|
|
385
440
|
const collection = await this.getCollection(objectName);
|
|
386
|
-
//
|
|
387
|
-
|
|
388
|
-
|
|
441
|
+
// Handle both filter objects and query objects
|
|
442
|
+
let actualFilters = filters;
|
|
443
|
+
if (filters && (filters.where || filters.filters)) {
|
|
444
|
+
// It's a query object with 'where' or 'filters' property
|
|
445
|
+
actualFilters = filters.where || filters.filters;
|
|
446
|
+
}
|
|
389
447
|
const filter = this.mapFilters(actualFilters);
|
|
390
448
|
return await collection.countDocuments(filter);
|
|
391
449
|
}
|
|
@@ -435,12 +493,164 @@ export class MongoDriver implements Driver {
|
|
|
435
493
|
return this.mapFromMongoArray(results);
|
|
436
494
|
}
|
|
437
495
|
|
|
496
|
+
/**
|
|
497
|
+
* Get distinct values for a field
|
|
498
|
+
* @param objectName - The collection name
|
|
499
|
+
* @param field - The field to get distinct values from
|
|
500
|
+
* @param filters - Optional filters to apply
|
|
501
|
+
* @param options - Optional query options
|
|
502
|
+
* @returns Array of distinct values
|
|
503
|
+
*/
|
|
504
|
+
async distinct(objectName: string, field: string, filters?: any, options?: any): Promise<any[]> {
|
|
505
|
+
const collection = await this.getCollection(objectName);
|
|
506
|
+
|
|
507
|
+
// Convert ObjectQL filters to MongoDB query format
|
|
508
|
+
const filter = filters ? this.mapFilters(filters) : {};
|
|
509
|
+
|
|
510
|
+
// Use MongoDB's native distinct method
|
|
511
|
+
const results = await collection.distinct(field, filter);
|
|
512
|
+
|
|
513
|
+
return results;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Find one document and update it atomically
|
|
518
|
+
* @param objectName - The collection name
|
|
519
|
+
* @param filters - Query filters to find the document
|
|
520
|
+
* @param update - Update operations to apply
|
|
521
|
+
* @param options - Optional query options (e.g., returnDocument, upsert)
|
|
522
|
+
* @returns The updated document
|
|
523
|
+
*
|
|
524
|
+
* @example
|
|
525
|
+
* // Find and update with returnDocument: 'after' to get the updated doc
|
|
526
|
+
* const updated = await driver.findOneAndUpdate('users',
|
|
527
|
+
* { email: 'user@example.com' },
|
|
528
|
+
* { $set: { status: 'active' } },
|
|
529
|
+
* { returnDocument: 'after' }
|
|
530
|
+
* );
|
|
531
|
+
*/
|
|
532
|
+
async findOneAndUpdate(objectName: string, filters: any, update: any, options?: any): Promise<any> {
|
|
533
|
+
const collection = await this.getCollection(objectName);
|
|
534
|
+
|
|
535
|
+
// Convert ObjectQL filters to MongoDB query format
|
|
536
|
+
const filter = this.mapFilters(filters);
|
|
537
|
+
|
|
538
|
+
// MongoDB findOneAndUpdate options
|
|
539
|
+
const mongoOptions: any = {
|
|
540
|
+
returnDocument: options?.returnDocument || 'after', // 'before' or 'after'
|
|
541
|
+
upsert: options?.upsert || false
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
// Execute the atomic find and update
|
|
545
|
+
const result = await collection.findOneAndUpdate(filter, update, mongoOptions);
|
|
546
|
+
|
|
547
|
+
// Map MongoDB document to API format (convert _id to id)
|
|
548
|
+
// MongoDB driver v5+ returns { value: document, ok: number }
|
|
549
|
+
// Older versions (v4) return the document directly
|
|
550
|
+
// We handle both for backward compatibility
|
|
551
|
+
const doc = result?.value !== undefined ? result.value : result;
|
|
552
|
+
return doc ? this.mapFromMongo(doc) : null;
|
|
553
|
+
}
|
|
554
|
+
|
|
438
555
|
async disconnect() {
|
|
556
|
+
// Close all active change streams
|
|
557
|
+
for (const [streamId, stream] of this.changeStreams.entries()) {
|
|
558
|
+
await stream.close();
|
|
559
|
+
}
|
|
560
|
+
this.changeStreams.clear();
|
|
561
|
+
|
|
562
|
+
// Close the MongoDB client
|
|
439
563
|
if (this.client) {
|
|
440
564
|
await this.client.close();
|
|
441
565
|
}
|
|
442
566
|
}
|
|
443
567
|
|
|
568
|
+
/**
|
|
569
|
+
* Watch for changes in a collection using MongoDB Change Streams
|
|
570
|
+
* @param objectName - The collection name to watch
|
|
571
|
+
* @param handler - Callback function to handle change events
|
|
572
|
+
* @param options - Optional change stream configuration
|
|
573
|
+
* @returns Stream ID that can be used to close the stream later
|
|
574
|
+
*
|
|
575
|
+
* @example
|
|
576
|
+
* const streamId = await driver.watch('users', async (change) => {
|
|
577
|
+
* console.log('Change detected:', change.operationType);
|
|
578
|
+
* if (change.operationType === 'insert') {
|
|
579
|
+
* console.log('New document:', change.fullDocument);
|
|
580
|
+
* }
|
|
581
|
+
* }, {
|
|
582
|
+
* operationTypes: ['insert', 'update'],
|
|
583
|
+
* fullDocument: 'updateLookup'
|
|
584
|
+
* });
|
|
585
|
+
*
|
|
586
|
+
* // Later, to stop watching:
|
|
587
|
+
* await driver.unwatchChangeStream(streamId);
|
|
588
|
+
*/
|
|
589
|
+
async watch(objectName: string, handler: ChangeStreamHandler, options?: ChangeStreamOptions): Promise<string> {
|
|
590
|
+
const collection = await this.getCollection(objectName);
|
|
591
|
+
|
|
592
|
+
// Build change stream pipeline
|
|
593
|
+
const pipeline: any[] = options?.pipeline || [];
|
|
594
|
+
|
|
595
|
+
// Add operation type filter if specified
|
|
596
|
+
if (options?.operationTypes && options.operationTypes.length > 0) {
|
|
597
|
+
pipeline.unshift({
|
|
598
|
+
$match: {
|
|
599
|
+
operationType: { $in: options.operationTypes }
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Configure change stream options
|
|
605
|
+
const streamOptions: any = {};
|
|
606
|
+
if (options?.fullDocument) {
|
|
607
|
+
streamOptions.fullDocument = options.fullDocument;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Create the change stream
|
|
611
|
+
const changeStream = collection.watch(pipeline, streamOptions);
|
|
612
|
+
|
|
613
|
+
// Generate unique stream ID
|
|
614
|
+
const streamId = `${objectName}_${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
|
615
|
+
|
|
616
|
+
// Store the stream
|
|
617
|
+
this.changeStreams.set(streamId, changeStream);
|
|
618
|
+
|
|
619
|
+
// Handle change events
|
|
620
|
+
changeStream.on('change', async (change) => {
|
|
621
|
+
try {
|
|
622
|
+
await handler(change);
|
|
623
|
+
} catch (error) {
|
|
624
|
+
console.error(`[MongoDriver] Error in change stream handler for ${objectName}:`, error);
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
changeStream.on('error', (error) => {
|
|
629
|
+
console.error(`[MongoDriver] Change stream error for ${objectName}:`, error);
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
return streamId;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Stop watching a change stream
|
|
637
|
+
* @param streamId - The stream ID returned by watch()
|
|
638
|
+
*/
|
|
639
|
+
async unwatchChangeStream(streamId: string): Promise<void> {
|
|
640
|
+
const stream = this.changeStreams.get(streamId);
|
|
641
|
+
if (stream) {
|
|
642
|
+
await stream.close();
|
|
643
|
+
this.changeStreams.delete(streamId);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Get all active change stream IDs
|
|
649
|
+
*/
|
|
650
|
+
getActiveChangeStreams(): string[] {
|
|
651
|
+
return Array.from(this.changeStreams.keys());
|
|
652
|
+
}
|
|
653
|
+
|
|
444
654
|
/**
|
|
445
655
|
* Execute a query using QueryAST (DriverInterface v4.0 method)
|
|
446
656
|
*
|
|
@@ -452,19 +662,9 @@ export class MongoDriver implements Driver {
|
|
|
452
662
|
* @returns Query results with value and count
|
|
453
663
|
*/
|
|
454
664
|
async executeQuery(ast: QueryAST, options?: any): Promise<{ value: any[]; count?: number }> {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
const legacyQuery: any = {
|
|
459
|
-
fields: ast.fields,
|
|
460
|
-
filters: this.convertFilterNodeToLegacy(ast.filters),
|
|
461
|
-
sort: ast.sort?.map((s: SortNode) => [s.field, s.order]),
|
|
462
|
-
limit: ast.top,
|
|
463
|
-
skip: ast.skip,
|
|
464
|
-
};
|
|
465
|
-
|
|
466
|
-
// Use existing find method
|
|
467
|
-
const results = await this.find(objectName, legacyQuery, options);
|
|
665
|
+
// QueryAST is now the same format as our internal query
|
|
666
|
+
// Just pass it directly to find
|
|
667
|
+
const results = await this.find(ast.object || '', ast, options);
|
|
468
668
|
|
|
469
669
|
return {
|
|
470
670
|
value: results,
|
|
@@ -575,54 +775,9 @@ export class MongoDriver implements Driver {
|
|
|
575
775
|
}
|
|
576
776
|
|
|
577
777
|
/**
|
|
578
|
-
* Convert
|
|
778
|
+
* Convert FilterCondition (QueryAST format) to legacy filter array format
|
|
579
779
|
* This allows reuse of existing filter logic while supporting new QueryAST
|
|
580
780
|
*
|
|
581
|
-
* @private
|
|
582
|
-
*/
|
|
583
|
-
private convertFilterNodeToLegacy(node?: FilterNode): any {
|
|
584
|
-
if (!node) return undefined;
|
|
585
|
-
|
|
586
|
-
switch (node.type) {
|
|
587
|
-
case 'comparison':
|
|
588
|
-
// Convert comparison node to [field, operator, value] format
|
|
589
|
-
const operator = node.operator || '=';
|
|
590
|
-
return [[node.field, operator, node.value]];
|
|
591
|
-
|
|
592
|
-
case 'and':
|
|
593
|
-
case 'or':
|
|
594
|
-
// Convert AND/OR node to array with separator
|
|
595
|
-
if (!node.children || node.children.length === 0) return undefined;
|
|
596
|
-
const results: any[] = [];
|
|
597
|
-
const separator = node.type; // 'and' or 'or'
|
|
598
|
-
|
|
599
|
-
for (const child of node.children) {
|
|
600
|
-
const converted = this.convertFilterNodeToLegacy(child);
|
|
601
|
-
if (converted) {
|
|
602
|
-
if (results.length > 0) {
|
|
603
|
-
results.push(separator);
|
|
604
|
-
}
|
|
605
|
-
results.push(...(Array.isArray(converted) ? converted : [converted]));
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
return results.length > 0 ? results : undefined;
|
|
609
|
-
|
|
610
|
-
case 'not':
|
|
611
|
-
// NOT is not directly supported in the legacy filter format
|
|
612
|
-
// MongoDB supports $not, but legacy array format doesn't have a NOT operator
|
|
613
|
-
// Use native MongoDB queries with $not instead:
|
|
614
|
-
// Example: { field: { $not: { $eq: value } } }
|
|
615
|
-
throw new Error(
|
|
616
|
-
'NOT filters are not supported in legacy filter format. ' +
|
|
617
|
-
'Use native MongoDB queries with $not operator instead. ' +
|
|
618
|
-
'Example: { field: { $not: { $eq: value } } }'
|
|
619
|
-
);
|
|
620
|
-
|
|
621
|
-
default:
|
|
622
|
-
return undefined;
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
|
|
626
781
|
/**
|
|
627
782
|
* Execute command (alternative signature for compatibility)
|
|
628
783
|
*
|