@objectql/driver-memory 3.0.1 → 4.0.1

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,14 +1,27 @@
1
1
  "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MemoryDriver = void 0;
4
+ /**
5
+ * ObjectQL
6
+ * Copyright (c) 2026-present ObjectStack Inc.
7
+ *
8
+ * This source code is licensed under the MIT license found in the
9
+ * LICENSE file in the root directory of this source tree.
10
+ */
2
11
  /**
3
12
  * Memory Driver for ObjectQL (Production-Ready)
4
13
  *
5
- * 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.
6
15
  * Perfect for testing, development, and environments where persistence is not required.
7
16
  *
17
+ * Implements the Driver interface from @objectql/types which includes all methods
18
+ * from the standard DriverInterface from @objectstack/spec for full compatibility
19
+ * with the new kernel-based plugin system.
20
+ *
8
21
  * ✅ Production-ready features:
9
- * - Zero external dependencies
22
+ * - MongoDB-like query engine powered by Mingo
10
23
  * - Thread-safe operations
11
- * - Full query support (filters, sorting, pagination)
24
+ * - Full query support (filters, sorting, pagination, aggregation)
12
25
  * - Atomic transactions
13
26
  * - High performance (no I/O overhead)
14
27
  *
@@ -18,10 +31,11 @@
18
31
  * - Edge/Worker environments (Cloudflare Workers, Deno Deploy)
19
32
  * - Client-side state management
20
33
  * - Temporary data caching
34
+ *
35
+ * @version 4.0.0 - DriverInterface compliant with Mingo integration
21
36
  */
22
- Object.defineProperty(exports, "__esModule", { value: true });
23
- exports.MemoryDriver = void 0;
24
37
  const types_1 = require("@objectql/types");
38
+ const mingo_1 = require("mingo");
25
39
  /**
26
40
  * Memory Driver Implementation
27
41
  *
@@ -32,6 +46,22 @@ const types_1 = require("@objectql/types");
32
46
  */
33
47
  class MemoryDriver {
34
48
  constructor(config = {}) {
49
+ // Driver metadata (ObjectStack-compatible)
50
+ this.name = 'MemoryDriver';
51
+ this.version = '4.0.0';
52
+ this.supports = {
53
+ transactions: false,
54
+ joins: false,
55
+ fullTextSearch: false,
56
+ jsonFields: true,
57
+ arrayFields: true,
58
+ queryFilters: true,
59
+ queryAggregations: false,
60
+ querySorting: true,
61
+ queryPagination: true,
62
+ queryWindowFunctions: false,
63
+ querySubqueries: false
64
+ };
35
65
  this.config = config;
36
66
  this.store = new Map();
37
67
  this.idCounters = new Map();
@@ -40,6 +70,20 @@ class MemoryDriver {
40
70
  this.loadInitialData(config.initialData);
41
71
  }
42
72
  }
73
+ /**
74
+ * Connect to the database (for DriverInterface compatibility)
75
+ * This is a no-op for memory driver as there's no external connection.
76
+ */
77
+ async connect() {
78
+ // No-op: Memory driver doesn't need connection
79
+ }
80
+ /**
81
+ * Check database connection health
82
+ */
83
+ async checkHealth() {
84
+ // Memory driver is always healthy if it exists
85
+ return true;
86
+ }
43
87
  /**
44
88
  * Load initial data into the store.
45
89
  */
@@ -54,37 +98,42 @@ class MemoryDriver {
54
98
  }
55
99
  /**
56
100
  * Find multiple records matching the query criteria.
57
- * Supports filtering, sorting, pagination, and field projection.
101
+ * Supports filtering, sorting, pagination, and field projection using Mingo.
58
102
  */
59
103
  async find(objectName, query = {}, options) {
104
+ // Normalize query to support both legacy and QueryAST formats
105
+ const normalizedQuery = this.normalizeQuery(query);
60
106
  // Get all records for this object type
61
107
  const pattern = `${objectName}:`;
62
- let results = [];
108
+ let records = [];
63
109
  for (const [key, value] of this.store.entries()) {
64
110
  if (key.startsWith(pattern)) {
65
- results.push({ ...value });
111
+ records.push({ ...value });
66
112
  }
67
113
  }
68
- // Apply filters
69
- if (query.filters) {
70
- results = this.applyFilters(results, query.filters);
114
+ // Convert ObjectQL filters to MongoDB query format
115
+ const mongoQuery = this.convertToMongoQuery(normalizedQuery.filters);
116
+ // Apply filters using Mingo
117
+ if (mongoQuery && Object.keys(mongoQuery).length > 0) {
118
+ const mingoQuery = new mingo_1.Query(mongoQuery);
119
+ records = mingoQuery.find(records).all();
71
120
  }
72
- // Apply sorting
73
- if (query.sort && Array.isArray(query.sort)) {
74
- results = this.applySort(results, query.sort);
121
+ // Apply sorting manually (Mingo's sort has issues with CJS builds)
122
+ if (normalizedQuery.sort && Array.isArray(normalizedQuery.sort) && normalizedQuery.sort.length > 0) {
123
+ records = this.applyManualSort(records, normalizedQuery.sort);
75
124
  }
76
125
  // Apply pagination
77
- if (query.skip) {
78
- results = results.slice(query.skip);
126
+ if (normalizedQuery.skip) {
127
+ records = records.slice(normalizedQuery.skip);
79
128
  }
80
- if (query.limit) {
81
- results = results.slice(0, query.limit);
129
+ if (normalizedQuery.limit) {
130
+ records = records.slice(0, normalizedQuery.limit);
82
131
  }
83
132
  // Apply field projection
84
- if (query.fields && Array.isArray(query.fields)) {
85
- results = results.map(doc => this.projectFields(doc, query.fields));
133
+ if (normalizedQuery.fields && Array.isArray(normalizedQuery.fields)) {
134
+ records = records.map(doc => this.projectFields(doc, normalizedQuery.fields));
86
135
  }
87
- return results;
136
+ return records;
88
137
  }
89
138
  /**
90
139
  * Find a single record by ID or query.
@@ -170,49 +219,61 @@ class MemoryDriver {
170
219
  return deleted;
171
220
  }
172
221
  /**
173
- * Count records matching filters.
222
+ * Count records matching filters using Mingo.
174
223
  */
175
224
  async count(objectName, filters, options) {
176
225
  const pattern = `${objectName}:`;
177
- let count = 0;
178
226
  // Extract actual filters from query object if needed
179
227
  let actualFilters = filters;
180
228
  if (filters && !Array.isArray(filters) && filters.filters) {
181
229
  actualFilters = filters.filters;
182
230
  }
183
- // If no filters, return total count
184
- if (!actualFilters || (Array.isArray(actualFilters) && actualFilters.length === 0)) {
185
- for (const key of this.store.keys()) {
186
- if (key.startsWith(pattern)) {
187
- count++;
188
- }
189
- }
190
- return count;
191
- }
192
- // Count only records matching filters
231
+ // Get all records for this object type
232
+ let records = [];
193
233
  for (const [key, value] of this.store.entries()) {
194
234
  if (key.startsWith(pattern)) {
195
- if (this.matchesFilters(value, actualFilters)) {
196
- count++;
197
- }
235
+ records.push(value);
198
236
  }
199
237
  }
200
- return count;
238
+ // If no filters, return total count
239
+ if (!actualFilters || (Array.isArray(actualFilters) && actualFilters.length === 0)) {
240
+ return records.length;
241
+ }
242
+ // Convert to MongoDB query and use Mingo to count
243
+ const mongoQuery = this.convertToMongoQuery(actualFilters);
244
+ if (mongoQuery && Object.keys(mongoQuery).length > 0) {
245
+ const mingoQuery = new mingo_1.Query(mongoQuery);
246
+ const matchedRecords = mingoQuery.find(records).all();
247
+ return matchedRecords.length;
248
+ }
249
+ return records.length;
201
250
  }
202
251
  /**
203
- * Get distinct values for a field.
252
+ * Get distinct values for a field using Mingo.
204
253
  */
205
254
  async distinct(objectName, field, filters, options) {
206
255
  const pattern = `${objectName}:`;
207
- const values = new Set();
208
- for (const [key, record] of this.store.entries()) {
256
+ // Get all records for this object type
257
+ let records = [];
258
+ for (const [key, value] of this.store.entries()) {
209
259
  if (key.startsWith(pattern)) {
210
- if (!filters || this.matchesFilters(record, filters)) {
211
- const value = record[field];
212
- if (value !== undefined && value !== null) {
213
- values.add(value);
214
- }
215
- }
260
+ records.push(value);
261
+ }
262
+ }
263
+ // Apply filters using Mingo if provided
264
+ if (filters) {
265
+ const mongoQuery = this.convertToMongoQuery(filters);
266
+ if (mongoQuery && Object.keys(mongoQuery).length > 0) {
267
+ const mingoQuery = new mingo_1.Query(mongoQuery);
268
+ records = mingoQuery.find(records).all();
269
+ }
270
+ }
271
+ // Extract distinct values
272
+ const values = new Set();
273
+ for (const record of records) {
274
+ const value = record[field];
275
+ if (value !== undefined && value !== null) {
276
+ values.add(value);
216
277
  }
217
278
  }
218
279
  return Array.from(values);
@@ -229,45 +290,73 @@ class MemoryDriver {
229
290
  return results;
230
291
  }
231
292
  /**
232
- * Update multiple records matching filters.
293
+ * Update multiple records matching filters using Mingo.
233
294
  */
234
295
  async updateMany(objectName, filters, data, options) {
235
296
  const pattern = `${objectName}:`;
236
- let count = 0;
297
+ // Get all records for this object type
298
+ let records = [];
299
+ const recordKeys = new Map();
237
300
  for (const [key, record] of this.store.entries()) {
238
301
  if (key.startsWith(pattern)) {
239
- if (this.matchesFilters(record, filters)) {
240
- const updated = {
241
- ...record,
242
- ...data,
243
- id: record.id, // Preserve ID
244
- created_at: record.created_at, // Preserve created_at
245
- updated_at: new Date().toISOString()
246
- };
247
- this.store.set(key, updated);
248
- count++;
249
- }
302
+ records.push(record);
303
+ recordKeys.set(record.id, key);
304
+ }
305
+ }
306
+ // Apply filters using Mingo
307
+ const mongoQuery = this.convertToMongoQuery(filters);
308
+ let matchedRecords = records;
309
+ if (mongoQuery && Object.keys(mongoQuery).length > 0) {
310
+ const mingoQuery = new mingo_1.Query(mongoQuery);
311
+ matchedRecords = mingoQuery.find(records).all();
312
+ }
313
+ // Update matched records
314
+ let count = 0;
315
+ for (const record of matchedRecords) {
316
+ const key = recordKeys.get(record.id);
317
+ if (key) {
318
+ const updated = {
319
+ ...record,
320
+ ...data,
321
+ id: record.id, // Preserve ID
322
+ created_at: record.created_at, // Preserve created_at
323
+ updated_at: new Date().toISOString()
324
+ };
325
+ this.store.set(key, updated);
326
+ count++;
250
327
  }
251
328
  }
252
329
  return { modifiedCount: count };
253
330
  }
254
331
  /**
255
- * Delete multiple records matching filters.
332
+ * Delete multiple records matching filters using Mingo.
256
333
  */
257
334
  async deleteMany(objectName, filters, options) {
258
335
  const pattern = `${objectName}:`;
259
- const keysToDelete = [];
336
+ // Get all records for this object type
337
+ let records = [];
338
+ const recordKeys = new Map();
260
339
  for (const [key, record] of this.store.entries()) {
261
340
  if (key.startsWith(pattern)) {
262
- if (this.matchesFilters(record, filters)) {
263
- keysToDelete.push(key);
264
- }
341
+ records.push(record);
342
+ recordKeys.set(record.id, key);
265
343
  }
266
344
  }
267
- for (const key of keysToDelete) {
268
- this.store.delete(key);
345
+ // Apply filters using Mingo
346
+ const mongoQuery = this.convertToMongoQuery(filters);
347
+ let matchedRecords = records;
348
+ if (mongoQuery && Object.keys(mongoQuery).length > 0) {
349
+ const mingoQuery = new mingo_1.Query(mongoQuery);
350
+ matchedRecords = mingoQuery.find(records).all();
351
+ }
352
+ // Delete matched records
353
+ for (const record of matchedRecords) {
354
+ const key = recordKeys.get(record.id);
355
+ if (key) {
356
+ this.store.delete(key);
357
+ }
269
358
  }
270
- return { deletedCount: keysToDelete.length };
359
+ return { deletedCount: matchedRecords.length };
271
360
  }
272
361
  /**
273
362
  * Clear all data from the store.
@@ -290,7 +379,36 @@ class MemoryDriver {
290
379
  }
291
380
  // ========== Helper Methods ==========
292
381
  /**
293
- * Apply filters to an array of records (in-memory filtering).
382
+ * Normalizes query format to support both legacy UnifiedQuery and QueryAST formats.
383
+ * This ensures backward compatibility while supporting the new @objectstack/spec interface.
384
+ *
385
+ * QueryAST format uses 'top' for limit, while UnifiedQuery uses 'limit'.
386
+ * QueryAST sort is array of {field, order}, while UnifiedQuery is array of [field, order].
387
+ */
388
+ normalizeQuery(query) {
389
+ if (!query)
390
+ return {};
391
+ const normalized = { ...query };
392
+ // Normalize limit/top
393
+ if (normalized.top !== undefined && normalized.limit === undefined) {
394
+ normalized.limit = normalized.top;
395
+ }
396
+ // Normalize sort format
397
+ if (normalized.sort && Array.isArray(normalized.sort)) {
398
+ // Check if it's already in the array format [field, order]
399
+ const firstSort = normalized.sort[0];
400
+ if (firstSort && typeof firstSort === 'object' && !Array.isArray(firstSort)) {
401
+ // Convert from QueryAST format {field, order} to internal format [field, order]
402
+ normalized.sort = normalized.sort.map((item) => [
403
+ item.field,
404
+ item.order || item.direction || item.dir || 'asc'
405
+ ]);
406
+ }
407
+ }
408
+ return normalized;
409
+ }
410
+ /**
411
+ * Convert ObjectQL filters to MongoDB query format for Mingo.
294
412
  *
295
413
  * Supports ObjectQL filter format:
296
414
  * [
@@ -298,95 +416,126 @@ class MemoryDriver {
298
416
  * 'or',
299
417
  * ['field2', 'operator', value2]
300
418
  * ]
419
+ *
420
+ * Converts to MongoDB query format:
421
+ * { $or: [{ field: { $operator: value }}, { field2: { $operator: value2 }}] }
301
422
  */
302
- applyFilters(records, filters) {
303
- if (!filters || filters.length === 0) {
304
- return records;
305
- }
306
- return records.filter(record => this.matchesFilters(record, filters));
307
- }
308
- /**
309
- * Check if a single record matches the filter conditions.
310
- */
311
- matchesFilters(record, filters) {
423
+ convertToMongoQuery(filters) {
312
424
  if (!filters || filters.length === 0) {
313
- return true;
425
+ return {};
314
426
  }
315
- let conditions = [];
316
- let operators = [];
427
+ // Process the filter array to build MongoDB query
428
+ const conditions = [];
429
+ let currentLogic = 'and';
430
+ const logicGroups = [
431
+ { logic: 'and', conditions: [] }
432
+ ];
317
433
  for (const item of filters) {
318
434
  if (typeof item === 'string') {
319
435
  // Logical operator (and/or)
320
- operators.push(item.toLowerCase());
436
+ const newLogic = item.toLowerCase();
437
+ if (newLogic !== currentLogic) {
438
+ currentLogic = newLogic;
439
+ logicGroups.push({ logic: currentLogic, conditions: [] });
440
+ }
321
441
  }
322
442
  else if (Array.isArray(item)) {
323
443
  const [field, operator, value] = item;
324
- // Handle nested filter groups
325
- if (typeof field !== 'string') {
326
- // Nested group - recursively evaluate
327
- conditions.push(this.matchesFilters(record, item));
444
+ // Convert single condition to MongoDB operator
445
+ const mongoCondition = this.convertConditionToMongo(field, operator, value);
446
+ if (mongoCondition) {
447
+ logicGroups[logicGroups.length - 1].conditions.push(mongoCondition);
448
+ }
449
+ }
450
+ }
451
+ // Build final query from logic groups
452
+ if (logicGroups.length === 1 && logicGroups[0].conditions.length === 1) {
453
+ return logicGroups[0].conditions[0];
454
+ }
455
+ // If there's only one group with multiple conditions, use its logic operator
456
+ if (logicGroups.length === 1) {
457
+ const group = logicGroups[0];
458
+ if (group.logic === 'or') {
459
+ return { $or: group.conditions };
460
+ }
461
+ else {
462
+ return { $and: group.conditions };
463
+ }
464
+ }
465
+ // Multiple groups - flatten all conditions and determine the top-level operator
466
+ const allConditions = [];
467
+ for (const group of logicGroups) {
468
+ if (group.conditions.length === 0)
469
+ continue;
470
+ if (group.conditions.length === 1) {
471
+ allConditions.push(group.conditions[0]);
472
+ }
473
+ else {
474
+ if (group.logic === 'or') {
475
+ allConditions.push({ $or: group.conditions });
328
476
  }
329
477
  else {
330
- // Single condition
331
- const matches = this.evaluateCondition(record[field], operator, value);
332
- conditions.push(matches);
478
+ allConditions.push({ $and: group.conditions });
333
479
  }
334
480
  }
335
481
  }
336
- // Combine conditions with operators
337
- if (conditions.length === 0) {
338
- return true;
482
+ if (allConditions.length === 0) {
483
+ return {};
339
484
  }
340
- let result = conditions[0];
341
- for (let i = 0; i < operators.length; i++) {
342
- const op = operators[i];
343
- const nextCondition = conditions[i + 1];
344
- if (op === 'or') {
345
- result = result || nextCondition;
485
+ else if (allConditions.length === 1) {
486
+ return allConditions[0];
487
+ }
488
+ else {
489
+ // Determine top-level operator: use OR if any non-empty group has OR logic
490
+ const hasOrLogic = logicGroups.some(g => g.logic === 'or' && g.conditions.length > 0);
491
+ if (hasOrLogic) {
492
+ return { $or: allConditions };
346
493
  }
347
- else { // 'and' or default
348
- result = result && nextCondition;
494
+ else {
495
+ return { $and: allConditions };
349
496
  }
350
497
  }
351
- return result;
352
498
  }
353
499
  /**
354
- * Evaluate a single filter condition.
500
+ * Convert a single ObjectQL condition to MongoDB operator format.
355
501
  */
356
- evaluateCondition(fieldValue, operator, compareValue) {
502
+ convertConditionToMongo(field, operator, value) {
357
503
  switch (operator) {
358
504
  case '=':
359
505
  case '==':
360
- return fieldValue === compareValue;
506
+ return { [field]: value };
361
507
  case '!=':
362
508
  case '<>':
363
- return fieldValue !== compareValue;
509
+ return { [field]: { $ne: value } };
364
510
  case '>':
365
- return fieldValue > compareValue;
511
+ return { [field]: { $gt: value } };
366
512
  case '>=':
367
- return fieldValue >= compareValue;
513
+ return { [field]: { $gte: value } };
368
514
  case '<':
369
- return fieldValue < compareValue;
515
+ return { [field]: { $lt: value } };
370
516
  case '<=':
371
- return fieldValue <= compareValue;
517
+ return { [field]: { $lte: value } };
372
518
  case 'in':
373
- return Array.isArray(compareValue) && compareValue.includes(fieldValue);
519
+ return { [field]: { $in: value } };
374
520
  case 'nin':
375
521
  case 'not in':
376
- return Array.isArray(compareValue) && !compareValue.includes(fieldValue);
522
+ return { [field]: { $nin: value } };
377
523
  case 'contains':
378
524
  case 'like':
379
- return String(fieldValue).toLowerCase().includes(String(compareValue).toLowerCase());
525
+ // MongoDB regex for case-insensitive contains
526
+ // Escape special regex characters to prevent ReDoS and ensure literal matching
527
+ return { [field]: { $regex: new RegExp(this.escapeRegex(value), 'i') } };
380
528
  case 'startswith':
381
529
  case 'starts_with':
382
- return String(fieldValue).toLowerCase().startsWith(String(compareValue).toLowerCase());
530
+ return { [field]: { $regex: new RegExp(`^${this.escapeRegex(value)}`, 'i') } };
383
531
  case 'endswith':
384
532
  case 'ends_with':
385
- return String(fieldValue).toLowerCase().endsWith(String(compareValue).toLowerCase());
533
+ return { [field]: { $regex: new RegExp(`${this.escapeRegex(value)}$`, 'i') } };
386
534
  case 'between':
387
- return Array.isArray(compareValue) &&
388
- fieldValue >= compareValue[0] &&
389
- fieldValue <= compareValue[1];
535
+ if (Array.isArray(value) && value.length === 2) {
536
+ return { [field]: { $gte: value[0], $lte: value[1] } };
537
+ }
538
+ return null;
390
539
  default:
391
540
  throw new types_1.ObjectQLError({
392
541
  code: 'UNSUPPORTED_OPERATOR',
@@ -395,11 +544,19 @@ class MemoryDriver {
395
544
  }
396
545
  }
397
546
  /**
398
- * Apply sorting to an array of records (in-memory sorting).
547
+ * Escape special regex characters to prevent ReDoS and ensure literal matching.
548
+ * This is crucial for security when using user input in regex patterns.
399
549
  */
400
- applySort(records, sort) {
550
+ escapeRegex(str) {
551
+ return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
552
+ }
553
+ /**
554
+ * Apply manual sorting to an array of records.
555
+ * This is used instead of Mingo's sort to avoid CJS build issues.
556
+ */
557
+ applyManualSort(records, sort) {
401
558
  const sorted = [...records];
402
- // Apply sorts in reverse order for correct precedence
559
+ // Apply sorts in reverse order for correct multi-field precedence
403
560
  for (let i = sort.length - 1; i >= 0; i--) {
404
561
  const sortItem = sort[i];
405
562
  let field;
@@ -426,9 +583,9 @@ class MemoryDriver {
426
583
  return -1;
427
584
  // Compare values
428
585
  if (aVal < bVal)
429
- return direction === 'asc' ? -1 : 1;
586
+ return direction.toLowerCase() === 'desc' ? 1 : -1;
430
587
  if (aVal > bVal)
431
- return direction === 'asc' ? 1 : -1;
588
+ return direction.toLowerCase() === 'desc' ? -1 : 1;
432
589
  return 0;
433
590
  });
434
591
  }
@@ -456,6 +613,198 @@ class MemoryDriver {
456
613
  const timestamp = Date.now();
457
614
  return `${objectName}-${timestamp}-${counter}`;
458
615
  }
616
+ /**
617
+ * Execute a query using QueryAST (DriverInterface v4.0 method)
618
+ *
619
+ * This is the new standard method for query execution using the
620
+ * ObjectStack QueryAST format.
621
+ *
622
+ * @param ast - The QueryAST representing the query
623
+ * @param options - Optional execution options
624
+ * @returns Query results with value and count
625
+ */
626
+ async executeQuery(ast, options) {
627
+ var _a;
628
+ const objectName = ast.object || '';
629
+ // Convert QueryAST to legacy query format
630
+ const legacyQuery = {
631
+ fields: ast.fields,
632
+ filters: this.convertFilterNodeToLegacy(ast.filters),
633
+ sort: (_a = ast.sort) === null || _a === void 0 ? void 0 : _a.map((s) => [s.field, s.order]),
634
+ limit: ast.top,
635
+ offset: ast.skip,
636
+ };
637
+ // Use existing find method
638
+ const results = await this.find(objectName, legacyQuery, options);
639
+ return {
640
+ value: results,
641
+ count: results.length
642
+ };
643
+ }
644
+ /**
645
+ * Execute a command (DriverInterface v4.0 method)
646
+ *
647
+ * This method handles all mutation operations (create, update, delete)
648
+ * using a unified command interface.
649
+ *
650
+ * @param command - The command to execute
651
+ * @param parameters - Optional command parameters (unused in this driver)
652
+ * @param options - Optional execution options
653
+ * @returns Command execution result
654
+ */
655
+ async executeCommand(command, options) {
656
+ try {
657
+ const cmdOptions = { ...options, ...command.options };
658
+ switch (command.type) {
659
+ case 'create':
660
+ if (!command.data) {
661
+ throw new Error('Create command requires data');
662
+ }
663
+ const created = await this.create(command.object, command.data, cmdOptions);
664
+ return {
665
+ success: true,
666
+ data: created,
667
+ affected: 1
668
+ };
669
+ case 'update':
670
+ if (!command.id || !command.data) {
671
+ throw new Error('Update command requires id and data');
672
+ }
673
+ const updated = await this.update(command.object, command.id, command.data, cmdOptions);
674
+ return {
675
+ success: true,
676
+ data: updated,
677
+ affected: 1
678
+ };
679
+ case 'delete':
680
+ if (!command.id) {
681
+ throw new Error('Delete command requires id');
682
+ }
683
+ await this.delete(command.object, command.id, cmdOptions);
684
+ return {
685
+ success: true,
686
+ affected: 1
687
+ };
688
+ case 'bulkCreate':
689
+ if (!command.records || !Array.isArray(command.records)) {
690
+ throw new Error('BulkCreate command requires records array');
691
+ }
692
+ const bulkCreated = [];
693
+ for (const record of command.records) {
694
+ const created = await this.create(command.object, record, cmdOptions);
695
+ bulkCreated.push(created);
696
+ }
697
+ return {
698
+ success: true,
699
+ data: bulkCreated,
700
+ affected: command.records.length
701
+ };
702
+ case 'bulkUpdate':
703
+ if (!command.updates || !Array.isArray(command.updates)) {
704
+ throw new Error('BulkUpdate command requires updates array');
705
+ }
706
+ const updateResults = [];
707
+ for (const update of command.updates) {
708
+ const result = await this.update(command.object, update.id, update.data, cmdOptions);
709
+ updateResults.push(result);
710
+ }
711
+ return {
712
+ success: true,
713
+ data: updateResults,
714
+ affected: command.updates.length
715
+ };
716
+ case 'bulkDelete':
717
+ if (!command.ids || !Array.isArray(command.ids)) {
718
+ throw new Error('BulkDelete command requires ids array');
719
+ }
720
+ let deleted = 0;
721
+ for (const id of command.ids) {
722
+ const result = await this.delete(command.object, id, cmdOptions);
723
+ if (result)
724
+ deleted++;
725
+ }
726
+ return {
727
+ success: true,
728
+ affected: deleted
729
+ };
730
+ default:
731
+ throw new Error(`Unknown command type: ${command.type}`);
732
+ }
733
+ }
734
+ catch (error) {
735
+ return {
736
+ success: false,
737
+ error: error.message || 'Command execution failed',
738
+ affected: 0
739
+ };
740
+ }
741
+ }
742
+ /**
743
+ * Convert FilterNode (QueryAST format) to legacy filter array format
744
+ * This allows reuse of existing filter logic while supporting new QueryAST
745
+ *
746
+ * @private
747
+ */
748
+ convertFilterNodeToLegacy(node) {
749
+ if (!node)
750
+ return undefined;
751
+ switch (node.type) {
752
+ case 'comparison':
753
+ // Convert comparison node to [field, operator, value] format
754
+ const operator = node.operator || '=';
755
+ return [[node.field, operator, node.value]];
756
+ case 'and':
757
+ // Convert AND node to array with 'and' separator
758
+ if (!node.children || node.children.length === 0)
759
+ return undefined;
760
+ const andResults = [];
761
+ for (const child of node.children) {
762
+ const converted = this.convertFilterNodeToLegacy(child);
763
+ if (converted) {
764
+ if (andResults.length > 0) {
765
+ andResults.push('and');
766
+ }
767
+ andResults.push(...(Array.isArray(converted) ? converted : [converted]));
768
+ }
769
+ }
770
+ return andResults.length > 0 ? andResults : undefined;
771
+ case 'or':
772
+ // Convert OR node to array with 'or' separator
773
+ if (!node.children || node.children.length === 0)
774
+ return undefined;
775
+ const orResults = [];
776
+ for (const child of node.children) {
777
+ const converted = this.convertFilterNodeToLegacy(child);
778
+ if (converted) {
779
+ if (orResults.length > 0) {
780
+ orResults.push('or');
781
+ }
782
+ orResults.push(...(Array.isArray(converted) ? converted : [converted]));
783
+ }
784
+ }
785
+ return orResults.length > 0 ? orResults : undefined;
786
+ case 'not':
787
+ // NOT is complex - we'll just process the first child for now
788
+ if (node.children && node.children.length > 0) {
789
+ return this.convertFilterNodeToLegacy(node.children[0]);
790
+ }
791
+ return undefined;
792
+ default:
793
+ return undefined;
794
+ }
795
+ }
796
+ /**
797
+ * Execute command (alternative signature for compatibility)
798
+ *
799
+ * @param command - Command string or object
800
+ * @param parameters - Command parameters
801
+ * @param options - Execution options
802
+ */
803
+ async execute(command, parameters, options) {
804
+ // For memory driver, this is primarily for compatibility
805
+ // We don't support raw SQL/commands
806
+ throw new Error('Memory driver does not support raw command execution. Use executeCommand() instead.');
807
+ }
459
808
  }
460
809
  exports.MemoryDriver = MemoryDriver;
461
810
  //# sourceMappingURL=index.js.map