@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/CHANGELOG.md +31 -3
- package/MIGRATION.md +23 -6
- package/MINGO_INTEGRATION.md +116 -0
- package/README.md +3 -3
- package/REFACTORING_SUMMARY.md +186 -0
- package/dist/index.d.ts +39 -35
- package/dist/index.js +234 -225
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
- package/src/index.ts +260 -236
- package/test/index.test.ts +15 -16
- package/tsconfig.tsbuildinfo +1 -1
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
|
|
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
|
|
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
|
-
* -
|
|
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 {
|
|
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
|
|
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
|
|
154
|
+
let records: any[] = [];
|
|
148
155
|
|
|
149
156
|
for (const [key, value] of this.store.entries()) {
|
|
150
157
|
if (key.startsWith(pattern)) {
|
|
151
|
-
|
|
158
|
+
records.push({ ...value });
|
|
152
159
|
}
|
|
153
160
|
}
|
|
154
161
|
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
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 (
|
|
162
|
-
|
|
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 (
|
|
167
|
-
|
|
177
|
+
if (query.offset) {
|
|
178
|
+
records = records.slice(query.offset);
|
|
168
179
|
}
|
|
169
|
-
if (
|
|
170
|
-
|
|
180
|
+
if (query.limit) {
|
|
181
|
+
records = records.slice(0, query.limit);
|
|
171
182
|
}
|
|
172
183
|
|
|
173
184
|
// Apply field projection
|
|
174
|
-
if (
|
|
175
|
-
|
|
185
|
+
if (query.fields && Array.isArray(query.fields)) {
|
|
186
|
+
records = records.map(doc => this.projectFields(doc, query.fields));
|
|
176
187
|
}
|
|
177
188
|
|
|
178
|
-
return
|
|
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
|
|
286
|
-
let
|
|
287
|
-
if (filters && !Array.isArray(filters) && filters.
|
|
288
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
}
|
|
430
|
+
records.push(record);
|
|
431
|
+
recordKeys.set(record.id, key);
|
|
384
432
|
}
|
|
385
433
|
}
|
|
386
434
|
|
|
387
|
-
|
|
388
|
-
|
|
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:
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
462
|
-
if (!filters
|
|
463
|
-
return
|
|
498
|
+
private convertToMongoQuery(filters?: any[] | Record<string, any>): Record<string, any> {
|
|
499
|
+
if (!filters) {
|
|
500
|
+
return {};
|
|
464
501
|
}
|
|
465
502
|
|
|
466
|
-
|
|
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
|
-
|
|
478
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
conditions.push(
|
|
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
|
-
//
|
|
500
|
-
if (conditions.length ===
|
|
501
|
-
return
|
|
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
|
-
|
|
505
|
-
|
|
506
|
-
const
|
|
507
|
-
|
|
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 (
|
|
510
|
-
|
|
511
|
-
} else {
|
|
512
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
586
|
+
* Convert a single ObjectQL condition to MongoDB operator format.
|
|
521
587
|
*/
|
|
522
|
-
private
|
|
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
|
|
592
|
+
return { [field]: value };
|
|
593
|
+
|
|
527
594
|
case '!=':
|
|
528
595
|
case '<>':
|
|
529
|
-
return
|
|
596
|
+
return { [field]: { $ne: value } };
|
|
597
|
+
|
|
530
598
|
case '>':
|
|
531
|
-
return
|
|
599
|
+
return { [field]: { $gt: value } };
|
|
600
|
+
|
|
532
601
|
case '>=':
|
|
533
|
-
return
|
|
602
|
+
return { [field]: { $gte: value } };
|
|
603
|
+
|
|
534
604
|
case '<':
|
|
535
|
-
return
|
|
605
|
+
return { [field]: { $lt: value } };
|
|
606
|
+
|
|
536
607
|
case '<=':
|
|
537
|
-
return
|
|
608
|
+
return { [field]: { $lte: value } };
|
|
609
|
+
|
|
538
610
|
case 'in':
|
|
539
|
-
return
|
|
611
|
+
return { [field]: { $in: value } };
|
|
612
|
+
|
|
540
613
|
case 'nin':
|
|
541
614
|
case 'not in':
|
|
542
|
-
return
|
|
615
|
+
return { [field]: { $nin: value } };
|
|
616
|
+
|
|
543
617
|
case 'contains':
|
|
544
618
|
case 'like':
|
|
545
|
-
|
|
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
|
|
625
|
+
return { [field]: { $regex: new RegExp(`^${this.escapeRegex(value)}`, 'i') } };
|
|
626
|
+
|
|
549
627
|
case 'endswith':
|
|
550
628
|
case 'ends_with':
|
|
551
|
-
return
|
|
629
|
+
return { [field]: { $regex: new RegExp(`${this.escapeRegex(value)}$`, 'i') } };
|
|
630
|
+
|
|
552
631
|
case 'between':
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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
|
-
*
|
|
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
|
|
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 === '
|
|
597
|
-
if (aVal > bVal) return direction === '
|
|
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
|
-
//
|
|
644
|
-
const
|
|
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
|
*
|