@objectql/driver-memory 4.0.2 → 4.0.3
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/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +16 -0
- package/dist/index.d.ts +109 -11
- package/dist/index.js +277 -6
- package/dist/index.js.map +1 -1
- package/jest.config.js +13 -1
- package/package.json +3 -3
- package/src/index.ts +347 -19
- package/test/index.test.ts +271 -0
- package/tsconfig.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectql/driver-memory",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.3",
|
|
4
4
|
"description": "In-memory driver for ObjectQL - Fast MongoDB-like query engine powered by Mingo with DriverInterface v4.0 compliance",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"objectql",
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
"main": "dist/index.js",
|
|
17
17
|
"types": "dist/index.d.ts",
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@objectstack/spec": "^0.
|
|
19
|
+
"@objectstack/spec": "^0.8.0",
|
|
20
20
|
"mingo": "^7.1.1",
|
|
21
|
-
"@objectql/types": "4.0.
|
|
21
|
+
"@objectql/types": "4.0.3"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@types/jest": "^29.0.0",
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Data,
|
|
1
|
+
import { Data, System as SystemSpec } from '@objectstack/spec';
|
|
2
2
|
type QueryAST = Data.QueryAST;
|
|
3
3
|
type SortNode = Data.SortNode;
|
|
4
|
-
type DriverInterface =
|
|
4
|
+
type DriverInterface = Data.DriverInterface;
|
|
5
5
|
/**
|
|
6
6
|
* ObjectQL
|
|
7
7
|
* Copyright (c) 2026-present ObjectStack Inc.
|
|
@@ -38,7 +38,7 @@ type DriverInterface = DriverSpec.DriverInterface;
|
|
|
38
38
|
*/
|
|
39
39
|
|
|
40
40
|
import { Driver, ObjectQLError } from '@objectql/types';
|
|
41
|
-
import { Query } from 'mingo';
|
|
41
|
+
import { Query, Aggregator } from 'mingo';
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* Command interface for executeCommand method
|
|
@@ -72,6 +72,24 @@ export interface MemoryDriverConfig {
|
|
|
72
72
|
initialData?: Record<string, any[]>;
|
|
73
73
|
/** Optional: Enable strict mode (throw on missing objects) */
|
|
74
74
|
strictMode?: boolean;
|
|
75
|
+
/** Optional: Enable persistence to file system */
|
|
76
|
+
persistence?: {
|
|
77
|
+
/** File path to persist data */
|
|
78
|
+
filePath: string;
|
|
79
|
+
/** Auto-save interval in milliseconds (default: 5000) */
|
|
80
|
+
autoSaveInterval?: number;
|
|
81
|
+
};
|
|
82
|
+
/** Optional: Fields to index for faster queries */
|
|
83
|
+
indexes?: Record<string, string[]>; // objectName -> field names
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* In-memory transaction
|
|
88
|
+
*/
|
|
89
|
+
interface MemoryTransaction {
|
|
90
|
+
id: string;
|
|
91
|
+
snapshot: Map<string, any>;
|
|
92
|
+
operations: Array<{ type: 'set' | 'delete'; key: string; value?: any }>;
|
|
75
93
|
}
|
|
76
94
|
|
|
77
95
|
/**
|
|
@@ -87,32 +105,47 @@ export class MemoryDriver implements Driver {
|
|
|
87
105
|
public readonly name = 'MemoryDriver';
|
|
88
106
|
public readonly version = '4.0.0';
|
|
89
107
|
public readonly supports = {
|
|
90
|
-
transactions:
|
|
108
|
+
transactions: true,
|
|
91
109
|
joins: false,
|
|
92
110
|
fullTextSearch: false,
|
|
93
111
|
jsonFields: true,
|
|
94
112
|
arrayFields: true,
|
|
95
113
|
queryFilters: true,
|
|
96
|
-
queryAggregations:
|
|
114
|
+
queryAggregations: true,
|
|
97
115
|
querySorting: true,
|
|
98
116
|
queryPagination: true,
|
|
99
117
|
queryWindowFunctions: false,
|
|
100
118
|
querySubqueries: false
|
|
101
119
|
};
|
|
102
120
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
121
|
+
protected store: Map<string, any>;
|
|
122
|
+
protected config: MemoryDriverConfig;
|
|
123
|
+
protected idCounters: Map<string, number>;
|
|
124
|
+
protected transactions: Map<string, MemoryTransaction>;
|
|
125
|
+
protected indexes: Map<string, Map<string, Set<string>>>; // objectName -> field -> Set<recordIds>
|
|
126
|
+
protected persistenceTimer?: NodeJS.Timeout;
|
|
106
127
|
|
|
107
128
|
constructor(config: MemoryDriverConfig = {}) {
|
|
108
129
|
this.config = config;
|
|
109
130
|
this.store = new Map<string, any>();
|
|
110
131
|
this.idCounters = new Map<string, number>();
|
|
132
|
+
this.transactions = new Map();
|
|
133
|
+
this.indexes = new Map();
|
|
111
134
|
|
|
112
135
|
// Load initial data if provided
|
|
113
136
|
if (config.initialData) {
|
|
114
137
|
this.loadInitialData(config.initialData);
|
|
115
138
|
}
|
|
139
|
+
|
|
140
|
+
// Set up persistence if configured
|
|
141
|
+
if (config.persistence) {
|
|
142
|
+
this.setupPersistence();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Build indexes if configured
|
|
146
|
+
if (config.indexes) {
|
|
147
|
+
this.buildIndexes(config.indexes);
|
|
148
|
+
}
|
|
116
149
|
}
|
|
117
150
|
|
|
118
151
|
/**
|
|
@@ -134,7 +167,7 @@ export class MemoryDriver implements Driver {
|
|
|
134
167
|
/**
|
|
135
168
|
* Load initial data into the store.
|
|
136
169
|
*/
|
|
137
|
-
|
|
170
|
+
protected loadInitialData(data: Record<string, any[]>): void {
|
|
138
171
|
for (const [objectName, records] of Object.entries(data)) {
|
|
139
172
|
for (const record of records) {
|
|
140
173
|
const id = record.id || this.generateId(objectName);
|
|
@@ -235,6 +268,10 @@ export class MemoryDriver implements Driver {
|
|
|
235
268
|
};
|
|
236
269
|
|
|
237
270
|
this.store.set(key, doc);
|
|
271
|
+
|
|
272
|
+
// Update indexes
|
|
273
|
+
this.updateIndex(objectName, id, doc);
|
|
274
|
+
|
|
238
275
|
return { ...doc };
|
|
239
276
|
}
|
|
240
277
|
|
|
@@ -265,6 +302,10 @@ export class MemoryDriver implements Driver {
|
|
|
265
302
|
};
|
|
266
303
|
|
|
267
304
|
this.store.set(key, doc);
|
|
305
|
+
|
|
306
|
+
// Update indexes
|
|
307
|
+
this.updateIndex(objectName, id.toString(), doc);
|
|
308
|
+
|
|
268
309
|
return { ...doc };
|
|
269
310
|
}
|
|
270
311
|
|
|
@@ -275,6 +316,11 @@ export class MemoryDriver implements Driver {
|
|
|
275
316
|
const key = `${objectName}:${id}`;
|
|
276
317
|
const deleted = this.store.delete(key);
|
|
277
318
|
|
|
319
|
+
// Remove from indexes
|
|
320
|
+
if (deleted) {
|
|
321
|
+
this.removeFromIndex(objectName, id.toString());
|
|
322
|
+
}
|
|
323
|
+
|
|
278
324
|
if (!deleted && this.config.strictMode) {
|
|
279
325
|
throw new ObjectQLError({
|
|
280
326
|
code: 'RECORD_NOT_FOUND',
|
|
@@ -468,10 +514,259 @@ export class MemoryDriver implements Driver {
|
|
|
468
514
|
}
|
|
469
515
|
|
|
470
516
|
/**
|
|
471
|
-
* Disconnect
|
|
517
|
+
* Disconnect and cleanup resources.
|
|
472
518
|
*/
|
|
473
519
|
async disconnect(): Promise<void> {
|
|
474
|
-
//
|
|
520
|
+
// Save data to disk if persistence is enabled
|
|
521
|
+
if (this.config.persistence) {
|
|
522
|
+
this.saveToDisk();
|
|
523
|
+
|
|
524
|
+
// Clear the auto-save timer
|
|
525
|
+
if (this.persistenceTimer) {
|
|
526
|
+
clearInterval(this.persistenceTimer);
|
|
527
|
+
this.persistenceTimer = undefined;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Perform aggregation operations using Mingo
|
|
534
|
+
* @param objectName - The object type to aggregate
|
|
535
|
+
* @param pipeline - MongoDB-style aggregation pipeline
|
|
536
|
+
* @param options - Optional query options
|
|
537
|
+
* @returns Aggregation results
|
|
538
|
+
*
|
|
539
|
+
* @example
|
|
540
|
+
* // Group by status and count
|
|
541
|
+
* const results = await driver.aggregate('orders', [
|
|
542
|
+
* { $match: { status: 'completed' } },
|
|
543
|
+
* { $group: { _id: '$customer', totalAmount: { $sum: '$amount' } } }
|
|
544
|
+
* ]);
|
|
545
|
+
*
|
|
546
|
+
* @example
|
|
547
|
+
* // Calculate average with filter
|
|
548
|
+
* const results = await driver.aggregate('products', [
|
|
549
|
+
* { $match: { category: 'electronics' } },
|
|
550
|
+
* { $group: { _id: null, avgPrice: { $avg: '$price' } } }
|
|
551
|
+
* ]);
|
|
552
|
+
*/
|
|
553
|
+
async aggregate(objectName: string, pipeline: any[], options?: any): Promise<any[]> {
|
|
554
|
+
const pattern = `${objectName}:`;
|
|
555
|
+
|
|
556
|
+
// Get all records for this object type
|
|
557
|
+
let records: any[] = [];
|
|
558
|
+
for (const [key, value] of this.store.entries()) {
|
|
559
|
+
if (key.startsWith(pattern)) {
|
|
560
|
+
records.push({ ...value });
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Use Mingo to execute the aggregation pipeline
|
|
565
|
+
const aggregator = new Aggregator(pipeline);
|
|
566
|
+
const results = aggregator.run(records);
|
|
567
|
+
|
|
568
|
+
return results;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Begin a new transaction
|
|
573
|
+
* @returns Transaction object that can be passed to other methods
|
|
574
|
+
*/
|
|
575
|
+
async beginTransaction(): Promise<any> {
|
|
576
|
+
const txId = `tx_${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
|
577
|
+
|
|
578
|
+
// Create a deep snapshot of the current store to ensure complete transaction isolation
|
|
579
|
+
// This prevents issues with nested objects and arrays being modified during the transaction
|
|
580
|
+
const snapshot = new Map<string, any>();
|
|
581
|
+
for (const [key, value] of this.store.entries()) {
|
|
582
|
+
snapshot.set(key, JSON.parse(JSON.stringify(value)));
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const transaction: MemoryTransaction = {
|
|
586
|
+
id: txId,
|
|
587
|
+
snapshot,
|
|
588
|
+
operations: []
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
this.transactions.set(txId, transaction);
|
|
592
|
+
|
|
593
|
+
return { id: txId };
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Commit a transaction
|
|
598
|
+
* @param transaction - Transaction object returned by beginTransaction()
|
|
599
|
+
*/
|
|
600
|
+
async commitTransaction(transaction: any): Promise<void> {
|
|
601
|
+
const txId = transaction?.id;
|
|
602
|
+
if (!txId) {
|
|
603
|
+
throw new ObjectQLError({
|
|
604
|
+
code: 'INVALID_TRANSACTION',
|
|
605
|
+
message: 'Invalid transaction object'
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const tx = this.transactions.get(txId);
|
|
610
|
+
if (!tx) {
|
|
611
|
+
throw new ObjectQLError({
|
|
612
|
+
code: 'TRANSACTION_NOT_FOUND',
|
|
613
|
+
message: `Transaction ${txId} not found`
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Operations are already applied to the store during the transaction
|
|
618
|
+
// Just clean up the transaction record
|
|
619
|
+
this.transactions.delete(txId);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Rollback a transaction
|
|
624
|
+
* @param transaction - Transaction object returned by beginTransaction()
|
|
625
|
+
*/
|
|
626
|
+
async rollbackTransaction(transaction: any): Promise<void> {
|
|
627
|
+
const txId = transaction?.id;
|
|
628
|
+
if (!txId) {
|
|
629
|
+
throw new ObjectQLError({
|
|
630
|
+
code: 'INVALID_TRANSACTION',
|
|
631
|
+
message: 'Invalid transaction object'
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const tx = this.transactions.get(txId);
|
|
636
|
+
if (!tx) {
|
|
637
|
+
throw new ObjectQLError({
|
|
638
|
+
code: 'TRANSACTION_NOT_FOUND',
|
|
639
|
+
message: `Transaction ${txId} not found`
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Restore the snapshot
|
|
644
|
+
this.store = new Map(tx.snapshot);
|
|
645
|
+
|
|
646
|
+
// Clean up the transaction
|
|
647
|
+
this.transactions.delete(txId);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Set up persistence to file system
|
|
652
|
+
* @private
|
|
653
|
+
*/
|
|
654
|
+
protected setupPersistence(): void {
|
|
655
|
+
if (!this.config.persistence) return;
|
|
656
|
+
|
|
657
|
+
const { filePath, autoSaveInterval = 5000 } = this.config.persistence;
|
|
658
|
+
|
|
659
|
+
// Try to load existing data from file
|
|
660
|
+
try {
|
|
661
|
+
const fs = require('fs');
|
|
662
|
+
if (fs.existsSync(filePath)) {
|
|
663
|
+
const fileData = fs.readFileSync(filePath, 'utf8');
|
|
664
|
+
const data = JSON.parse(fileData);
|
|
665
|
+
|
|
666
|
+
// Load data into store
|
|
667
|
+
for (const [key, value] of Object.entries(data.store || {})) {
|
|
668
|
+
this.store.set(key, value);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Load ID counters
|
|
672
|
+
if (data.idCounters) {
|
|
673
|
+
for (const [key, value] of Object.entries(data.idCounters)) {
|
|
674
|
+
this.idCounters.set(key, value as number);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
} catch (error) {
|
|
679
|
+
console.warn('[MemoryDriver] Failed to load persisted data:', error);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Set up auto-save timer
|
|
683
|
+
// Use unref() to allow Node.js process to exit gracefully when idle
|
|
684
|
+
this.persistenceTimer = setInterval(() => {
|
|
685
|
+
this.saveToDisk();
|
|
686
|
+
}, autoSaveInterval);
|
|
687
|
+
this.persistenceTimer.unref();
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Save current state to disk
|
|
692
|
+
* @private
|
|
693
|
+
*/
|
|
694
|
+
protected saveToDisk(): void {
|
|
695
|
+
if (!this.config.persistence) return;
|
|
696
|
+
|
|
697
|
+
try {
|
|
698
|
+
const fs = require('fs');
|
|
699
|
+
const data = {
|
|
700
|
+
store: Object.fromEntries(this.store),
|
|
701
|
+
idCounters: Object.fromEntries(this.idCounters),
|
|
702
|
+
timestamp: new Date().toISOString()
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
fs.writeFileSync(this.config.persistence.filePath, JSON.stringify(data, null, 2), 'utf8');
|
|
706
|
+
} catch (error) {
|
|
707
|
+
console.error('[MemoryDriver] Failed to save data to disk:', error);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Build indexes for faster queries
|
|
713
|
+
* @private
|
|
714
|
+
*/
|
|
715
|
+
protected buildIndexes(indexConfig: Record<string, string[]>): void {
|
|
716
|
+
for (const [objectName, fields] of Object.entries(indexConfig)) {
|
|
717
|
+
const objectIndexes = new Map<string, Set<string>>();
|
|
718
|
+
|
|
719
|
+
for (const field of fields) {
|
|
720
|
+
const fieldIndex = new Set<string>();
|
|
721
|
+
|
|
722
|
+
// Scan all records of this object type
|
|
723
|
+
const pattern = `${objectName}:`;
|
|
724
|
+
for (const [key, record] of this.store.entries()) {
|
|
725
|
+
if (key.startsWith(pattern)) {
|
|
726
|
+
const fieldValue = record[field];
|
|
727
|
+
if (fieldValue !== undefined && fieldValue !== null) {
|
|
728
|
+
// Store the record ID in the index
|
|
729
|
+
fieldIndex.add(record.id);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
objectIndexes.set(field, fieldIndex);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
this.indexes.set(objectName, objectIndexes);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Update index when a record is created or updated
|
|
743
|
+
* @private
|
|
744
|
+
*/
|
|
745
|
+
protected updateIndex(objectName: string, recordId: string, record: any): void {
|
|
746
|
+
const objectIndexes = this.indexes.get(objectName);
|
|
747
|
+
if (!objectIndexes) return;
|
|
748
|
+
|
|
749
|
+
for (const [field, indexSet] of objectIndexes.entries()) {
|
|
750
|
+
const fieldValue = record[field];
|
|
751
|
+
if (fieldValue !== undefined && fieldValue !== null) {
|
|
752
|
+
indexSet.add(recordId);
|
|
753
|
+
} else {
|
|
754
|
+
indexSet.delete(recordId);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Remove record from indexes
|
|
761
|
+
* @private
|
|
762
|
+
*/
|
|
763
|
+
protected removeFromIndex(objectName: string, recordId: string): void {
|
|
764
|
+
const objectIndexes = this.indexes.get(objectName);
|
|
765
|
+
if (!objectIndexes) return;
|
|
766
|
+
|
|
767
|
+
for (const indexSet of objectIndexes.values()) {
|
|
768
|
+
indexSet.delete(recordId);
|
|
769
|
+
}
|
|
475
770
|
}
|
|
476
771
|
|
|
477
772
|
// ========== Helper Methods ==========
|
|
@@ -495,13 +790,46 @@ export class MemoryDriver implements Driver {
|
|
|
495
790
|
* Converts to MongoDB query format:
|
|
496
791
|
* { $or: [{ field: { $operator: value }}, { field2: { $operator: value2 }}] }
|
|
497
792
|
*/
|
|
498
|
-
|
|
793
|
+
/**
|
|
794
|
+
* Convert ObjectQL filter format to MongoDB query format.
|
|
795
|
+
* Supports three input formats:
|
|
796
|
+
*
|
|
797
|
+
* 1. AST Comparison Node: { type: 'comparison', field: string, operator: string, value: any }
|
|
798
|
+
* 2. AST Logical Node: { type: 'logical', operator: 'and' | 'or', conditions: Node[] }
|
|
799
|
+
* 3. Legacy Array Format: [field, operator, value, 'and', field2, operator2, value2]
|
|
800
|
+
* 4. MongoDB Format: { field: value } or { field: { $eq: value } } (passthrough)
|
|
801
|
+
*
|
|
802
|
+
* @param filters - Filter in any supported format
|
|
803
|
+
* @returns MongoDB query object
|
|
804
|
+
*/
|
|
805
|
+
protected convertToMongoQuery(filters?: any[] | Record<string, any>): Record<string, any> {
|
|
499
806
|
if (!filters) {
|
|
500
807
|
return {};
|
|
501
808
|
}
|
|
502
809
|
|
|
503
|
-
//
|
|
504
|
-
if (!Array.isArray(filters)) {
|
|
810
|
+
// Handle AST node format (ObjectQL QueryAST)
|
|
811
|
+
if (!Array.isArray(filters) && typeof filters === 'object') {
|
|
812
|
+
// AST Comparison Node: { type: 'comparison', field, operator, value }
|
|
813
|
+
if (filters.type === 'comparison') {
|
|
814
|
+
const mongoCondition = this.convertConditionToMongo(filters.field, filters.operator, filters.value);
|
|
815
|
+
return mongoCondition || {};
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// AST Logical Node: { type: 'logical', operator: 'and' | 'or', conditions: [...] }
|
|
819
|
+
else if (filters.type === 'logical') {
|
|
820
|
+
const conditions = filters.conditions?.map((cond: any) => this.convertToMongoQuery(cond)) || [];
|
|
821
|
+
if (conditions.length === 0) {
|
|
822
|
+
return {};
|
|
823
|
+
}
|
|
824
|
+
if (conditions.length === 1) {
|
|
825
|
+
return conditions[0];
|
|
826
|
+
}
|
|
827
|
+
const operator = filters.operator === 'or' ? '$or' : '$and';
|
|
828
|
+
return { [operator]: conditions };
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// MongoDB format (passthrough): { field: value } or { field: { $eq: value } }
|
|
832
|
+
// This handles cases where filters is already a proper MongoDB query
|
|
505
833
|
return filters;
|
|
506
834
|
}
|
|
507
835
|
|
|
@@ -585,7 +913,7 @@ export class MemoryDriver implements Driver {
|
|
|
585
913
|
/**
|
|
586
914
|
* Convert a single ObjectQL condition to MongoDB operator format.
|
|
587
915
|
*/
|
|
588
|
-
|
|
916
|
+
protected convertConditionToMongo(field: string, operator: string, value: any): Record<string, any> | null {
|
|
589
917
|
switch (operator) {
|
|
590
918
|
case '=':
|
|
591
919
|
case '==':
|
|
@@ -646,7 +974,7 @@ export class MemoryDriver implements Driver {
|
|
|
646
974
|
* Escape special regex characters to prevent ReDoS and ensure literal matching.
|
|
647
975
|
* This is crucial for security when using user input in regex patterns.
|
|
648
976
|
*/
|
|
649
|
-
|
|
977
|
+
protected escapeRegex(str: string): string {
|
|
650
978
|
return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
651
979
|
}
|
|
652
980
|
|
|
@@ -654,7 +982,7 @@ export class MemoryDriver implements Driver {
|
|
|
654
982
|
* Apply manual sorting to an array of records.
|
|
655
983
|
* This is used instead of Mingo's sort to avoid CJS build issues.
|
|
656
984
|
*/
|
|
657
|
-
|
|
985
|
+
protected applyManualSort(records: any[], sort: any[]): any[] {
|
|
658
986
|
const sorted = [...records];
|
|
659
987
|
|
|
660
988
|
// Apply sorts in reverse order for correct multi-field precedence
|
|
@@ -695,7 +1023,7 @@ export class MemoryDriver implements Driver {
|
|
|
695
1023
|
/**
|
|
696
1024
|
* Project specific fields from a document.
|
|
697
1025
|
*/
|
|
698
|
-
|
|
1026
|
+
protected projectFields(doc: any, fields: string[]): any {
|
|
699
1027
|
const result: any = {};
|
|
700
1028
|
for (const field of fields) {
|
|
701
1029
|
if (doc[field] !== undefined) {
|
|
@@ -708,7 +1036,7 @@ export class MemoryDriver implements Driver {
|
|
|
708
1036
|
/**
|
|
709
1037
|
* Generate a unique ID for a record.
|
|
710
1038
|
*/
|
|
711
|
-
|
|
1039
|
+
protected generateId(objectName: string): string {
|
|
712
1040
|
const counter = (this.idCounters.get(objectName) || 0) + 1;
|
|
713
1041
|
this.idCounters.set(objectName, counter);
|
|
714
1042
|
|