@objectql/driver-memory 4.0.0 → 4.0.2

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/src/index.ts CHANGED
@@ -1,3 +1,7 @@
1
+ import { Data, Driver as DriverSpec } from '@objectstack/spec';
2
+ type QueryAST = Data.QueryAST;
3
+ type SortNode = Data.SortNode;
4
+ type DriverInterface = DriverSpec.DriverInterface;
1
5
  /**
2
6
  * ObjectQL
3
7
  * Copyright (c) 2026-present ObjectStack Inc.
@@ -9,17 +13,17 @@
9
13
  /**
10
14
  * Memory Driver for ObjectQL (Production-Ready)
11
15
  *
12
- * A high-performance in-memory driver for ObjectQL that stores data in JavaScript Maps.
16
+ * A high-performance in-memory driver for ObjectQL powered by Mingo.
13
17
  * Perfect for testing, development, and environments where persistence is not required.
14
18
  *
15
- * Implements both the legacy Driver interface from @objectql/types and
16
- * the standard DriverInterface from @objectstack/spec for full compatibility
19
+ * Implements the Driver interface from @objectql/types which includes all methods
20
+ * from the standard DriverInterface from @objectstack/spec for full compatibility
17
21
  * with the new kernel-based plugin system.
18
22
  *
19
23
  * ✅ Production-ready features:
20
- * - Zero external dependencies
24
+ * - MongoDB-like query engine powered by Mingo
21
25
  * - Thread-safe operations
22
- * - Full query support (filters, sorting, pagination)
26
+ * - Full query support (filters, sorting, pagination, aggregation)
23
27
  * - Atomic transactions
24
28
  * - High performance (no I/O overhead)
25
29
  *
@@ -30,11 +34,11 @@
30
34
  * - Client-side state management
31
35
  * - Temporary data caching
32
36
  *
33
- * @version 4.0.0 - DriverInterface compliant
37
+ * @version 4.0.0 - DriverInterface compliant with Mingo integration
34
38
  */
35
39
 
36
40
  import { Driver, ObjectQLError } from '@objectql/types';
37
- import { DriverInterface, QueryAST, FilterNode, SortNode } from '@objectstack/spec';
41
+ import { Query } from 'mingo';
38
42
 
39
43
  /**
40
44
  * Command interface for executeCommand method
@@ -78,7 +82,7 @@ export interface MemoryDriverConfig {
78
82
  *
79
83
  * Example: `users:user-123` → `{id: "user-123", name: "Alice", ...}`
80
84
  */
81
- export class MemoryDriver implements Driver, DriverInterface {
85
+ export class MemoryDriver implements Driver {
82
86
  // Driver metadata (ObjectStack-compatible)
83
87
  public readonly name = 'MemoryDriver';
84
88
  public readonly version = '4.0.0';
@@ -87,7 +91,13 @@ export class MemoryDriver implements Driver, DriverInterface {
87
91
  joins: false,
88
92
  fullTextSearch: false,
89
93
  jsonFields: true,
90
- arrayFields: true
94
+ arrayFields: true,
95
+ queryFilters: true,
96
+ queryAggregations: false,
97
+ querySorting: true,
98
+ queryPagination: true,
99
+ queryWindowFunctions: false,
100
+ querySubqueries: false
91
101
  };
92
102
 
93
103
  private store: Map<string, any>;
@@ -136,46 +146,47 @@ export class MemoryDriver implements Driver, DriverInterface {
136
146
 
137
147
  /**
138
148
  * Find multiple records matching the query criteria.
139
- * Supports filtering, sorting, pagination, and field projection.
149
+ * Supports filtering, sorting, pagination, and field projection using Mingo.
140
150
  */
141
151
  async find(objectName: string, query: any = {}, options?: any): Promise<any[]> {
142
- // Normalize query to support both legacy and QueryAST formats
143
- const normalizedQuery = this.normalizeQuery(query);
144
-
145
152
  // Get all records for this object type
146
153
  const pattern = `${objectName}:`;
147
- let results: any[] = [];
154
+ let records: any[] = [];
148
155
 
149
156
  for (const [key, value] of this.store.entries()) {
150
157
  if (key.startsWith(pattern)) {
151
- results.push({ ...value });
158
+ records.push({ ...value });
152
159
  }
153
160
  }
154
161
 
155
- // Apply filters
156
- if (normalizedQuery.filters) {
157
- results = this.applyFilters(results, normalizedQuery.filters);
162
+ // Convert ObjectQL filters to MongoDB query format
163
+ const mongoQuery = this.convertToMongoQuery(query.where);
164
+
165
+ // Apply filters using Mingo
166
+ if (mongoQuery && Object.keys(mongoQuery).length > 0) {
167
+ const mingoQuery = new Query(mongoQuery);
168
+ records = mingoQuery.find(records).all();
158
169
  }
159
170
 
160
- // Apply sorting
161
- if (normalizedQuery.sort && Array.isArray(normalizedQuery.sort)) {
162
- results = this.applySort(results, normalizedQuery.sort);
171
+ // Apply sorting manually (Mingo's sort has issues with CJS builds)
172
+ if (query.orderBy && Array.isArray(query.orderBy) && query.orderBy.length > 0) {
173
+ records = this.applyManualSort(records, query.orderBy);
163
174
  }
164
175
 
165
176
  // Apply pagination
166
- if (normalizedQuery.skip) {
167
- results = results.slice(normalizedQuery.skip);
177
+ if (query.offset) {
178
+ records = records.slice(query.offset);
168
179
  }
169
- if (normalizedQuery.limit) {
170
- results = results.slice(0, normalizedQuery.limit);
180
+ if (query.limit) {
181
+ records = records.slice(0, query.limit);
171
182
  }
172
183
 
173
184
  // Apply field projection
174
- if (normalizedQuery.fields && Array.isArray(normalizedQuery.fields)) {
175
- results = results.map(doc => this.projectFields(doc, normalizedQuery.fields));
185
+ if (query.fields && Array.isArray(query.fields)) {
186
+ records = records.map(doc => this.projectFields(doc, query.fields));
176
187
  }
177
188
 
178
- return results;
189
+ return records;
179
190
  }
180
191
 
181
192
  /**
@@ -276,55 +287,70 @@ export class MemoryDriver implements Driver, DriverInterface {
276
287
  }
277
288
 
278
289
  /**
279
- * Count records matching filters.
290
+ * Count records matching filters using Mingo.
280
291
  */
281
292
  async count(objectName: string, filters: any, options?: any): Promise<number> {
282
293
  const pattern = `${objectName}:`;
283
- let count = 0;
284
294
 
285
- // Extract actual filters from query object if needed
286
- let actualFilters = filters;
287
- if (filters && !Array.isArray(filters) && filters.filters) {
288
- actualFilters = filters.filters;
289
- }
290
-
291
- // If no filters, return total count
292
- if (!actualFilters || (Array.isArray(actualFilters) && actualFilters.length === 0)) {
293
- for (const key of this.store.keys()) {
294
- if (key.startsWith(pattern)) {
295
- count++;
296
- }
297
- }
298
- return count;
295
+ // Extract where condition from query object if needed
296
+ let whereCondition = filters;
297
+ if (filters && !Array.isArray(filters) && filters.where) {
298
+ whereCondition = filters.where;
299
299
  }
300
300
 
301
- // Count only records matching filters
301
+ // Get all records for this object type
302
+ let records: any[] = [];
302
303
  for (const [key, value] of this.store.entries()) {
303
304
  if (key.startsWith(pattern)) {
304
- if (this.matchesFilters(value, actualFilters)) {
305
- count++;
306
- }
305
+ records.push(value);
307
306
  }
308
307
  }
309
308
 
310
- return count;
309
+ // If no filters, return total count
310
+ if (!whereCondition || (Array.isArray(whereCondition) && whereCondition.length === 0)) {
311
+ return records.length;
312
+ }
313
+
314
+ // Convert to MongoDB query and use Mingo to count
315
+ const mongoQuery = this.convertToMongoQuery(whereCondition);
316
+ if (mongoQuery && Object.keys(mongoQuery).length > 0) {
317
+ const mingoQuery = new Query(mongoQuery);
318
+ const matchedRecords = mingoQuery.find(records).all();
319
+ return matchedRecords.length;
320
+ }
321
+
322
+ return records.length;
311
323
  }
312
324
 
313
325
  /**
314
- * Get distinct values for a field.
326
+ * Get distinct values for a field using Mingo.
315
327
  */
316
328
  async distinct(objectName: string, field: string, filters?: any, options?: any): Promise<any[]> {
317
329
  const pattern = `${objectName}:`;
318
- const values = new Set<any>();
319
330
 
320
- for (const [key, record] of this.store.entries()) {
331
+ // Get all records for this object type
332
+ let records: any[] = [];
333
+ for (const [key, value] of this.store.entries()) {
321
334
  if (key.startsWith(pattern)) {
322
- if (!filters || this.matchesFilters(record, filters)) {
323
- const value = record[field];
324
- if (value !== undefined && value !== null) {
325
- values.add(value);
326
- }
327
- }
335
+ records.push(value);
336
+ }
337
+ }
338
+
339
+ // Apply filters using Mingo if provided
340
+ if (filters) {
341
+ const mongoQuery = this.convertToMongoQuery(filters);
342
+ if (mongoQuery && Object.keys(mongoQuery).length > 0) {
343
+ const mingoQuery = new Query(mongoQuery);
344
+ records = mingoQuery.find(records).all();
345
+ }
346
+ }
347
+
348
+ // Extract distinct values
349
+ const values = new Set<any>();
350
+ for (const record of records) {
351
+ const value = record[field];
352
+ if (value !== undefined && value !== null) {
353
+ values.add(value);
328
354
  }
329
355
  }
330
356
 
@@ -344,25 +370,45 @@ export class MemoryDriver implements Driver, DriverInterface {
344
370
  }
345
371
 
346
372
  /**
347
- * Update multiple records matching filters.
373
+ * Update multiple records matching filters using Mingo.
348
374
  */
349
375
  async updateMany(objectName: string, filters: any, data: any, options?: any): Promise<any> {
350
376
  const pattern = `${objectName}:`;
351
- let count = 0;
377
+
378
+ // Get all records for this object type
379
+ let records: any[] = [];
380
+ const recordKeys = new Map<string, string>();
352
381
 
353
382
  for (const [key, record] of this.store.entries()) {
354
383
  if (key.startsWith(pattern)) {
355
- if (this.matchesFilters(record, filters)) {
356
- const updated = {
357
- ...record,
358
- ...data,
359
- id: record.id, // Preserve ID
360
- created_at: record.created_at, // Preserve created_at
361
- updated_at: new Date().toISOString()
362
- };
363
- this.store.set(key, updated);
364
- count++;
365
- }
384
+ records.push(record);
385
+ recordKeys.set(record.id, key);
386
+ }
387
+ }
388
+
389
+ // Apply filters using Mingo
390
+ const mongoQuery = this.convertToMongoQuery(filters);
391
+ let matchedRecords = records;
392
+
393
+ if (mongoQuery && Object.keys(mongoQuery).length > 0) {
394
+ const mingoQuery = new Query(mongoQuery);
395
+ matchedRecords = mingoQuery.find(records).all();
396
+ }
397
+
398
+ // Update matched records
399
+ let count = 0;
400
+ for (const record of matchedRecords) {
401
+ const key = recordKeys.get(record.id);
402
+ if (key) {
403
+ const updated = {
404
+ ...record,
405
+ ...data,
406
+ id: record.id, // Preserve ID
407
+ created_at: record.created_at, // Preserve created_at
408
+ updated_at: new Date().toISOString()
409
+ };
410
+ this.store.set(key, updated);
411
+ count++;
366
412
  }
367
413
  }
368
414
 
@@ -370,25 +416,40 @@ export class MemoryDriver implements Driver, DriverInterface {
370
416
  }
371
417
 
372
418
  /**
373
- * Delete multiple records matching filters.
419
+ * Delete multiple records matching filters using Mingo.
374
420
  */
375
421
  async deleteMany(objectName: string, filters: any, options?: any): Promise<any> {
376
422
  const pattern = `${objectName}:`;
377
- const keysToDelete: string[] = [];
423
+
424
+ // Get all records for this object type
425
+ let records: any[] = [];
426
+ const recordKeys = new Map<string, string>();
378
427
 
379
428
  for (const [key, record] of this.store.entries()) {
380
429
  if (key.startsWith(pattern)) {
381
- if (this.matchesFilters(record, filters)) {
382
- keysToDelete.push(key);
383
- }
430
+ records.push(record);
431
+ recordKeys.set(record.id, key);
384
432
  }
385
433
  }
386
434
 
387
- for (const key of keysToDelete) {
388
- this.store.delete(key);
435
+ // Apply filters using Mingo
436
+ const mongoQuery = this.convertToMongoQuery(filters);
437
+ let matchedRecords = records;
438
+
439
+ if (mongoQuery && Object.keys(mongoQuery).length > 0) {
440
+ const mingoQuery = new Query(mongoQuery);
441
+ matchedRecords = mingoQuery.find(records).all();
442
+ }
443
+
444
+ // Delete matched records
445
+ for (const record of matchedRecords) {
446
+ const key = recordKeys.get(record.id);
447
+ if (key) {
448
+ this.store.delete(key);
449
+ }
389
450
  }
390
451
 
391
- return { deletedCount: keysToDelete.length };
452
+ return { deletedCount: matchedRecords.length };
392
453
  }
393
454
 
394
455
  /**
@@ -422,137 +483,157 @@ export class MemoryDriver implements Driver, DriverInterface {
422
483
  * QueryAST format uses 'top' for limit, while UnifiedQuery uses 'limit'.
423
484
  * QueryAST sort is array of {field, order}, while UnifiedQuery is array of [field, order].
424
485
  */
425
- private normalizeQuery(query: any): any {
426
- if (!query) return {};
427
-
428
- const normalized: any = { ...query };
429
-
430
- // Normalize limit/top
431
- if (normalized.top !== undefined && normalized.limit === undefined) {
432
- normalized.limit = normalized.top;
433
- }
434
-
435
- // Normalize sort format
436
- if (normalized.sort && Array.isArray(normalized.sort)) {
437
- // Check if it's already in the array format [field, order]
438
- const firstSort = normalized.sort[0];
439
- if (firstSort && typeof firstSort === 'object' && !Array.isArray(firstSort)) {
440
- // Convert from QueryAST format {field, order} to internal format [field, order]
441
- normalized.sort = normalized.sort.map((item: any) => [
442
- item.field,
443
- item.order || item.direction || item.dir || 'asc'
444
- ]);
445
- }
446
- }
447
-
448
- return normalized;
449
- }
450
-
451
486
  /**
452
- * Apply filters to an array of records (in-memory filtering).
487
+ * Convert ObjectQL filters to MongoDB query format for Mingo.
488
+ *
489
+ * Supports both:
490
+ * 1. Legacy ObjectQL filter format (array):
491
+ * [['field', 'operator', value], 'or', ['field2', 'operator', value2']]
492
+ * 2. New FilterCondition format (object - already MongoDB-like):
493
+ * { $and: [{ field: { $eq: value }}, { field2: { $gt: value2 }}] }
453
494
  *
454
- * Supports ObjectQL filter format:
455
- * [
456
- * ['field', 'operator', value],
457
- * 'or',
458
- * ['field2', 'operator', value2]
459
- * ]
495
+ * Converts to MongoDB query format:
496
+ * { $or: [{ field: { $operator: value }}, { field2: { $operator: value2 }}] }
460
497
  */
461
- private applyFilters(records: any[], filters: any[]): any[] {
462
- if (!filters || filters.length === 0) {
463
- return records;
498
+ private convertToMongoQuery(filters?: any[] | Record<string, any>): Record<string, any> {
499
+ if (!filters) {
500
+ return {};
464
501
  }
465
502
 
466
- return records.filter(record => this.matchesFilters(record, filters));
467
- }
468
-
469
- /**
470
- * Check if a single record matches the filter conditions.
471
- */
472
- private matchesFilters(record: any, filters: any[]): boolean {
473
- if (!filters || filters.length === 0) {
474
- return true;
503
+ // If filters is already an object (FilterCondition format), return it directly
504
+ if (!Array.isArray(filters)) {
505
+ return filters;
475
506
  }
476
507
 
477
- let conditions: boolean[] = [];
478
- let operators: string[] = [];
508
+ // Handle legacy array format
509
+ if (filters.length === 0) {
510
+ return {};
511
+ }
512
+
513
+ // Process the filter array to build MongoDB query
514
+ const conditions: Record<string, any>[] = [];
515
+ let currentLogic: 'and' | 'or' = 'and';
516
+ const logicGroups: { logic: 'and' | 'or', conditions: Record<string, any>[] }[] = [
517
+ { logic: 'and', conditions: [] }
518
+ ];
479
519
 
480
520
  for (const item of filters) {
481
521
  if (typeof item === 'string') {
482
522
  // Logical operator (and/or)
483
- operators.push(item.toLowerCase());
523
+ const newLogic = item.toLowerCase() as 'and' | 'or';
524
+ if (newLogic !== currentLogic) {
525
+ currentLogic = newLogic;
526
+ logicGroups.push({ logic: currentLogic, conditions: [] });
527
+ }
484
528
  } else if (Array.isArray(item)) {
485
529
  const [field, operator, value] = item;
486
530
 
487
- // Handle nested filter groups
488
- if (typeof field !== 'string') {
489
- // Nested group - recursively evaluate
490
- conditions.push(this.matchesFilters(record, item));
491
- } else {
492
- // Single condition
493
- const matches = this.evaluateCondition(record[field], operator, value);
494
- conditions.push(matches);
531
+ // Convert single condition to MongoDB operator
532
+ const mongoCondition = this.convertConditionToMongo(field, operator, value);
533
+ if (mongoCondition) {
534
+ logicGroups[logicGroups.length - 1].conditions.push(mongoCondition);
495
535
  }
496
536
  }
497
537
  }
498
538
 
499
- // Combine conditions with operators
500
- if (conditions.length === 0) {
501
- return true;
539
+ // Build final query from logic groups
540
+ if (logicGroups.length === 1 && logicGroups[0].conditions.length === 1) {
541
+ return logicGroups[0].conditions[0];
502
542
  }
503
543
 
504
- let result = conditions[0];
505
- for (let i = 0; i < operators.length; i++) {
506
- const op = operators[i];
507
- const nextCondition = conditions[i + 1];
544
+ // If there's only one group with multiple conditions, use its logic operator
545
+ if (logicGroups.length === 1) {
546
+ const group = logicGroups[0];
547
+ if (group.logic === 'or') {
548
+ return { $or: group.conditions };
549
+ } else {
550
+ return { $and: group.conditions };
551
+ }
552
+ }
553
+
554
+ // Multiple groups - flatten all conditions and determine the top-level operator
555
+ const allConditions: Record<string, any>[] = [];
556
+ for (const group of logicGroups) {
557
+ if (group.conditions.length === 0) continue;
508
558
 
509
- if (op === 'or') {
510
- result = result || nextCondition;
511
- } else { // 'and' or default
512
- result = result && nextCondition;
559
+ if (group.conditions.length === 1) {
560
+ allConditions.push(group.conditions[0]);
561
+ } else {
562
+ if (group.logic === 'or') {
563
+ allConditions.push({ $or: group.conditions });
564
+ } else {
565
+ allConditions.push({ $and: group.conditions });
566
+ }
513
567
  }
514
568
  }
515
569
 
516
- return result;
570
+ if (allConditions.length === 0) {
571
+ return {};
572
+ } else if (allConditions.length === 1) {
573
+ return allConditions[0];
574
+ } else {
575
+ // Determine top-level operator: use OR if any non-empty group has OR logic
576
+ const hasOrLogic = logicGroups.some(g => g.logic === 'or' && g.conditions.length > 0);
577
+ if (hasOrLogic) {
578
+ return { $or: allConditions };
579
+ } else {
580
+ return { $and: allConditions };
581
+ }
582
+ }
517
583
  }
518
584
 
519
585
  /**
520
- * Evaluate a single filter condition.
586
+ * Convert a single ObjectQL condition to MongoDB operator format.
521
587
  */
522
- private evaluateCondition(fieldValue: any, operator: string, compareValue: any): boolean {
588
+ private convertConditionToMongo(field: string, operator: string, value: any): Record<string, any> | null {
523
589
  switch (operator) {
524
590
  case '=':
525
591
  case '==':
526
- return fieldValue === compareValue;
592
+ return { [field]: value };
593
+
527
594
  case '!=':
528
595
  case '<>':
529
- return fieldValue !== compareValue;
596
+ return { [field]: { $ne: value } };
597
+
530
598
  case '>':
531
- return fieldValue > compareValue;
599
+ return { [field]: { $gt: value } };
600
+
532
601
  case '>=':
533
- return fieldValue >= compareValue;
602
+ return { [field]: { $gte: value } };
603
+
534
604
  case '<':
535
- return fieldValue < compareValue;
605
+ return { [field]: { $lt: value } };
606
+
536
607
  case '<=':
537
- return fieldValue <= compareValue;
608
+ return { [field]: { $lte: value } };
609
+
538
610
  case 'in':
539
- return Array.isArray(compareValue) && compareValue.includes(fieldValue);
611
+ return { [field]: { $in: value } };
612
+
540
613
  case 'nin':
541
614
  case 'not in':
542
- return Array.isArray(compareValue) && !compareValue.includes(fieldValue);
615
+ return { [field]: { $nin: value } };
616
+
543
617
  case 'contains':
544
618
  case 'like':
545
- return String(fieldValue).toLowerCase().includes(String(compareValue).toLowerCase());
619
+ // MongoDB regex for case-insensitive contains
620
+ // Escape special regex characters to prevent ReDoS and ensure literal matching
621
+ return { [field]: { $regex: new RegExp(this.escapeRegex(value), 'i') } };
622
+
546
623
  case 'startswith':
547
624
  case 'starts_with':
548
- return String(fieldValue).toLowerCase().startsWith(String(compareValue).toLowerCase());
625
+ return { [field]: { $regex: new RegExp(`^${this.escapeRegex(value)}`, 'i') } };
626
+
549
627
  case 'endswith':
550
628
  case 'ends_with':
551
- return String(fieldValue).toLowerCase().endsWith(String(compareValue).toLowerCase());
629
+ return { [field]: { $regex: new RegExp(`${this.escapeRegex(value)}$`, 'i') } };
630
+
552
631
  case 'between':
553
- return Array.isArray(compareValue) &&
554
- fieldValue >= compareValue[0] &&
555
- fieldValue <= compareValue[1];
632
+ if (Array.isArray(value) && value.length === 2) {
633
+ return { [field]: { $gte: value[0], $lte: value[1] } };
634
+ }
635
+ return null;
636
+
556
637
  default:
557
638
  throw new ObjectQLError({
558
639
  code: 'UNSUPPORTED_OPERATOR',
@@ -562,12 +643,21 @@ export class MemoryDriver implements Driver, DriverInterface {
562
643
  }
563
644
 
564
645
  /**
565
- * Apply sorting to an array of records (in-memory sorting).
646
+ * Escape special regex characters to prevent ReDoS and ensure literal matching.
647
+ * This is crucial for security when using user input in regex patterns.
648
+ */
649
+ private escapeRegex(str: string): string {
650
+ return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
651
+ }
652
+
653
+ /**
654
+ * Apply manual sorting to an array of records.
655
+ * This is used instead of Mingo's sort to avoid CJS build issues.
566
656
  */
567
- private applySort(records: any[], sort: any[]): any[] {
657
+ private applyManualSort(records: any[], sort: any[]): any[] {
568
658
  const sorted = [...records];
569
659
 
570
- // Apply sorts in reverse order for correct precedence
660
+ // Apply sorts in reverse order for correct multi-field precedence
571
661
  for (let i = sort.length - 1; i >= 0; i--) {
572
662
  const sortItem = sort[i];
573
663
 
@@ -593,8 +683,8 @@ export class MemoryDriver implements Driver, DriverInterface {
593
683
  if (bVal == null) return -1;
594
684
 
595
685
  // Compare values
596
- if (aVal < bVal) return direction === 'asc' ? -1 : 1;
597
- if (aVal > bVal) return direction === 'asc' ? 1 : -1;
686
+ if (aVal < bVal) return direction.toLowerCase() === 'desc' ? 1 : -1;
687
+ if (aVal > bVal) return direction.toLowerCase() === 'desc' ? -1 : 1;
598
688
  return 0;
599
689
  });
600
690
  }
@@ -640,17 +730,8 @@ export class MemoryDriver implements Driver, DriverInterface {
640
730
  async executeQuery(ast: QueryAST, options?: any): Promise<{ value: any[]; count?: number }> {
641
731
  const objectName = ast.object || '';
642
732
 
643
- // Convert QueryAST to legacy query format
644
- const legacyQuery: any = {
645
- fields: ast.fields,
646
- filters: this.convertFilterNodeToLegacy(ast.filters),
647
- sort: ast.sort?.map((s: SortNode) => [s.field, s.order]),
648
- limit: ast.top,
649
- offset: ast.skip,
650
- };
651
-
652
- // Use existing find method
653
- const results = await this.find(objectName, legacyQuery, options);
733
+ // Use existing find method with QueryAST directly
734
+ const results = await this.find(objectName, ast, options);
654
735
 
655
736
  return {
656
737
  value: results,
@@ -762,63 +843,6 @@ export class MemoryDriver implements Driver, DriverInterface {
762
843
  }
763
844
  }
764
845
 
765
- /**
766
- * Convert FilterNode (QueryAST format) to legacy filter array format
767
- * This allows reuse of existing filter logic while supporting new QueryAST
768
- *
769
- * @private
770
- */
771
- private convertFilterNodeToLegacy(node?: FilterNode): any {
772
- if (!node) return undefined;
773
-
774
- switch (node.type) {
775
- case 'comparison':
776
- // Convert comparison node to [field, operator, value] format
777
- const operator = node.operator || '=';
778
- return [[node.field, operator, node.value]];
779
-
780
- case 'and':
781
- // Convert AND node to array with 'and' separator
782
- if (!node.children || node.children.length === 0) return undefined;
783
- const andResults: any[] = [];
784
- for (const child of node.children) {
785
- const converted = this.convertFilterNodeToLegacy(child);
786
- if (converted) {
787
- if (andResults.length > 0) {
788
- andResults.push('and');
789
- }
790
- andResults.push(...(Array.isArray(converted) ? converted : [converted]));
791
- }
792
- }
793
- return andResults.length > 0 ? andResults : undefined;
794
-
795
- case 'or':
796
- // Convert OR node to array with 'or' separator
797
- if (!node.children || node.children.length === 0) return undefined;
798
- const orResults: any[] = [];
799
- for (const child of node.children) {
800
- const converted = this.convertFilterNodeToLegacy(child);
801
- if (converted) {
802
- if (orResults.length > 0) {
803
- orResults.push('or');
804
- }
805
- orResults.push(...(Array.isArray(converted) ? converted : [converted]));
806
- }
807
- }
808
- return orResults.length > 0 ? orResults : undefined;
809
-
810
- case 'not':
811
- // NOT is complex - we'll just process the first child for now
812
- if (node.children && node.children.length > 0) {
813
- return this.convertFilterNodeToLegacy(node.children[0]);
814
- }
815
- return undefined;
816
-
817
- default:
818
- return undefined;
819
- }
820
- }
821
-
822
846
  /**
823
847
  * Execute command (alternative signature for compatibility)
824
848
  *