@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/dist/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MemoryDriver = void 0;
2
4
  /**
3
5
  * ObjectQL
4
6
  * Copyright (c) 2026-present ObjectStack Inc.
@@ -6,22 +8,20 @@
6
8
  * This source code is licensed under the MIT license found in the
7
9
  * LICENSE file in the root directory of this source tree.
8
10
  */
9
- Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.MemoryDriver = void 0;
11
11
  /**
12
12
  * Memory Driver for ObjectQL (Production-Ready)
13
13
  *
14
- * A high-performance in-memory driver for ObjectQL that stores data in JavaScript Maps.
14
+ * A high-performance in-memory driver for ObjectQL powered by Mingo.
15
15
  * Perfect for testing, development, and environments where persistence is not required.
16
16
  *
17
- * Implements both the legacy Driver interface from @objectql/types and
18
- * the standard DriverInterface from @objectstack/spec for full compatibility
17
+ * Implements the Driver interface from @objectql/types which includes all methods
18
+ * from the standard DriverInterface from @objectstack/spec for full compatibility
19
19
  * with the new kernel-based plugin system.
20
20
  *
21
21
  * ✅ Production-ready features:
22
- * - Zero external dependencies
22
+ * - MongoDB-like query engine powered by Mingo
23
23
  * - Thread-safe operations
24
- * - Full query support (filters, sorting, pagination)
24
+ * - Full query support (filters, sorting, pagination, aggregation)
25
25
  * - Atomic transactions
26
26
  * - High performance (no I/O overhead)
27
27
  *
@@ -32,9 +32,10 @@ exports.MemoryDriver = void 0;
32
32
  * - Client-side state management
33
33
  * - Temporary data caching
34
34
  *
35
- * @version 4.0.0 - DriverInterface compliant
35
+ * @version 4.0.0 - DriverInterface compliant with Mingo integration
36
36
  */
37
37
  const types_1 = require("@objectql/types");
38
+ const mingo_1 = require("mingo");
38
39
  /**
39
40
  * Memory Driver Implementation
40
41
  *
@@ -53,7 +54,13 @@ class MemoryDriver {
53
54
  joins: false,
54
55
  fullTextSearch: false,
55
56
  jsonFields: true,
56
- arrayFields: true
57
+ arrayFields: true,
58
+ queryFilters: true,
59
+ queryAggregations: false,
60
+ querySorting: true,
61
+ queryPagination: true,
62
+ queryWindowFunctions: false,
63
+ querySubqueries: false
57
64
  };
58
65
  this.config = config;
59
66
  this.store = new Map();
@@ -91,39 +98,40 @@ class MemoryDriver {
91
98
  }
92
99
  /**
93
100
  * Find multiple records matching the query criteria.
94
- * Supports filtering, sorting, pagination, and field projection.
101
+ * Supports filtering, sorting, pagination, and field projection using Mingo.
95
102
  */
96
103
  async find(objectName, query = {}, options) {
97
- // Normalize query to support both legacy and QueryAST formats
98
- const normalizedQuery = this.normalizeQuery(query);
99
104
  // Get all records for this object type
100
105
  const pattern = `${objectName}:`;
101
- let results = [];
106
+ let records = [];
102
107
  for (const [key, value] of this.store.entries()) {
103
108
  if (key.startsWith(pattern)) {
104
- results.push({ ...value });
109
+ records.push({ ...value });
105
110
  }
106
111
  }
107
- // Apply filters
108
- if (normalizedQuery.filters) {
109
- results = this.applyFilters(results, normalizedQuery.filters);
112
+ // Convert ObjectQL filters to MongoDB query format
113
+ const mongoQuery = this.convertToMongoQuery(query.where);
114
+ // Apply filters using Mingo
115
+ if (mongoQuery && Object.keys(mongoQuery).length > 0) {
116
+ const mingoQuery = new mingo_1.Query(mongoQuery);
117
+ records = mingoQuery.find(records).all();
110
118
  }
111
- // Apply sorting
112
- if (normalizedQuery.sort && Array.isArray(normalizedQuery.sort)) {
113
- results = this.applySort(results, normalizedQuery.sort);
119
+ // Apply sorting manually (Mingo's sort has issues with CJS builds)
120
+ if (query.orderBy && Array.isArray(query.orderBy) && query.orderBy.length > 0) {
121
+ records = this.applyManualSort(records, query.orderBy);
114
122
  }
115
123
  // Apply pagination
116
- if (normalizedQuery.skip) {
117
- results = results.slice(normalizedQuery.skip);
124
+ if (query.offset) {
125
+ records = records.slice(query.offset);
118
126
  }
119
- if (normalizedQuery.limit) {
120
- results = results.slice(0, normalizedQuery.limit);
127
+ if (query.limit) {
128
+ records = records.slice(0, query.limit);
121
129
  }
122
130
  // Apply field projection
123
- if (normalizedQuery.fields && Array.isArray(normalizedQuery.fields)) {
124
- results = results.map(doc => this.projectFields(doc, normalizedQuery.fields));
131
+ if (query.fields && Array.isArray(query.fields)) {
132
+ records = records.map(doc => this.projectFields(doc, query.fields));
125
133
  }
126
- return results;
134
+ return records;
127
135
  }
128
136
  /**
129
137
  * Find a single record by ID or query.
@@ -209,49 +217,61 @@ class MemoryDriver {
209
217
  return deleted;
210
218
  }
211
219
  /**
212
- * Count records matching filters.
220
+ * Count records matching filters using Mingo.
213
221
  */
214
222
  async count(objectName, filters, options) {
215
223
  const pattern = `${objectName}:`;
216
- let count = 0;
217
- // Extract actual filters from query object if needed
218
- let actualFilters = filters;
219
- if (filters && !Array.isArray(filters) && filters.filters) {
220
- actualFilters = filters.filters;
221
- }
222
- // If no filters, return total count
223
- if (!actualFilters || (Array.isArray(actualFilters) && actualFilters.length === 0)) {
224
- for (const key of this.store.keys()) {
225
- if (key.startsWith(pattern)) {
226
- count++;
227
- }
228
- }
229
- return count;
224
+ // Extract where condition from query object if needed
225
+ let whereCondition = filters;
226
+ if (filters && !Array.isArray(filters) && filters.where) {
227
+ whereCondition = filters.where;
230
228
  }
231
- // Count only records matching filters
229
+ // Get all records for this object type
230
+ let records = [];
232
231
  for (const [key, value] of this.store.entries()) {
233
232
  if (key.startsWith(pattern)) {
234
- if (this.matchesFilters(value, actualFilters)) {
235
- count++;
236
- }
233
+ records.push(value);
237
234
  }
238
235
  }
239
- return count;
236
+ // If no filters, return total count
237
+ if (!whereCondition || (Array.isArray(whereCondition) && whereCondition.length === 0)) {
238
+ return records.length;
239
+ }
240
+ // Convert to MongoDB query and use Mingo to count
241
+ const mongoQuery = this.convertToMongoQuery(whereCondition);
242
+ if (mongoQuery && Object.keys(mongoQuery).length > 0) {
243
+ const mingoQuery = new mingo_1.Query(mongoQuery);
244
+ const matchedRecords = mingoQuery.find(records).all();
245
+ return matchedRecords.length;
246
+ }
247
+ return records.length;
240
248
  }
241
249
  /**
242
- * Get distinct values for a field.
250
+ * Get distinct values for a field using Mingo.
243
251
  */
244
252
  async distinct(objectName, field, filters, options) {
245
253
  const pattern = `${objectName}:`;
246
- const values = new Set();
247
- for (const [key, record] of this.store.entries()) {
254
+ // Get all records for this object type
255
+ let records = [];
256
+ for (const [key, value] of this.store.entries()) {
248
257
  if (key.startsWith(pattern)) {
249
- if (!filters || this.matchesFilters(record, filters)) {
250
- const value = record[field];
251
- if (value !== undefined && value !== null) {
252
- values.add(value);
253
- }
254
- }
258
+ records.push(value);
259
+ }
260
+ }
261
+ // Apply filters using Mingo if provided
262
+ if (filters) {
263
+ const mongoQuery = this.convertToMongoQuery(filters);
264
+ if (mongoQuery && Object.keys(mongoQuery).length > 0) {
265
+ const mingoQuery = new mingo_1.Query(mongoQuery);
266
+ records = mingoQuery.find(records).all();
267
+ }
268
+ }
269
+ // Extract distinct values
270
+ const values = new Set();
271
+ for (const record of records) {
272
+ const value = record[field];
273
+ if (value !== undefined && value !== null) {
274
+ values.add(value);
255
275
  }
256
276
  }
257
277
  return Array.from(values);
@@ -268,45 +288,73 @@ class MemoryDriver {
268
288
  return results;
269
289
  }
270
290
  /**
271
- * Update multiple records matching filters.
291
+ * Update multiple records matching filters using Mingo.
272
292
  */
273
293
  async updateMany(objectName, filters, data, options) {
274
294
  const pattern = `${objectName}:`;
275
- let count = 0;
295
+ // Get all records for this object type
296
+ let records = [];
297
+ const recordKeys = new Map();
276
298
  for (const [key, record] of this.store.entries()) {
277
299
  if (key.startsWith(pattern)) {
278
- if (this.matchesFilters(record, filters)) {
279
- const updated = {
280
- ...record,
281
- ...data,
282
- id: record.id, // Preserve ID
283
- created_at: record.created_at, // Preserve created_at
284
- updated_at: new Date().toISOString()
285
- };
286
- this.store.set(key, updated);
287
- count++;
288
- }
300
+ records.push(record);
301
+ recordKeys.set(record.id, key);
302
+ }
303
+ }
304
+ // Apply filters using Mingo
305
+ const mongoQuery = this.convertToMongoQuery(filters);
306
+ let matchedRecords = records;
307
+ if (mongoQuery && Object.keys(mongoQuery).length > 0) {
308
+ const mingoQuery = new mingo_1.Query(mongoQuery);
309
+ matchedRecords = mingoQuery.find(records).all();
310
+ }
311
+ // Update matched records
312
+ let count = 0;
313
+ for (const record of matchedRecords) {
314
+ const key = recordKeys.get(record.id);
315
+ if (key) {
316
+ const updated = {
317
+ ...record,
318
+ ...data,
319
+ id: record.id, // Preserve ID
320
+ created_at: record.created_at, // Preserve created_at
321
+ updated_at: new Date().toISOString()
322
+ };
323
+ this.store.set(key, updated);
324
+ count++;
289
325
  }
290
326
  }
291
327
  return { modifiedCount: count };
292
328
  }
293
329
  /**
294
- * Delete multiple records matching filters.
330
+ * Delete multiple records matching filters using Mingo.
295
331
  */
296
332
  async deleteMany(objectName, filters, options) {
297
333
  const pattern = `${objectName}:`;
298
- const keysToDelete = [];
334
+ // Get all records for this object type
335
+ let records = [];
336
+ const recordKeys = new Map();
299
337
  for (const [key, record] of this.store.entries()) {
300
338
  if (key.startsWith(pattern)) {
301
- if (this.matchesFilters(record, filters)) {
302
- keysToDelete.push(key);
303
- }
339
+ records.push(record);
340
+ recordKeys.set(record.id, key);
304
341
  }
305
342
  }
306
- for (const key of keysToDelete) {
307
- this.store.delete(key);
343
+ // Apply filters using Mingo
344
+ const mongoQuery = this.convertToMongoQuery(filters);
345
+ let matchedRecords = records;
346
+ if (mongoQuery && Object.keys(mongoQuery).length > 0) {
347
+ const mingoQuery = new mingo_1.Query(mongoQuery);
348
+ matchedRecords = mingoQuery.find(records).all();
349
+ }
350
+ // Delete matched records
351
+ for (const record of matchedRecords) {
352
+ const key = recordKeys.get(record.id);
353
+ if (key) {
354
+ this.store.delete(key);
355
+ }
308
356
  }
309
- return { deletedCount: keysToDelete.length };
357
+ return { deletedCount: matchedRecords.length };
310
358
  }
311
359
  /**
312
360
  * Clear all data from the store.
@@ -335,126 +383,142 @@ class MemoryDriver {
335
383
  * QueryAST format uses 'top' for limit, while UnifiedQuery uses 'limit'.
336
384
  * QueryAST sort is array of {field, order}, while UnifiedQuery is array of [field, order].
337
385
  */
338
- normalizeQuery(query) {
339
- if (!query)
340
- return {};
341
- const normalized = { ...query };
342
- // Normalize limit/top
343
- if (normalized.top !== undefined && normalized.limit === undefined) {
344
- normalized.limit = normalized.top;
345
- }
346
- // Normalize sort format
347
- if (normalized.sort && Array.isArray(normalized.sort)) {
348
- // Check if it's already in the array format [field, order]
349
- const firstSort = normalized.sort[0];
350
- if (firstSort && typeof firstSort === 'object' && !Array.isArray(firstSort)) {
351
- // Convert from QueryAST format {field, order} to internal format [field, order]
352
- normalized.sort = normalized.sort.map((item) => [
353
- item.field,
354
- item.order || item.direction || item.dir || 'asc'
355
- ]);
356
- }
357
- }
358
- return normalized;
359
- }
360
386
  /**
361
- * Apply filters to an array of records (in-memory filtering).
387
+ * Convert ObjectQL filters to MongoDB query format for Mingo.
388
+ *
389
+ * Supports both:
390
+ * 1. Legacy ObjectQL filter format (array):
391
+ * [['field', 'operator', value], 'or', ['field2', 'operator', value2']]
392
+ * 2. New FilterCondition format (object - already MongoDB-like):
393
+ * { $and: [{ field: { $eq: value }}, { field2: { $gt: value2 }}] }
362
394
  *
363
- * Supports ObjectQL filter format:
364
- * [
365
- * ['field', 'operator', value],
366
- * 'or',
367
- * ['field2', 'operator', value2]
368
- * ]
395
+ * Converts to MongoDB query format:
396
+ * { $or: [{ field: { $operator: value }}, { field2: { $operator: value2 }}] }
369
397
  */
370
- applyFilters(records, filters) {
371
- if (!filters || filters.length === 0) {
372
- return records;
398
+ convertToMongoQuery(filters) {
399
+ if (!filters) {
400
+ return {};
373
401
  }
374
- return records.filter(record => this.matchesFilters(record, filters));
375
- }
376
- /**
377
- * Check if a single record matches the filter conditions.
378
- */
379
- matchesFilters(record, filters) {
380
- if (!filters || filters.length === 0) {
381
- return true;
402
+ // If filters is already an object (FilterCondition format), return it directly
403
+ if (!Array.isArray(filters)) {
404
+ return filters;
405
+ }
406
+ // Handle legacy array format
407
+ if (filters.length === 0) {
408
+ return {};
382
409
  }
383
- let conditions = [];
384
- let operators = [];
410
+ // Process the filter array to build MongoDB query
411
+ const conditions = [];
412
+ let currentLogic = 'and';
413
+ const logicGroups = [
414
+ { logic: 'and', conditions: [] }
415
+ ];
385
416
  for (const item of filters) {
386
417
  if (typeof item === 'string') {
387
418
  // Logical operator (and/or)
388
- operators.push(item.toLowerCase());
419
+ const newLogic = item.toLowerCase();
420
+ if (newLogic !== currentLogic) {
421
+ currentLogic = newLogic;
422
+ logicGroups.push({ logic: currentLogic, conditions: [] });
423
+ }
389
424
  }
390
425
  else if (Array.isArray(item)) {
391
426
  const [field, operator, value] = item;
392
- // Handle nested filter groups
393
- if (typeof field !== 'string') {
394
- // Nested group - recursively evaluate
395
- conditions.push(this.matchesFilters(record, item));
427
+ // Convert single condition to MongoDB operator
428
+ const mongoCondition = this.convertConditionToMongo(field, operator, value);
429
+ if (mongoCondition) {
430
+ logicGroups[logicGroups.length - 1].conditions.push(mongoCondition);
431
+ }
432
+ }
433
+ }
434
+ // Build final query from logic groups
435
+ if (logicGroups.length === 1 && logicGroups[0].conditions.length === 1) {
436
+ return logicGroups[0].conditions[0];
437
+ }
438
+ // If there's only one group with multiple conditions, use its logic operator
439
+ if (logicGroups.length === 1) {
440
+ const group = logicGroups[0];
441
+ if (group.logic === 'or') {
442
+ return { $or: group.conditions };
443
+ }
444
+ else {
445
+ return { $and: group.conditions };
446
+ }
447
+ }
448
+ // Multiple groups - flatten all conditions and determine the top-level operator
449
+ const allConditions = [];
450
+ for (const group of logicGroups) {
451
+ if (group.conditions.length === 0)
452
+ continue;
453
+ if (group.conditions.length === 1) {
454
+ allConditions.push(group.conditions[0]);
455
+ }
456
+ else {
457
+ if (group.logic === 'or') {
458
+ allConditions.push({ $or: group.conditions });
396
459
  }
397
460
  else {
398
- // Single condition
399
- const matches = this.evaluateCondition(record[field], operator, value);
400
- conditions.push(matches);
461
+ allConditions.push({ $and: group.conditions });
401
462
  }
402
463
  }
403
464
  }
404
- // Combine conditions with operators
405
- if (conditions.length === 0) {
406
- return true;
465
+ if (allConditions.length === 0) {
466
+ return {};
467
+ }
468
+ else if (allConditions.length === 1) {
469
+ return allConditions[0];
407
470
  }
408
- let result = conditions[0];
409
- for (let i = 0; i < operators.length; i++) {
410
- const op = operators[i];
411
- const nextCondition = conditions[i + 1];
412
- if (op === 'or') {
413
- result = result || nextCondition;
471
+ else {
472
+ // Determine top-level operator: use OR if any non-empty group has OR logic
473
+ const hasOrLogic = logicGroups.some(g => g.logic === 'or' && g.conditions.length > 0);
474
+ if (hasOrLogic) {
475
+ return { $or: allConditions };
414
476
  }
415
- else { // 'and' or default
416
- result = result && nextCondition;
477
+ else {
478
+ return { $and: allConditions };
417
479
  }
418
480
  }
419
- return result;
420
481
  }
421
482
  /**
422
- * Evaluate a single filter condition.
483
+ * Convert a single ObjectQL condition to MongoDB operator format.
423
484
  */
424
- evaluateCondition(fieldValue, operator, compareValue) {
485
+ convertConditionToMongo(field, operator, value) {
425
486
  switch (operator) {
426
487
  case '=':
427
488
  case '==':
428
- return fieldValue === compareValue;
489
+ return { [field]: value };
429
490
  case '!=':
430
491
  case '<>':
431
- return fieldValue !== compareValue;
492
+ return { [field]: { $ne: value } };
432
493
  case '>':
433
- return fieldValue > compareValue;
494
+ return { [field]: { $gt: value } };
434
495
  case '>=':
435
- return fieldValue >= compareValue;
496
+ return { [field]: { $gte: value } };
436
497
  case '<':
437
- return fieldValue < compareValue;
498
+ return { [field]: { $lt: value } };
438
499
  case '<=':
439
- return fieldValue <= compareValue;
500
+ return { [field]: { $lte: value } };
440
501
  case 'in':
441
- return Array.isArray(compareValue) && compareValue.includes(fieldValue);
502
+ return { [field]: { $in: value } };
442
503
  case 'nin':
443
504
  case 'not in':
444
- return Array.isArray(compareValue) && !compareValue.includes(fieldValue);
505
+ return { [field]: { $nin: value } };
445
506
  case 'contains':
446
507
  case 'like':
447
- return String(fieldValue).toLowerCase().includes(String(compareValue).toLowerCase());
508
+ // MongoDB regex for case-insensitive contains
509
+ // Escape special regex characters to prevent ReDoS and ensure literal matching
510
+ return { [field]: { $regex: new RegExp(this.escapeRegex(value), 'i') } };
448
511
  case 'startswith':
449
512
  case 'starts_with':
450
- return String(fieldValue).toLowerCase().startsWith(String(compareValue).toLowerCase());
513
+ return { [field]: { $regex: new RegExp(`^${this.escapeRegex(value)}`, 'i') } };
451
514
  case 'endswith':
452
515
  case 'ends_with':
453
- return String(fieldValue).toLowerCase().endsWith(String(compareValue).toLowerCase());
516
+ return { [field]: { $regex: new RegExp(`${this.escapeRegex(value)}$`, 'i') } };
454
517
  case 'between':
455
- return Array.isArray(compareValue) &&
456
- fieldValue >= compareValue[0] &&
457
- fieldValue <= compareValue[1];
518
+ if (Array.isArray(value) && value.length === 2) {
519
+ return { [field]: { $gte: value[0], $lte: value[1] } };
520
+ }
521
+ return null;
458
522
  default:
459
523
  throw new types_1.ObjectQLError({
460
524
  code: 'UNSUPPORTED_OPERATOR',
@@ -463,11 +527,19 @@ class MemoryDriver {
463
527
  }
464
528
  }
465
529
  /**
466
- * Apply sorting to an array of records (in-memory sorting).
530
+ * Escape special regex characters to prevent ReDoS and ensure literal matching.
531
+ * This is crucial for security when using user input in regex patterns.
467
532
  */
468
- applySort(records, sort) {
533
+ escapeRegex(str) {
534
+ return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
535
+ }
536
+ /**
537
+ * Apply manual sorting to an array of records.
538
+ * This is used instead of Mingo's sort to avoid CJS build issues.
539
+ */
540
+ applyManualSort(records, sort) {
469
541
  const sorted = [...records];
470
- // Apply sorts in reverse order for correct precedence
542
+ // Apply sorts in reverse order for correct multi-field precedence
471
543
  for (let i = sort.length - 1; i >= 0; i--) {
472
544
  const sortItem = sort[i];
473
545
  let field;
@@ -494,9 +566,9 @@ class MemoryDriver {
494
566
  return -1;
495
567
  // Compare values
496
568
  if (aVal < bVal)
497
- return direction === 'asc' ? -1 : 1;
569
+ return direction.toLowerCase() === 'desc' ? 1 : -1;
498
570
  if (aVal > bVal)
499
- return direction === 'asc' ? 1 : -1;
571
+ return direction.toLowerCase() === 'desc' ? -1 : 1;
500
572
  return 0;
501
573
  });
502
574
  }
@@ -535,18 +607,9 @@ class MemoryDriver {
535
607
  * @returns Query results with value and count
536
608
  */
537
609
  async executeQuery(ast, options) {
538
- var _a;
539
610
  const objectName = ast.object || '';
540
- // Convert QueryAST to legacy query format
541
- const legacyQuery = {
542
- fields: ast.fields,
543
- filters: this.convertFilterNodeToLegacy(ast.filters),
544
- sort: (_a = ast.sort) === null || _a === void 0 ? void 0 : _a.map((s) => [s.field, s.order]),
545
- limit: ast.top,
546
- offset: ast.skip,
547
- };
548
- // Use existing find method
549
- const results = await this.find(objectName, legacyQuery, options);
611
+ // Use existing find method with QueryAST directly
612
+ const results = await this.find(objectName, ast, options);
550
613
  return {
551
614
  value: results,
552
615
  count: results.length
@@ -650,60 +713,6 @@ class MemoryDriver {
650
713
  };
651
714
  }
652
715
  }
653
- /**
654
- * Convert FilterNode (QueryAST format) to legacy filter array format
655
- * This allows reuse of existing filter logic while supporting new QueryAST
656
- *
657
- * @private
658
- */
659
- convertFilterNodeToLegacy(node) {
660
- if (!node)
661
- return undefined;
662
- switch (node.type) {
663
- case 'comparison':
664
- // Convert comparison node to [field, operator, value] format
665
- const operator = node.operator || '=';
666
- return [[node.field, operator, node.value]];
667
- case 'and':
668
- // Convert AND node to array with 'and' separator
669
- if (!node.children || node.children.length === 0)
670
- return undefined;
671
- const andResults = [];
672
- for (const child of node.children) {
673
- const converted = this.convertFilterNodeToLegacy(child);
674
- if (converted) {
675
- if (andResults.length > 0) {
676
- andResults.push('and');
677
- }
678
- andResults.push(...(Array.isArray(converted) ? converted : [converted]));
679
- }
680
- }
681
- return andResults.length > 0 ? andResults : undefined;
682
- case 'or':
683
- // Convert OR node to array with 'or' separator
684
- if (!node.children || node.children.length === 0)
685
- return undefined;
686
- const orResults = [];
687
- for (const child of node.children) {
688
- const converted = this.convertFilterNodeToLegacy(child);
689
- if (converted) {
690
- if (orResults.length > 0) {
691
- orResults.push('or');
692
- }
693
- orResults.push(...(Array.isArray(converted) ? converted : [converted]));
694
- }
695
- }
696
- return orResults.length > 0 ? orResults : undefined;
697
- case 'not':
698
- // NOT is complex - we'll just process the first child for now
699
- if (node.children && node.children.length > 0) {
700
- return this.convertFilterNodeToLegacy(node.children[0]);
701
- }
702
- return undefined;
703
- default:
704
- return undefined;
705
- }
706
- }
707
716
  /**
708
717
  * Execute command (alternative signature for compatibility)
709
718
  *