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