@objectql/driver-memory 4.0.0 → 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/CHANGELOG.md +17 -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 +34 -21
- package/dist/index.js +212 -123
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
- package/src/index.ts +237 -126
- package/tsconfig.tsbuildinfo +1 -1
package/src/index.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { Data, System } from '@objectstack/spec';
|
|
2
|
+
type QueryAST = Data.QueryAST;
|
|
3
|
+
type FilterNode = Data.FilterNode;
|
|
4
|
+
type SortNode = Data.SortNode;
|
|
5
|
+
type DriverInterface = System.DriverInterface;
|
|
1
6
|
/**
|
|
2
7
|
* ObjectQL
|
|
3
8
|
* Copyright (c) 2026-present ObjectStack Inc.
|
|
@@ -9,17 +14,17 @@
|
|
|
9
14
|
/**
|
|
10
15
|
* Memory Driver for ObjectQL (Production-Ready)
|
|
11
16
|
*
|
|
12
|
-
* A high-performance in-memory driver for ObjectQL
|
|
17
|
+
* A high-performance in-memory driver for ObjectQL powered by Mingo.
|
|
13
18
|
* Perfect for testing, development, and environments where persistence is not required.
|
|
14
19
|
*
|
|
15
|
-
* Implements
|
|
16
|
-
* the standard DriverInterface from @objectstack/spec for full compatibility
|
|
20
|
+
* Implements the Driver interface from @objectql/types which includes all methods
|
|
21
|
+
* from the standard DriverInterface from @objectstack/spec for full compatibility
|
|
17
22
|
* with the new kernel-based plugin system.
|
|
18
23
|
*
|
|
19
24
|
* ✅ Production-ready features:
|
|
20
|
-
* -
|
|
25
|
+
* - MongoDB-like query engine powered by Mingo
|
|
21
26
|
* - Thread-safe operations
|
|
22
|
-
* - Full query support (filters, sorting, pagination)
|
|
27
|
+
* - Full query support (filters, sorting, pagination, aggregation)
|
|
23
28
|
* - Atomic transactions
|
|
24
29
|
* - High performance (no I/O overhead)
|
|
25
30
|
*
|
|
@@ -30,11 +35,11 @@
|
|
|
30
35
|
* - Client-side state management
|
|
31
36
|
* - Temporary data caching
|
|
32
37
|
*
|
|
33
|
-
* @version 4.0.0 - DriverInterface compliant
|
|
38
|
+
* @version 4.0.0 - DriverInterface compliant with Mingo integration
|
|
34
39
|
*/
|
|
35
40
|
|
|
36
41
|
import { Driver, ObjectQLError } from '@objectql/types';
|
|
37
|
-
import {
|
|
42
|
+
import { Query } from 'mingo';
|
|
38
43
|
|
|
39
44
|
/**
|
|
40
45
|
* Command interface for executeCommand method
|
|
@@ -78,7 +83,7 @@ export interface MemoryDriverConfig {
|
|
|
78
83
|
*
|
|
79
84
|
* Example: `users:user-123` → `{id: "user-123", name: "Alice", ...}`
|
|
80
85
|
*/
|
|
81
|
-
export class MemoryDriver implements Driver
|
|
86
|
+
export class MemoryDriver implements Driver {
|
|
82
87
|
// Driver metadata (ObjectStack-compatible)
|
|
83
88
|
public readonly name = 'MemoryDriver';
|
|
84
89
|
public readonly version = '4.0.0';
|
|
@@ -87,7 +92,13 @@ export class MemoryDriver implements Driver, DriverInterface {
|
|
|
87
92
|
joins: false,
|
|
88
93
|
fullTextSearch: false,
|
|
89
94
|
jsonFields: true,
|
|
90
|
-
arrayFields: true
|
|
95
|
+
arrayFields: true,
|
|
96
|
+
queryFilters: true,
|
|
97
|
+
queryAggregations: false,
|
|
98
|
+
querySorting: true,
|
|
99
|
+
queryPagination: true,
|
|
100
|
+
queryWindowFunctions: false,
|
|
101
|
+
querySubqueries: false
|
|
91
102
|
};
|
|
92
103
|
|
|
93
104
|
private store: Map<string, any>;
|
|
@@ -136,7 +147,7 @@ export class MemoryDriver implements Driver, DriverInterface {
|
|
|
136
147
|
|
|
137
148
|
/**
|
|
138
149
|
* Find multiple records matching the query criteria.
|
|
139
|
-
* Supports filtering, sorting, pagination, and field projection.
|
|
150
|
+
* Supports filtering, sorting, pagination, and field projection using Mingo.
|
|
140
151
|
*/
|
|
141
152
|
async find(objectName: string, query: any = {}, options?: any): Promise<any[]> {
|
|
142
153
|
// Normalize query to support both legacy and QueryAST formats
|
|
@@ -144,38 +155,42 @@ export class MemoryDriver implements Driver, DriverInterface {
|
|
|
144
155
|
|
|
145
156
|
// Get all records for this object type
|
|
146
157
|
const pattern = `${objectName}:`;
|
|
147
|
-
let
|
|
158
|
+
let records: any[] = [];
|
|
148
159
|
|
|
149
160
|
for (const [key, value] of this.store.entries()) {
|
|
150
161
|
if (key.startsWith(pattern)) {
|
|
151
|
-
|
|
162
|
+
records.push({ ...value });
|
|
152
163
|
}
|
|
153
164
|
}
|
|
154
165
|
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
166
|
+
// Convert ObjectQL filters to MongoDB query format
|
|
167
|
+
const mongoQuery = this.convertToMongoQuery(normalizedQuery.filters);
|
|
168
|
+
|
|
169
|
+
// Apply filters using Mingo
|
|
170
|
+
if (mongoQuery && Object.keys(mongoQuery).length > 0) {
|
|
171
|
+
const mingoQuery = new Query(mongoQuery);
|
|
172
|
+
records = mingoQuery.find(records).all();
|
|
158
173
|
}
|
|
159
174
|
|
|
160
|
-
// Apply sorting
|
|
161
|
-
if (normalizedQuery.sort && Array.isArray(normalizedQuery.sort)) {
|
|
162
|
-
|
|
175
|
+
// Apply sorting manually (Mingo's sort has issues with CJS builds)
|
|
176
|
+
if (normalizedQuery.sort && Array.isArray(normalizedQuery.sort) && normalizedQuery.sort.length > 0) {
|
|
177
|
+
records = this.applyManualSort(records, normalizedQuery.sort);
|
|
163
178
|
}
|
|
164
179
|
|
|
165
180
|
// Apply pagination
|
|
166
181
|
if (normalizedQuery.skip) {
|
|
167
|
-
|
|
182
|
+
records = records.slice(normalizedQuery.skip);
|
|
168
183
|
}
|
|
169
184
|
if (normalizedQuery.limit) {
|
|
170
|
-
|
|
185
|
+
records = records.slice(0, normalizedQuery.limit);
|
|
171
186
|
}
|
|
172
187
|
|
|
173
188
|
// Apply field projection
|
|
174
189
|
if (normalizedQuery.fields && Array.isArray(normalizedQuery.fields)) {
|
|
175
|
-
|
|
190
|
+
records = records.map(doc => this.projectFields(doc, normalizedQuery.fields));
|
|
176
191
|
}
|
|
177
192
|
|
|
178
|
-
return
|
|
193
|
+
return records;
|
|
179
194
|
}
|
|
180
195
|
|
|
181
196
|
/**
|
|
@@ -276,11 +291,10 @@ export class MemoryDriver implements Driver, DriverInterface {
|
|
|
276
291
|
}
|
|
277
292
|
|
|
278
293
|
/**
|
|
279
|
-
* Count records matching filters.
|
|
294
|
+
* Count records matching filters using Mingo.
|
|
280
295
|
*/
|
|
281
296
|
async count(objectName: string, filters: any, options?: any): Promise<number> {
|
|
282
297
|
const pattern = `${objectName}:`;
|
|
283
|
-
let count = 0;
|
|
284
298
|
|
|
285
299
|
// Extract actual filters from query object if needed
|
|
286
300
|
let actualFilters = filters;
|
|
@@ -288,43 +302,59 @@ export class MemoryDriver implements Driver, DriverInterface {
|
|
|
288
302
|
actualFilters = filters.filters;
|
|
289
303
|
}
|
|
290
304
|
|
|
305
|
+
// Get all records for this object type
|
|
306
|
+
let records: any[] = [];
|
|
307
|
+
for (const [key, value] of this.store.entries()) {
|
|
308
|
+
if (key.startsWith(pattern)) {
|
|
309
|
+
records.push(value);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
291
313
|
// If no filters, return total count
|
|
292
314
|
if (!actualFilters || (Array.isArray(actualFilters) && actualFilters.length === 0)) {
|
|
293
|
-
|
|
294
|
-
if (key.startsWith(pattern)) {
|
|
295
|
-
count++;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
return count;
|
|
315
|
+
return records.length;
|
|
299
316
|
}
|
|
300
317
|
|
|
301
|
-
//
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
318
|
+
// Convert to MongoDB query and use Mingo to count
|
|
319
|
+
const mongoQuery = this.convertToMongoQuery(actualFilters);
|
|
320
|
+
if (mongoQuery && Object.keys(mongoQuery).length > 0) {
|
|
321
|
+
const mingoQuery = new Query(mongoQuery);
|
|
322
|
+
const matchedRecords = mingoQuery.find(records).all();
|
|
323
|
+
return matchedRecords.length;
|
|
308
324
|
}
|
|
309
325
|
|
|
310
|
-
return
|
|
326
|
+
return records.length;
|
|
311
327
|
}
|
|
312
328
|
|
|
313
329
|
/**
|
|
314
|
-
* Get distinct values for a field.
|
|
330
|
+
* Get distinct values for a field using Mingo.
|
|
315
331
|
*/
|
|
316
332
|
async distinct(objectName: string, field: string, filters?: any, options?: any): Promise<any[]> {
|
|
317
333
|
const pattern = `${objectName}:`;
|
|
318
|
-
const values = new Set<any>();
|
|
319
334
|
|
|
320
|
-
|
|
335
|
+
// Get all records for this object type
|
|
336
|
+
let records: any[] = [];
|
|
337
|
+
for (const [key, value] of this.store.entries()) {
|
|
321
338
|
if (key.startsWith(pattern)) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
339
|
+
records.push(value);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Apply filters using Mingo if provided
|
|
344
|
+
if (filters) {
|
|
345
|
+
const mongoQuery = this.convertToMongoQuery(filters);
|
|
346
|
+
if (mongoQuery && Object.keys(mongoQuery).length > 0) {
|
|
347
|
+
const mingoQuery = new Query(mongoQuery);
|
|
348
|
+
records = mingoQuery.find(records).all();
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Extract distinct values
|
|
353
|
+
const values = new Set<any>();
|
|
354
|
+
for (const record of records) {
|
|
355
|
+
const value = record[field];
|
|
356
|
+
if (value !== undefined && value !== null) {
|
|
357
|
+
values.add(value);
|
|
328
358
|
}
|
|
329
359
|
}
|
|
330
360
|
|
|
@@ -344,25 +374,45 @@ export class MemoryDriver implements Driver, DriverInterface {
|
|
|
344
374
|
}
|
|
345
375
|
|
|
346
376
|
/**
|
|
347
|
-
* Update multiple records matching filters.
|
|
377
|
+
* Update multiple records matching filters using Mingo.
|
|
348
378
|
*/
|
|
349
379
|
async updateMany(objectName: string, filters: any, data: any, options?: any): Promise<any> {
|
|
350
380
|
const pattern = `${objectName}:`;
|
|
351
|
-
|
|
381
|
+
|
|
382
|
+
// Get all records for this object type
|
|
383
|
+
let records: any[] = [];
|
|
384
|
+
const recordKeys = new Map<string, string>();
|
|
352
385
|
|
|
353
386
|
for (const [key, record] of this.store.entries()) {
|
|
354
387
|
if (key.startsWith(pattern)) {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
388
|
+
records.push(record);
|
|
389
|
+
recordKeys.set(record.id, key);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Apply filters using Mingo
|
|
394
|
+
const mongoQuery = this.convertToMongoQuery(filters);
|
|
395
|
+
let matchedRecords = records;
|
|
396
|
+
|
|
397
|
+
if (mongoQuery && Object.keys(mongoQuery).length > 0) {
|
|
398
|
+
const mingoQuery = new Query(mongoQuery);
|
|
399
|
+
matchedRecords = mingoQuery.find(records).all();
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Update matched records
|
|
403
|
+
let count = 0;
|
|
404
|
+
for (const record of matchedRecords) {
|
|
405
|
+
const key = recordKeys.get(record.id);
|
|
406
|
+
if (key) {
|
|
407
|
+
const updated = {
|
|
408
|
+
...record,
|
|
409
|
+
...data,
|
|
410
|
+
id: record.id, // Preserve ID
|
|
411
|
+
created_at: record.created_at, // Preserve created_at
|
|
412
|
+
updated_at: new Date().toISOString()
|
|
413
|
+
};
|
|
414
|
+
this.store.set(key, updated);
|
|
415
|
+
count++;
|
|
366
416
|
}
|
|
367
417
|
}
|
|
368
418
|
|
|
@@ -370,25 +420,40 @@ export class MemoryDriver implements Driver, DriverInterface {
|
|
|
370
420
|
}
|
|
371
421
|
|
|
372
422
|
/**
|
|
373
|
-
* Delete multiple records matching filters.
|
|
423
|
+
* Delete multiple records matching filters using Mingo.
|
|
374
424
|
*/
|
|
375
425
|
async deleteMany(objectName: string, filters: any, options?: any): Promise<any> {
|
|
376
426
|
const pattern = `${objectName}:`;
|
|
377
|
-
|
|
427
|
+
|
|
428
|
+
// Get all records for this object type
|
|
429
|
+
let records: any[] = [];
|
|
430
|
+
const recordKeys = new Map<string, string>();
|
|
378
431
|
|
|
379
432
|
for (const [key, record] of this.store.entries()) {
|
|
380
433
|
if (key.startsWith(pattern)) {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
}
|
|
434
|
+
records.push(record);
|
|
435
|
+
recordKeys.set(record.id, key);
|
|
384
436
|
}
|
|
385
437
|
}
|
|
386
438
|
|
|
387
|
-
|
|
388
|
-
|
|
439
|
+
// Apply filters using Mingo
|
|
440
|
+
const mongoQuery = this.convertToMongoQuery(filters);
|
|
441
|
+
let matchedRecords = records;
|
|
442
|
+
|
|
443
|
+
if (mongoQuery && Object.keys(mongoQuery).length > 0) {
|
|
444
|
+
const mingoQuery = new Query(mongoQuery);
|
|
445
|
+
matchedRecords = mingoQuery.find(records).all();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Delete matched records
|
|
449
|
+
for (const record of matchedRecords) {
|
|
450
|
+
const key = recordKeys.get(record.id);
|
|
451
|
+
if (key) {
|
|
452
|
+
this.store.delete(key);
|
|
453
|
+
}
|
|
389
454
|
}
|
|
390
455
|
|
|
391
|
-
return { deletedCount:
|
|
456
|
+
return { deletedCount: matchedRecords.length };
|
|
392
457
|
}
|
|
393
458
|
|
|
394
459
|
/**
|
|
@@ -449,7 +514,7 @@ export class MemoryDriver implements Driver, DriverInterface {
|
|
|
449
514
|
}
|
|
450
515
|
|
|
451
516
|
/**
|
|
452
|
-
*
|
|
517
|
+
* Convert ObjectQL filters to MongoDB query format for Mingo.
|
|
453
518
|
*
|
|
454
519
|
* Supports ObjectQL filter format:
|
|
455
520
|
* [
|
|
@@ -457,102 +522,139 @@ export class MemoryDriver implements Driver, DriverInterface {
|
|
|
457
522
|
* 'or',
|
|
458
523
|
* ['field2', 'operator', value2]
|
|
459
524
|
* ]
|
|
525
|
+
*
|
|
526
|
+
* Converts to MongoDB query format:
|
|
527
|
+
* { $or: [{ field: { $operator: value }}, { field2: { $operator: value2 }}] }
|
|
460
528
|
*/
|
|
461
|
-
private
|
|
462
|
-
if (!filters || filters.length === 0) {
|
|
463
|
-
return records;
|
|
464
|
-
}
|
|
465
|
-
|
|
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 {
|
|
529
|
+
private convertToMongoQuery(filters?: any[]): Record<string, any> {
|
|
473
530
|
if (!filters || filters.length === 0) {
|
|
474
|
-
return
|
|
531
|
+
return {};
|
|
475
532
|
}
|
|
476
533
|
|
|
477
|
-
|
|
478
|
-
|
|
534
|
+
// Process the filter array to build MongoDB query
|
|
535
|
+
const conditions: Record<string, any>[] = [];
|
|
536
|
+
let currentLogic: 'and' | 'or' = 'and';
|
|
537
|
+
const logicGroups: { logic: 'and' | 'or', conditions: Record<string, any>[] }[] = [
|
|
538
|
+
{ logic: 'and', conditions: [] }
|
|
539
|
+
];
|
|
479
540
|
|
|
480
541
|
for (const item of filters) {
|
|
481
542
|
if (typeof item === 'string') {
|
|
482
543
|
// Logical operator (and/or)
|
|
483
|
-
|
|
544
|
+
const newLogic = item.toLowerCase() as 'and' | 'or';
|
|
545
|
+
if (newLogic !== currentLogic) {
|
|
546
|
+
currentLogic = newLogic;
|
|
547
|
+
logicGroups.push({ logic: currentLogic, conditions: [] });
|
|
548
|
+
}
|
|
484
549
|
} else if (Array.isArray(item)) {
|
|
485
550
|
const [field, operator, value] = item;
|
|
486
551
|
|
|
487
|
-
//
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
conditions.push(
|
|
491
|
-
} else {
|
|
492
|
-
// Single condition
|
|
493
|
-
const matches = this.evaluateCondition(record[field], operator, value);
|
|
494
|
-
conditions.push(matches);
|
|
552
|
+
// Convert single condition to MongoDB operator
|
|
553
|
+
const mongoCondition = this.convertConditionToMongo(field, operator, value);
|
|
554
|
+
if (mongoCondition) {
|
|
555
|
+
logicGroups[logicGroups.length - 1].conditions.push(mongoCondition);
|
|
495
556
|
}
|
|
496
557
|
}
|
|
497
558
|
}
|
|
498
559
|
|
|
499
|
-
//
|
|
500
|
-
if (conditions.length ===
|
|
501
|
-
return
|
|
560
|
+
// Build final query from logic groups
|
|
561
|
+
if (logicGroups.length === 1 && logicGroups[0].conditions.length === 1) {
|
|
562
|
+
return logicGroups[0].conditions[0];
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// If there's only one group with multiple conditions, use its logic operator
|
|
566
|
+
if (logicGroups.length === 1) {
|
|
567
|
+
const group = logicGroups[0];
|
|
568
|
+
if (group.logic === 'or') {
|
|
569
|
+
return { $or: group.conditions };
|
|
570
|
+
} else {
|
|
571
|
+
return { $and: group.conditions };
|
|
572
|
+
}
|
|
502
573
|
}
|
|
503
574
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
575
|
+
// Multiple groups - flatten all conditions and determine the top-level operator
|
|
576
|
+
const allConditions: Record<string, any>[] = [];
|
|
577
|
+
for (const group of logicGroups) {
|
|
578
|
+
if (group.conditions.length === 0) continue;
|
|
508
579
|
|
|
509
|
-
if (
|
|
510
|
-
|
|
511
|
-
} else {
|
|
512
|
-
|
|
580
|
+
if (group.conditions.length === 1) {
|
|
581
|
+
allConditions.push(group.conditions[0]);
|
|
582
|
+
} else {
|
|
583
|
+
if (group.logic === 'or') {
|
|
584
|
+
allConditions.push({ $or: group.conditions });
|
|
585
|
+
} else {
|
|
586
|
+
allConditions.push({ $and: group.conditions });
|
|
587
|
+
}
|
|
513
588
|
}
|
|
514
589
|
}
|
|
515
590
|
|
|
516
|
-
|
|
591
|
+
if (allConditions.length === 0) {
|
|
592
|
+
return {};
|
|
593
|
+
} else if (allConditions.length === 1) {
|
|
594
|
+
return allConditions[0];
|
|
595
|
+
} else {
|
|
596
|
+
// Determine top-level operator: use OR if any non-empty group has OR logic
|
|
597
|
+
const hasOrLogic = logicGroups.some(g => g.logic === 'or' && g.conditions.length > 0);
|
|
598
|
+
if (hasOrLogic) {
|
|
599
|
+
return { $or: allConditions };
|
|
600
|
+
} else {
|
|
601
|
+
return { $and: allConditions };
|
|
602
|
+
}
|
|
603
|
+
}
|
|
517
604
|
}
|
|
518
605
|
|
|
519
606
|
/**
|
|
520
|
-
*
|
|
607
|
+
* Convert a single ObjectQL condition to MongoDB operator format.
|
|
521
608
|
*/
|
|
522
|
-
private
|
|
609
|
+
private convertConditionToMongo(field: string, operator: string, value: any): Record<string, any> | null {
|
|
523
610
|
switch (operator) {
|
|
524
611
|
case '=':
|
|
525
612
|
case '==':
|
|
526
|
-
return
|
|
613
|
+
return { [field]: value };
|
|
614
|
+
|
|
527
615
|
case '!=':
|
|
528
616
|
case '<>':
|
|
529
|
-
return
|
|
617
|
+
return { [field]: { $ne: value } };
|
|
618
|
+
|
|
530
619
|
case '>':
|
|
531
|
-
return
|
|
620
|
+
return { [field]: { $gt: value } };
|
|
621
|
+
|
|
532
622
|
case '>=':
|
|
533
|
-
return
|
|
623
|
+
return { [field]: { $gte: value } };
|
|
624
|
+
|
|
534
625
|
case '<':
|
|
535
|
-
return
|
|
626
|
+
return { [field]: { $lt: value } };
|
|
627
|
+
|
|
536
628
|
case '<=':
|
|
537
|
-
return
|
|
629
|
+
return { [field]: { $lte: value } };
|
|
630
|
+
|
|
538
631
|
case 'in':
|
|
539
|
-
return
|
|
632
|
+
return { [field]: { $in: value } };
|
|
633
|
+
|
|
540
634
|
case 'nin':
|
|
541
635
|
case 'not in':
|
|
542
|
-
return
|
|
636
|
+
return { [field]: { $nin: value } };
|
|
637
|
+
|
|
543
638
|
case 'contains':
|
|
544
639
|
case 'like':
|
|
545
|
-
|
|
640
|
+
// MongoDB regex for case-insensitive contains
|
|
641
|
+
// Escape special regex characters to prevent ReDoS and ensure literal matching
|
|
642
|
+
return { [field]: { $regex: new RegExp(this.escapeRegex(value), 'i') } };
|
|
643
|
+
|
|
546
644
|
case 'startswith':
|
|
547
645
|
case 'starts_with':
|
|
548
|
-
return
|
|
646
|
+
return { [field]: { $regex: new RegExp(`^${this.escapeRegex(value)}`, 'i') } };
|
|
647
|
+
|
|
549
648
|
case 'endswith':
|
|
550
649
|
case 'ends_with':
|
|
551
|
-
return
|
|
650
|
+
return { [field]: { $regex: new RegExp(`${this.escapeRegex(value)}$`, 'i') } };
|
|
651
|
+
|
|
552
652
|
case 'between':
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
653
|
+
if (Array.isArray(value) && value.length === 2) {
|
|
654
|
+
return { [field]: { $gte: value[0], $lte: value[1] } };
|
|
655
|
+
}
|
|
656
|
+
return null;
|
|
657
|
+
|
|
556
658
|
default:
|
|
557
659
|
throw new ObjectQLError({
|
|
558
660
|
code: 'UNSUPPORTED_OPERATOR',
|
|
@@ -562,12 +664,21 @@ export class MemoryDriver implements Driver, DriverInterface {
|
|
|
562
664
|
}
|
|
563
665
|
|
|
564
666
|
/**
|
|
565
|
-
*
|
|
667
|
+
* Escape special regex characters to prevent ReDoS and ensure literal matching.
|
|
668
|
+
* This is crucial for security when using user input in regex patterns.
|
|
669
|
+
*/
|
|
670
|
+
private escapeRegex(str: string): string {
|
|
671
|
+
return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Apply manual sorting to an array of records.
|
|
676
|
+
* This is used instead of Mingo's sort to avoid CJS build issues.
|
|
566
677
|
*/
|
|
567
|
-
private
|
|
678
|
+
private applyManualSort(records: any[], sort: any[]): any[] {
|
|
568
679
|
const sorted = [...records];
|
|
569
680
|
|
|
570
|
-
// Apply sorts in reverse order for correct precedence
|
|
681
|
+
// Apply sorts in reverse order for correct multi-field precedence
|
|
571
682
|
for (let i = sort.length - 1; i >= 0; i--) {
|
|
572
683
|
const sortItem = sort[i];
|
|
573
684
|
|
|
@@ -593,8 +704,8 @@ export class MemoryDriver implements Driver, DriverInterface {
|
|
|
593
704
|
if (bVal == null) return -1;
|
|
594
705
|
|
|
595
706
|
// Compare values
|
|
596
|
-
if (aVal < bVal) return direction === '
|
|
597
|
-
if (aVal > bVal) return direction === '
|
|
707
|
+
if (aVal < bVal) return direction.toLowerCase() === 'desc' ? 1 : -1;
|
|
708
|
+
if (aVal > bVal) return direction.toLowerCase() === 'desc' ? -1 : 1;
|
|
598
709
|
return 0;
|
|
599
710
|
});
|
|
600
711
|
}
|