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