@objectstack/driver-memory 2.0.5 → 2.0.7
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 +10 -10
- package/CHANGELOG.md +17 -0
- package/dist/index.d.mts +98 -11
- package/dist/index.d.ts +98 -11
- package/dist/index.js +347 -159
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +347 -159
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -5
- package/src/index.ts +2 -13
- package/src/memory-driver.test.ts +473 -1
- package/src/memory-driver.ts +413 -82
package/dist/index.js
CHANGED
|
@@ -27,35 +27,9 @@ module.exports = __toCommonJS(index_exports);
|
|
|
27
27
|
|
|
28
28
|
// src/memory-driver.ts
|
|
29
29
|
var import_core = require("@objectstack/core");
|
|
30
|
+
var import_mingo = require("mingo");
|
|
30
31
|
|
|
31
32
|
// src/memory-matcher.ts
|
|
32
|
-
function match(record, filter) {
|
|
33
|
-
if (!filter || Object.keys(filter).length === 0) return true;
|
|
34
|
-
if (Array.isArray(filter.$and)) {
|
|
35
|
-
if (!filter.$and.every((f) => match(record, f))) {
|
|
36
|
-
return false;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
if (Array.isArray(filter.$or)) {
|
|
40
|
-
if (!filter.$or.some((f) => match(record, f))) {
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
if (filter.$not) {
|
|
45
|
-
if (match(record, filter.$not)) {
|
|
46
|
-
return false;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
for (const key of Object.keys(filter)) {
|
|
50
|
-
if (key.startsWith("$")) continue;
|
|
51
|
-
const condition = filter[key];
|
|
52
|
-
const value = getValueByPath(record, key);
|
|
53
|
-
if (!checkCondition(value, condition)) {
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return true;
|
|
58
|
-
}
|
|
59
33
|
function getValueByPath(obj, path) {
|
|
60
34
|
if (path === "_id" && obj._id === void 0 && obj.id !== void 0) {
|
|
61
35
|
return obj.id;
|
|
@@ -63,89 +37,19 @@ function getValueByPath(obj, path) {
|
|
|
63
37
|
if (!path.includes(".")) return obj[path];
|
|
64
38
|
return path.split(".").reduce((o, i) => o ? o[i] : void 0, obj);
|
|
65
39
|
}
|
|
66
|
-
function checkCondition(value, condition) {
|
|
67
|
-
if (typeof condition !== "object" || condition === null || condition instanceof Date || Array.isArray(condition)) {
|
|
68
|
-
return value == condition;
|
|
69
|
-
}
|
|
70
|
-
const keys = Object.keys(condition);
|
|
71
|
-
const isOperatorObject = keys.some((k) => k.startsWith("$"));
|
|
72
|
-
if (!isOperatorObject) {
|
|
73
|
-
return JSON.stringify(value) === JSON.stringify(condition);
|
|
74
|
-
}
|
|
75
|
-
for (const op of keys) {
|
|
76
|
-
const target = condition[op];
|
|
77
|
-
if (value === void 0 && op !== "$exists" && op !== "$ne") {
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
switch (op) {
|
|
81
|
-
case "$eq":
|
|
82
|
-
if (value != target) return false;
|
|
83
|
-
break;
|
|
84
|
-
case "$ne":
|
|
85
|
-
if (value == target) return false;
|
|
86
|
-
break;
|
|
87
|
-
// Numeric / Date
|
|
88
|
-
case "$gt":
|
|
89
|
-
if (!(value > target)) return false;
|
|
90
|
-
break;
|
|
91
|
-
case "$gte":
|
|
92
|
-
if (!(value >= target)) return false;
|
|
93
|
-
break;
|
|
94
|
-
case "$lt":
|
|
95
|
-
if (!(value < target)) return false;
|
|
96
|
-
break;
|
|
97
|
-
case "$lte":
|
|
98
|
-
if (!(value <= target)) return false;
|
|
99
|
-
break;
|
|
100
|
-
case "$between":
|
|
101
|
-
if (Array.isArray(target) && (value < target[0] || value > target[1])) return false;
|
|
102
|
-
break;
|
|
103
|
-
// Sets
|
|
104
|
-
case "$in":
|
|
105
|
-
if (!Array.isArray(target) || !target.includes(value)) return false;
|
|
106
|
-
break;
|
|
107
|
-
case "$nin":
|
|
108
|
-
if (Array.isArray(target) && target.includes(value)) return false;
|
|
109
|
-
break;
|
|
110
|
-
// Existence
|
|
111
|
-
case "$exists":
|
|
112
|
-
const exists = value !== void 0 && value !== null;
|
|
113
|
-
if (exists !== !!target) return false;
|
|
114
|
-
break;
|
|
115
|
-
// Strings
|
|
116
|
-
case "$contains":
|
|
117
|
-
if (typeof value !== "string" || !value.includes(target)) return false;
|
|
118
|
-
break;
|
|
119
|
-
case "$startsWith":
|
|
120
|
-
if (typeof value !== "string" || !value.startsWith(target)) return false;
|
|
121
|
-
break;
|
|
122
|
-
case "$endsWith":
|
|
123
|
-
if (typeof value !== "string" || !value.endsWith(target)) return false;
|
|
124
|
-
break;
|
|
125
|
-
case "$regex":
|
|
126
|
-
try {
|
|
127
|
-
const re = new RegExp(target, condition.$options || "");
|
|
128
|
-
if (!re.test(String(value))) return false;
|
|
129
|
-
} catch (e) {
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
break;
|
|
133
|
-
default:
|
|
134
|
-
break;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
return true;
|
|
138
|
-
}
|
|
139
40
|
|
|
140
41
|
// src/memory-driver.ts
|
|
141
42
|
var InMemoryDriver = class {
|
|
142
43
|
constructor(config) {
|
|
143
44
|
this.name = "com.objectstack.driver.memory";
|
|
144
45
|
this.type = "driver";
|
|
145
|
-
this.version = "0.0
|
|
46
|
+
this.version = "1.0.0";
|
|
47
|
+
this.idCounters = /* @__PURE__ */ new Map();
|
|
48
|
+
this.transactions = /* @__PURE__ */ new Map();
|
|
146
49
|
this.supports = {
|
|
147
50
|
// Transaction & Connection Management
|
|
148
|
-
transactions:
|
|
51
|
+
transactions: true,
|
|
52
|
+
// Snapshot-based transactions
|
|
149
53
|
// Query Operations
|
|
150
54
|
queryFilters: true,
|
|
151
55
|
// Implemented via memory-matcher
|
|
@@ -179,7 +83,7 @@ var InMemoryDriver = class {
|
|
|
179
83
|
this.db = {};
|
|
180
84
|
this.config = config || {};
|
|
181
85
|
this.logger = config?.logger || (0, import_core.createLogger)({ level: "info", format: "pretty" });
|
|
182
|
-
this.logger.debug("InMemory driver instance created"
|
|
86
|
+
this.logger.debug("InMemory driver instance created");
|
|
183
87
|
}
|
|
184
88
|
// Duck-typed RuntimePlugin hook
|
|
185
89
|
install(ctx) {
|
|
@@ -195,7 +99,20 @@ var InMemoryDriver = class {
|
|
|
195
99
|
// Lifecycle
|
|
196
100
|
// ===================================
|
|
197
101
|
async connect() {
|
|
198
|
-
this.
|
|
102
|
+
if (this.config.initialData) {
|
|
103
|
+
for (const [objectName, records] of Object.entries(this.config.initialData)) {
|
|
104
|
+
const table = this.getTable(objectName);
|
|
105
|
+
for (const record of records) {
|
|
106
|
+
const id = record.id || this.generateId(objectName);
|
|
107
|
+
table.push({ ...record, id });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
this.logger.info("InMemory Database Connected with initial data", {
|
|
111
|
+
tables: Object.keys(this.config.initialData).length
|
|
112
|
+
});
|
|
113
|
+
} else {
|
|
114
|
+
this.logger.info("InMemory Database Connected (Virtual)");
|
|
115
|
+
}
|
|
199
116
|
}
|
|
200
117
|
async disconnect() {
|
|
201
118
|
const tableCount = Object.keys(this.db).length;
|
|
@@ -226,25 +143,20 @@ var InMemoryDriver = class {
|
|
|
226
143
|
async find(object, query, options) {
|
|
227
144
|
this.logger.debug("Find operation", { object, query });
|
|
228
145
|
const table = this.getTable(object);
|
|
229
|
-
let results = table;
|
|
146
|
+
let results = [...table];
|
|
230
147
|
if (query.where) {
|
|
231
|
-
|
|
148
|
+
const mongoQuery = this.convertToMongoQuery(query.where);
|
|
149
|
+
if (mongoQuery && Object.keys(mongoQuery).length > 0) {
|
|
150
|
+
const mingoQuery = new import_mingo.Query(mongoQuery);
|
|
151
|
+
results = mingoQuery.find(results).all();
|
|
152
|
+
}
|
|
232
153
|
}
|
|
233
154
|
if (query.groupBy || query.aggregations && query.aggregations.length > 0) {
|
|
234
155
|
results = this.performAggregation(results, query);
|
|
235
156
|
}
|
|
236
157
|
if (query.orderBy) {
|
|
237
158
|
const sortFields = Array.isArray(query.orderBy) ? query.orderBy : [query.orderBy];
|
|
238
|
-
results.
|
|
239
|
-
for (const { field, order } of sortFields) {
|
|
240
|
-
const valA = getValueByPath(a, field);
|
|
241
|
-
const valB = getValueByPath(b, field);
|
|
242
|
-
if (valA === valB) continue;
|
|
243
|
-
const comparison = valA > valB ? 1 : -1;
|
|
244
|
-
return order === "desc" ? -comparison : comparison;
|
|
245
|
-
}
|
|
246
|
-
return 0;
|
|
247
|
-
});
|
|
159
|
+
results = this.applySort(results, sortFields);
|
|
248
160
|
}
|
|
249
161
|
if (query.offset) {
|
|
250
162
|
results = results.slice(query.offset);
|
|
@@ -252,6 +164,9 @@ var InMemoryDriver = class {
|
|
|
252
164
|
if (query.limit) {
|
|
253
165
|
results = results.slice(0, query.limit);
|
|
254
166
|
}
|
|
167
|
+
if (query.fields && Array.isArray(query.fields) && query.fields.length > 0) {
|
|
168
|
+
results = results.map((record) => this.projectFields(record, query.fields));
|
|
169
|
+
}
|
|
255
170
|
this.logger.debug("Find completed", { object, resultCount: results.length });
|
|
256
171
|
return results;
|
|
257
172
|
}
|
|
@@ -273,31 +188,38 @@ var InMemoryDriver = class {
|
|
|
273
188
|
this.logger.debug("Create operation", { object, hasData: !!data });
|
|
274
189
|
const table = this.getTable(object);
|
|
275
190
|
const newRecord = {
|
|
276
|
-
id: data.id || this.generateId(),
|
|
191
|
+
id: data.id || this.generateId(object),
|
|
277
192
|
...data,
|
|
278
|
-
created_at: data.created_at || /* @__PURE__ */ new Date(),
|
|
279
|
-
updated_at: data.updated_at || /* @__PURE__ */ new Date()
|
|
193
|
+
created_at: data.created_at || (/* @__PURE__ */ new Date()).toISOString(),
|
|
194
|
+
updated_at: data.updated_at || (/* @__PURE__ */ new Date()).toISOString()
|
|
280
195
|
};
|
|
281
196
|
table.push(newRecord);
|
|
282
197
|
this.logger.debug("Record created", { object, id: newRecord.id, tableSize: table.length });
|
|
283
|
-
return newRecord;
|
|
198
|
+
return { ...newRecord };
|
|
284
199
|
}
|
|
285
200
|
async update(object, id, data, options) {
|
|
286
201
|
this.logger.debug("Update operation", { object, id });
|
|
287
202
|
const table = this.getTable(object);
|
|
288
203
|
const index = table.findIndex((r) => r.id == id);
|
|
289
204
|
if (index === -1) {
|
|
290
|
-
this.
|
|
291
|
-
|
|
205
|
+
if (this.config.strictMode) {
|
|
206
|
+
this.logger.warn("Record not found for update", { object, id });
|
|
207
|
+
throw new Error(`Record with ID ${id} not found in ${object}`);
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
292
210
|
}
|
|
293
211
|
const updatedRecord = {
|
|
294
212
|
...table[index],
|
|
295
213
|
...data,
|
|
296
|
-
|
|
214
|
+
id: table[index].id,
|
|
215
|
+
// Preserve original ID
|
|
216
|
+
created_at: table[index].created_at,
|
|
217
|
+
// Preserve created_at
|
|
218
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
297
219
|
};
|
|
298
220
|
table[index] = updatedRecord;
|
|
299
221
|
this.logger.debug("Record updated", { object, id });
|
|
300
|
-
return updatedRecord;
|
|
222
|
+
return { ...updatedRecord };
|
|
301
223
|
}
|
|
302
224
|
async upsert(object, data, conflictKeys, options) {
|
|
303
225
|
this.logger.debug("Upsert operation", { object, conflictKeys });
|
|
@@ -321,6 +243,9 @@ var InMemoryDriver = class {
|
|
|
321
243
|
const table = this.getTable(object);
|
|
322
244
|
const index = table.findIndex((r) => r.id == id);
|
|
323
245
|
if (index === -1) {
|
|
246
|
+
if (this.config.strictMode) {
|
|
247
|
+
throw new Error(`Record with ID ${id} not found in ${object}`);
|
|
248
|
+
}
|
|
324
249
|
this.logger.warn("Record not found for deletion", { object, id });
|
|
325
250
|
return false;
|
|
326
251
|
}
|
|
@@ -329,11 +254,15 @@ var InMemoryDriver = class {
|
|
|
329
254
|
return true;
|
|
330
255
|
}
|
|
331
256
|
async count(object, query, options) {
|
|
332
|
-
let
|
|
257
|
+
let records = this.getTable(object);
|
|
333
258
|
if (query?.where) {
|
|
334
|
-
|
|
259
|
+
const mongoQuery = this.convertToMongoQuery(query.where);
|
|
260
|
+
if (mongoQuery && Object.keys(mongoQuery).length > 0) {
|
|
261
|
+
const mingoQuery = new import_mingo.Query(mongoQuery);
|
|
262
|
+
records = mingoQuery.find(records).all();
|
|
263
|
+
}
|
|
335
264
|
}
|
|
336
|
-
const count =
|
|
265
|
+
const count = records.length;
|
|
337
266
|
this.logger.debug("Count operation", { object, count });
|
|
338
267
|
return count;
|
|
339
268
|
}
|
|
@@ -351,7 +280,11 @@ var InMemoryDriver = class {
|
|
|
351
280
|
const table = this.getTable(object);
|
|
352
281
|
let targetRecords = table;
|
|
353
282
|
if (query && query.where) {
|
|
354
|
-
|
|
283
|
+
const mongoQuery = this.convertToMongoQuery(query.where);
|
|
284
|
+
if (mongoQuery && Object.keys(mongoQuery).length > 0) {
|
|
285
|
+
const mingoQuery = new import_mingo.Query(mongoQuery);
|
|
286
|
+
targetRecords = mingoQuery.find(targetRecords).all();
|
|
287
|
+
}
|
|
355
288
|
}
|
|
356
289
|
const count = targetRecords.length;
|
|
357
290
|
for (const record of targetRecords) {
|
|
@@ -360,7 +293,7 @@ var InMemoryDriver = class {
|
|
|
360
293
|
const updated = {
|
|
361
294
|
...table[index],
|
|
362
295
|
...data,
|
|
363
|
-
updated_at: /* @__PURE__ */ new Date()
|
|
296
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
364
297
|
};
|
|
365
298
|
table[index] = updated;
|
|
366
299
|
}
|
|
@@ -372,13 +305,20 @@ var InMemoryDriver = class {
|
|
|
372
305
|
this.logger.debug("DeleteMany operation", { object, query });
|
|
373
306
|
const table = this.getTable(object);
|
|
374
307
|
const initialLength = table.length;
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
308
|
+
if (query && query.where) {
|
|
309
|
+
const mongoQuery = this.convertToMongoQuery(query.where);
|
|
310
|
+
if (mongoQuery && Object.keys(mongoQuery).length > 0) {
|
|
311
|
+
const mingoQuery = new import_mingo.Query(mongoQuery);
|
|
312
|
+
const matched = mingoQuery.find(table).all();
|
|
313
|
+
const matchedIds = new Set(matched.map((r) => r.id));
|
|
314
|
+
this.db[object] = table.filter((r) => !matchedIds.has(r.id));
|
|
315
|
+
} else {
|
|
316
|
+
this.db[object] = [];
|
|
317
|
+
}
|
|
318
|
+
} else {
|
|
319
|
+
this.db[object] = [];
|
|
320
|
+
}
|
|
321
|
+
const count = initialLength - this.db[object].length;
|
|
382
322
|
this.logger.debug("DeleteMany completed", { object, count });
|
|
383
323
|
return { count };
|
|
384
324
|
}
|
|
@@ -395,27 +335,213 @@ var InMemoryDriver = class {
|
|
|
395
335
|
this.logger.debug("BulkDelete completed", { object, count: ids.length });
|
|
396
336
|
}
|
|
397
337
|
// ===================================
|
|
398
|
-
//
|
|
338
|
+
// Transaction Management
|
|
399
339
|
// ===================================
|
|
400
|
-
async
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
340
|
+
async beginTransaction() {
|
|
341
|
+
const txId = `tx_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
342
|
+
const snapshot = {};
|
|
343
|
+
for (const [table, records] of Object.entries(this.db)) {
|
|
344
|
+
snapshot[table] = records.map((r) => ({ ...r }));
|
|
404
345
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
346
|
+
const transaction = { id: txId, snapshot };
|
|
347
|
+
this.transactions.set(txId, transaction);
|
|
348
|
+
this.logger.debug("Transaction started", { txId });
|
|
349
|
+
return { id: txId };
|
|
350
|
+
}
|
|
351
|
+
async commit(txHandle) {
|
|
352
|
+
const txId = txHandle?.id;
|
|
353
|
+
if (!txId || !this.transactions.has(txId)) {
|
|
354
|
+
this.logger.warn("Commit called with unknown transaction");
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
this.transactions.delete(txId);
|
|
358
|
+
this.logger.debug("Transaction committed", { txId });
|
|
359
|
+
}
|
|
360
|
+
async rollback(txHandle) {
|
|
361
|
+
const txId = txHandle?.id;
|
|
362
|
+
if (!txId || !this.transactions.has(txId)) {
|
|
363
|
+
this.logger.warn("Rollback called with unknown transaction");
|
|
364
|
+
return;
|
|
411
365
|
}
|
|
366
|
+
const tx = this.transactions.get(txId);
|
|
367
|
+
this.db = tx.snapshot;
|
|
368
|
+
this.transactions.delete(txId);
|
|
369
|
+
this.logger.debug("Transaction rolled back", { txId });
|
|
412
370
|
}
|
|
413
|
-
|
|
414
|
-
|
|
371
|
+
// ===================================
|
|
372
|
+
// Utility Methods
|
|
373
|
+
// ===================================
|
|
374
|
+
/**
|
|
375
|
+
* Remove all data from the store.
|
|
376
|
+
*/
|
|
377
|
+
async clear() {
|
|
378
|
+
this.db = {};
|
|
379
|
+
this.idCounters.clear();
|
|
380
|
+
this.logger.debug("All data cleared");
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Get total number of records across all tables.
|
|
384
|
+
*/
|
|
385
|
+
getSize() {
|
|
386
|
+
return Object.values(this.db).reduce((sum, table) => sum + table.length, 0);
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Get distinct values for a field, optionally filtered.
|
|
390
|
+
*/
|
|
391
|
+
async distinct(object, field, query) {
|
|
392
|
+
let records = this.getTable(object);
|
|
393
|
+
if (query?.where) {
|
|
394
|
+
const mongoQuery = this.convertToMongoQuery(query.where);
|
|
395
|
+
if (mongoQuery && Object.keys(mongoQuery).length > 0) {
|
|
396
|
+
const mingoQuery = new import_mingo.Query(mongoQuery);
|
|
397
|
+
records = mingoQuery.find(records).all();
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const values = /* @__PURE__ */ new Set();
|
|
401
|
+
for (const record of records) {
|
|
402
|
+
const value = getValueByPath(record, field);
|
|
403
|
+
if (value !== void 0 && value !== null) {
|
|
404
|
+
values.add(value);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return Array.from(values);
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Execute a MongoDB-style aggregation pipeline using Mingo.
|
|
411
|
+
*
|
|
412
|
+
* Supports all standard MongoDB pipeline stages:
|
|
413
|
+
* - $match, $group, $sort, $project, $unwind, $limit, $skip
|
|
414
|
+
* - $addFields, $replaceRoot, $lookup (limited), $count
|
|
415
|
+
* - Accumulator operators: $sum, $avg, $min, $max, $first, $last, $push, $addToSet
|
|
416
|
+
*
|
|
417
|
+
* @example
|
|
418
|
+
* // Group by status and count
|
|
419
|
+
* const results = await driver.aggregate('orders', [
|
|
420
|
+
* { $match: { status: 'completed' } },
|
|
421
|
+
* { $group: { _id: '$customer', totalAmount: { $sum: '$amount' } } }
|
|
422
|
+
* ]);
|
|
423
|
+
*
|
|
424
|
+
* @example
|
|
425
|
+
* // Calculate average with filter
|
|
426
|
+
* const results = await driver.aggregate('products', [
|
|
427
|
+
* { $match: { category: 'electronics' } },
|
|
428
|
+
* { $group: { _id: null, avgPrice: { $avg: '$price' } } }
|
|
429
|
+
* ]);
|
|
430
|
+
*/
|
|
431
|
+
async aggregate(object, pipeline, options) {
|
|
432
|
+
this.logger.debug("Aggregate operation", { object, stageCount: pipeline.length });
|
|
433
|
+
const records = this.getTable(object).map((r) => ({ ...r }));
|
|
434
|
+
const aggregator = new import_mingo.Aggregator(pipeline);
|
|
435
|
+
const results = aggregator.run(records);
|
|
436
|
+
this.logger.debug("Aggregate completed", { object, resultCount: results.length });
|
|
437
|
+
return results;
|
|
415
438
|
}
|
|
416
|
-
|
|
439
|
+
// ===================================
|
|
440
|
+
// Query Conversion (ObjectQL → MongoDB)
|
|
441
|
+
// ===================================
|
|
442
|
+
/**
|
|
443
|
+
* Convert ObjectQL filter format to MongoDB query format for Mingo.
|
|
444
|
+
*
|
|
445
|
+
* Supports:
|
|
446
|
+
* 1. AST Comparison Node: { type: 'comparison', field, operator, value }
|
|
447
|
+
* 2. AST Logical Node: { type: 'logical', operator: 'and'|'or', conditions: [...] }
|
|
448
|
+
* 3. Legacy Array Format: [['field', 'op', value], 'and', ['field2', 'op', value2]]
|
|
449
|
+
* 4. MongoDB Format: { field: value } or { field: { $eq: value } } (passthrough)
|
|
450
|
+
*/
|
|
451
|
+
convertToMongoQuery(filters) {
|
|
452
|
+
if (!filters) return {};
|
|
453
|
+
if (!Array.isArray(filters) && typeof filters === "object") {
|
|
454
|
+
if (filters.type === "comparison") {
|
|
455
|
+
return this.convertConditionToMongo(filters.field, filters.operator, filters.value) || {};
|
|
456
|
+
}
|
|
457
|
+
if (filters.type === "logical") {
|
|
458
|
+
const conditions = filters.conditions?.map((c) => this.convertToMongoQuery(c)) || [];
|
|
459
|
+
if (conditions.length === 0) return {};
|
|
460
|
+
if (conditions.length === 1) return conditions[0];
|
|
461
|
+
const op = filters.operator === "or" ? "$or" : "$and";
|
|
462
|
+
return { [op]: conditions };
|
|
463
|
+
}
|
|
464
|
+
return filters;
|
|
465
|
+
}
|
|
466
|
+
if (!Array.isArray(filters) || filters.length === 0) return {};
|
|
467
|
+
const logicGroups = [
|
|
468
|
+
{ logic: "and", conditions: [] }
|
|
469
|
+
];
|
|
470
|
+
let currentLogic = "and";
|
|
471
|
+
for (const item of filters) {
|
|
472
|
+
if (typeof item === "string") {
|
|
473
|
+
const newLogic = item.toLowerCase();
|
|
474
|
+
if (newLogic !== currentLogic) {
|
|
475
|
+
currentLogic = newLogic;
|
|
476
|
+
logicGroups.push({ logic: currentLogic, conditions: [] });
|
|
477
|
+
}
|
|
478
|
+
} else if (Array.isArray(item)) {
|
|
479
|
+
const [field, operator, value] = item;
|
|
480
|
+
const cond = this.convertConditionToMongo(field, operator, value);
|
|
481
|
+
if (cond) logicGroups[logicGroups.length - 1].conditions.push(cond);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
const allConditions = [];
|
|
485
|
+
for (const group of logicGroups) {
|
|
486
|
+
if (group.conditions.length === 0) continue;
|
|
487
|
+
if (group.conditions.length === 1) {
|
|
488
|
+
allConditions.push(group.conditions[0]);
|
|
489
|
+
} else {
|
|
490
|
+
const op = group.logic === "or" ? "$or" : "$and";
|
|
491
|
+
allConditions.push({ [op]: group.conditions });
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (allConditions.length === 0) return {};
|
|
495
|
+
if (allConditions.length === 1) return allConditions[0];
|
|
496
|
+
return { $and: allConditions };
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Convert a single ObjectQL condition to MongoDB operator format.
|
|
500
|
+
*/
|
|
501
|
+
convertConditionToMongo(field, operator, value) {
|
|
502
|
+
switch (operator) {
|
|
503
|
+
case "=":
|
|
504
|
+
case "==":
|
|
505
|
+
return { [field]: value };
|
|
506
|
+
case "!=":
|
|
507
|
+
case "<>":
|
|
508
|
+
return { [field]: { $ne: value } };
|
|
509
|
+
case ">":
|
|
510
|
+
return { [field]: { $gt: value } };
|
|
511
|
+
case ">=":
|
|
512
|
+
return { [field]: { $gte: value } };
|
|
513
|
+
case "<":
|
|
514
|
+
return { [field]: { $lt: value } };
|
|
515
|
+
case "<=":
|
|
516
|
+
return { [field]: { $lte: value } };
|
|
517
|
+
case "in":
|
|
518
|
+
return { [field]: { $in: value } };
|
|
519
|
+
case "nin":
|
|
520
|
+
case "not in":
|
|
521
|
+
return { [field]: { $nin: value } };
|
|
522
|
+
case "contains":
|
|
523
|
+
case "like":
|
|
524
|
+
return { [field]: { $regex: new RegExp(this.escapeRegex(value), "i") } };
|
|
525
|
+
case "startswith":
|
|
526
|
+
case "starts_with":
|
|
527
|
+
return { [field]: { $regex: new RegExp(`^${this.escapeRegex(value)}`, "i") } };
|
|
528
|
+
case "endswith":
|
|
529
|
+
case "ends_with":
|
|
530
|
+
return { [field]: { $regex: new RegExp(`${this.escapeRegex(value)}$`, "i") } };
|
|
531
|
+
case "between":
|
|
532
|
+
if (Array.isArray(value) && value.length === 2) {
|
|
533
|
+
return { [field]: { $gte: value[0], $lte: value[1] } };
|
|
534
|
+
}
|
|
535
|
+
return null;
|
|
536
|
+
default:
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
417
539
|
}
|
|
418
|
-
|
|
540
|
+
/**
|
|
541
|
+
* Escape special regex characters for safe literal matching.
|
|
542
|
+
*/
|
|
543
|
+
escapeRegex(str) {
|
|
544
|
+
return String(str).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
419
545
|
}
|
|
420
546
|
// ===================================
|
|
421
547
|
// Aggregation Logic
|
|
@@ -436,14 +562,10 @@ var InMemoryDriver = class {
|
|
|
436
562
|
groups.get(key).push(record);
|
|
437
563
|
}
|
|
438
564
|
} else {
|
|
439
|
-
|
|
440
|
-
groups.set("all", records);
|
|
441
|
-
} else {
|
|
442
|
-
groups.set("all", records);
|
|
443
|
-
}
|
|
565
|
+
groups.set("all", records);
|
|
444
566
|
}
|
|
445
567
|
const resultRows = [];
|
|
446
|
-
for (const [
|
|
568
|
+
for (const [_key, groupRecords] of groups.entries()) {
|
|
447
569
|
const row = {};
|
|
448
570
|
if (groupBy && groupBy.length > 0) {
|
|
449
571
|
if (groupRecords.length > 0) {
|
|
@@ -502,16 +624,82 @@ var InMemoryDriver = class {
|
|
|
502
624
|
current[parts[parts.length - 1]] = value;
|
|
503
625
|
}
|
|
504
626
|
// ===================================
|
|
627
|
+
// Schema Management
|
|
628
|
+
// ===================================
|
|
629
|
+
async syncSchema(object, schema, options) {
|
|
630
|
+
if (!this.db[object]) {
|
|
631
|
+
this.db[object] = [];
|
|
632
|
+
this.logger.info("Created in-memory table", { object });
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
async dropTable(object, options) {
|
|
636
|
+
if (this.db[object]) {
|
|
637
|
+
const recordCount = this.db[object].length;
|
|
638
|
+
delete this.db[object];
|
|
639
|
+
this.logger.info("Dropped in-memory table", { object, recordCount });
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
// ===================================
|
|
505
643
|
// Helpers
|
|
506
644
|
// ===================================
|
|
645
|
+
/**
|
|
646
|
+
* Apply manual sorting (Mingo sort has CJS build issues).
|
|
647
|
+
*/
|
|
648
|
+
applySort(records, sortFields) {
|
|
649
|
+
const sorted = [...records];
|
|
650
|
+
for (let i = sortFields.length - 1; i >= 0; i--) {
|
|
651
|
+
const sortItem = sortFields[i];
|
|
652
|
+
let field;
|
|
653
|
+
let direction;
|
|
654
|
+
if (typeof sortItem === "object" && !Array.isArray(sortItem)) {
|
|
655
|
+
field = sortItem.field;
|
|
656
|
+
direction = sortItem.order || sortItem.direction || "asc";
|
|
657
|
+
} else if (Array.isArray(sortItem)) {
|
|
658
|
+
[field, direction] = sortItem;
|
|
659
|
+
} else {
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
sorted.sort((a, b) => {
|
|
663
|
+
const aVal = getValueByPath(a, field);
|
|
664
|
+
const bVal = getValueByPath(b, field);
|
|
665
|
+
if (aVal == null && bVal == null) return 0;
|
|
666
|
+
if (aVal == null) return 1;
|
|
667
|
+
if (bVal == null) return -1;
|
|
668
|
+
if (aVal < bVal) return direction === "desc" ? 1 : -1;
|
|
669
|
+
if (aVal > bVal) return direction === "desc" ? -1 : 1;
|
|
670
|
+
return 0;
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
return sorted;
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Project specific fields from a record.
|
|
677
|
+
*/
|
|
678
|
+
projectFields(record, fields) {
|
|
679
|
+
const result = {};
|
|
680
|
+
for (const field of fields) {
|
|
681
|
+
const value = getValueByPath(record, field);
|
|
682
|
+
if (value !== void 0) {
|
|
683
|
+
result[field] = value;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
if (!fields.includes("id") && record.id !== void 0) {
|
|
687
|
+
result.id = record.id;
|
|
688
|
+
}
|
|
689
|
+
return result;
|
|
690
|
+
}
|
|
507
691
|
getTable(name) {
|
|
508
692
|
if (!this.db[name]) {
|
|
509
693
|
this.db[name] = [];
|
|
510
694
|
}
|
|
511
695
|
return this.db[name];
|
|
512
696
|
}
|
|
513
|
-
generateId() {
|
|
514
|
-
|
|
697
|
+
generateId(objectName) {
|
|
698
|
+
const key = objectName || "_global";
|
|
699
|
+
const counter = (this.idCounters.get(key) || 0) + 1;
|
|
700
|
+
this.idCounters.set(key, counter);
|
|
701
|
+
const timestamp = Date.now();
|
|
702
|
+
return `${key}-${timestamp}-${counter}`;
|
|
515
703
|
}
|
|
516
704
|
};
|
|
517
705
|
|