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