@objectstack/driver-memory 2.0.6 → 3.0.0
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 +20 -0
- package/dist/index.d.mts +165 -12
- package/dist/index.d.ts +165 -12
- package/dist/index.js +742 -159
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +740 -158
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -5
- package/src/index.ts +4 -12
- package/src/memory-analytics.test.ts +346 -0
- package/src/memory-analytics.ts +518 -0
- package/src/memory-driver.test.ts +473 -1
- package/src/memory-driver.ts +416 -85
package/dist/index.js
CHANGED
|
@@ -21,41 +21,16 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
InMemoryDriver: () => InMemoryDriver,
|
|
24
|
+
MemoryAnalyticsService: () => MemoryAnalyticsService,
|
|
24
25
|
default: () => index_default
|
|
25
26
|
});
|
|
26
27
|
module.exports = __toCommonJS(index_exports);
|
|
27
28
|
|
|
28
29
|
// src/memory-driver.ts
|
|
29
30
|
var import_core = require("@objectstack/core");
|
|
31
|
+
var import_mingo = require("mingo");
|
|
30
32
|
|
|
31
33
|
// 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
34
|
function getValueByPath(obj, path) {
|
|
60
35
|
if (path === "_id" && obj._id === void 0 && obj.id !== void 0) {
|
|
61
36
|
return obj.id;
|
|
@@ -63,89 +38,19 @@ function getValueByPath(obj, path) {
|
|
|
63
38
|
if (!path.includes(".")) return obj[path];
|
|
64
39
|
return path.split(".").reduce((o, i) => o ? o[i] : void 0, obj);
|
|
65
40
|
}
|
|
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
41
|
|
|
140
42
|
// src/memory-driver.ts
|
|
141
43
|
var InMemoryDriver = class {
|
|
142
44
|
constructor(config) {
|
|
143
45
|
this.name = "com.objectstack.driver.memory";
|
|
144
46
|
this.type = "driver";
|
|
145
|
-
this.version = "0.0
|
|
47
|
+
this.version = "1.0.0";
|
|
48
|
+
this.idCounters = /* @__PURE__ */ new Map();
|
|
49
|
+
this.transactions = /* @__PURE__ */ new Map();
|
|
146
50
|
this.supports = {
|
|
147
51
|
// Transaction & Connection Management
|
|
148
|
-
transactions:
|
|
52
|
+
transactions: true,
|
|
53
|
+
// Snapshot-based transactions
|
|
149
54
|
// Query Operations
|
|
150
55
|
queryFilters: true,
|
|
151
56
|
// Implemented via memory-matcher
|
|
@@ -179,7 +84,7 @@ var InMemoryDriver = class {
|
|
|
179
84
|
this.db = {};
|
|
180
85
|
this.config = config || {};
|
|
181
86
|
this.logger = config?.logger || (0, import_core.createLogger)({ level: "info", format: "pretty" });
|
|
182
|
-
this.logger.debug("InMemory driver instance created"
|
|
87
|
+
this.logger.debug("InMemory driver instance created");
|
|
183
88
|
}
|
|
184
89
|
// Duck-typed RuntimePlugin hook
|
|
185
90
|
install(ctx) {
|
|
@@ -195,7 +100,20 @@ var InMemoryDriver = class {
|
|
|
195
100
|
// Lifecycle
|
|
196
101
|
// ===================================
|
|
197
102
|
async connect() {
|
|
198
|
-
this.
|
|
103
|
+
if (this.config.initialData) {
|
|
104
|
+
for (const [objectName, records] of Object.entries(this.config.initialData)) {
|
|
105
|
+
const table = this.getTable(objectName);
|
|
106
|
+
for (const record of records) {
|
|
107
|
+
const id = record.id || this.generateId(objectName);
|
|
108
|
+
table.push({ ...record, id });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
this.logger.info("InMemory Database Connected with initial data", {
|
|
112
|
+
tables: Object.keys(this.config.initialData).length
|
|
113
|
+
});
|
|
114
|
+
} else {
|
|
115
|
+
this.logger.info("InMemory Database Connected (Virtual)");
|
|
116
|
+
}
|
|
199
117
|
}
|
|
200
118
|
async disconnect() {
|
|
201
119
|
const tableCount = Object.keys(this.db).length;
|
|
@@ -226,25 +144,20 @@ var InMemoryDriver = class {
|
|
|
226
144
|
async find(object, query, options) {
|
|
227
145
|
this.logger.debug("Find operation", { object, query });
|
|
228
146
|
const table = this.getTable(object);
|
|
229
|
-
let results = table;
|
|
147
|
+
let results = [...table];
|
|
230
148
|
if (query.where) {
|
|
231
|
-
|
|
149
|
+
const mongoQuery = this.convertToMongoQuery(query.where);
|
|
150
|
+
if (mongoQuery && Object.keys(mongoQuery).length > 0) {
|
|
151
|
+
const mingoQuery = new import_mingo.Query(mongoQuery);
|
|
152
|
+
results = mingoQuery.find(results).all();
|
|
153
|
+
}
|
|
232
154
|
}
|
|
233
155
|
if (query.groupBy || query.aggregations && query.aggregations.length > 0) {
|
|
234
156
|
results = this.performAggregation(results, query);
|
|
235
157
|
}
|
|
236
158
|
if (query.orderBy) {
|
|
237
159
|
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
|
-
});
|
|
160
|
+
results = this.applySort(results, sortFields);
|
|
248
161
|
}
|
|
249
162
|
if (query.offset) {
|
|
250
163
|
results = results.slice(query.offset);
|
|
@@ -252,6 +165,9 @@ var InMemoryDriver = class {
|
|
|
252
165
|
if (query.limit) {
|
|
253
166
|
results = results.slice(0, query.limit);
|
|
254
167
|
}
|
|
168
|
+
if (query.fields && Array.isArray(query.fields) && query.fields.length > 0) {
|
|
169
|
+
results = results.map((record) => this.projectFields(record, query.fields));
|
|
170
|
+
}
|
|
255
171
|
this.logger.debug("Find completed", { object, resultCount: results.length });
|
|
256
172
|
return results;
|
|
257
173
|
}
|
|
@@ -273,31 +189,38 @@ var InMemoryDriver = class {
|
|
|
273
189
|
this.logger.debug("Create operation", { object, hasData: !!data });
|
|
274
190
|
const table = this.getTable(object);
|
|
275
191
|
const newRecord = {
|
|
276
|
-
id: data.id || this.generateId(),
|
|
192
|
+
id: data.id || this.generateId(object),
|
|
277
193
|
...data,
|
|
278
|
-
created_at: data.created_at || /* @__PURE__ */ new Date(),
|
|
279
|
-
updated_at: data.updated_at || /* @__PURE__ */ new Date()
|
|
194
|
+
created_at: data.created_at || (/* @__PURE__ */ new Date()).toISOString(),
|
|
195
|
+
updated_at: data.updated_at || (/* @__PURE__ */ new Date()).toISOString()
|
|
280
196
|
};
|
|
281
197
|
table.push(newRecord);
|
|
282
198
|
this.logger.debug("Record created", { object, id: newRecord.id, tableSize: table.length });
|
|
283
|
-
return newRecord;
|
|
199
|
+
return { ...newRecord };
|
|
284
200
|
}
|
|
285
201
|
async update(object, id, data, options) {
|
|
286
202
|
this.logger.debug("Update operation", { object, id });
|
|
287
203
|
const table = this.getTable(object);
|
|
288
204
|
const index = table.findIndex((r) => r.id == id);
|
|
289
205
|
if (index === -1) {
|
|
290
|
-
this.
|
|
291
|
-
|
|
206
|
+
if (this.config.strictMode) {
|
|
207
|
+
this.logger.warn("Record not found for update", { object, id });
|
|
208
|
+
throw new Error(`Record with ID ${id} not found in ${object}`);
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
292
211
|
}
|
|
293
212
|
const updatedRecord = {
|
|
294
213
|
...table[index],
|
|
295
214
|
...data,
|
|
296
|
-
|
|
215
|
+
id: table[index].id,
|
|
216
|
+
// Preserve original ID
|
|
217
|
+
created_at: table[index].created_at,
|
|
218
|
+
// Preserve created_at
|
|
219
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
297
220
|
};
|
|
298
221
|
table[index] = updatedRecord;
|
|
299
222
|
this.logger.debug("Record updated", { object, id });
|
|
300
|
-
return updatedRecord;
|
|
223
|
+
return { ...updatedRecord };
|
|
301
224
|
}
|
|
302
225
|
async upsert(object, data, conflictKeys, options) {
|
|
303
226
|
this.logger.debug("Upsert operation", { object, conflictKeys });
|
|
@@ -321,6 +244,9 @@ var InMemoryDriver = class {
|
|
|
321
244
|
const table = this.getTable(object);
|
|
322
245
|
const index = table.findIndex((r) => r.id == id);
|
|
323
246
|
if (index === -1) {
|
|
247
|
+
if (this.config.strictMode) {
|
|
248
|
+
throw new Error(`Record with ID ${id} not found in ${object}`);
|
|
249
|
+
}
|
|
324
250
|
this.logger.warn("Record not found for deletion", { object, id });
|
|
325
251
|
return false;
|
|
326
252
|
}
|
|
@@ -329,11 +255,15 @@ var InMemoryDriver = class {
|
|
|
329
255
|
return true;
|
|
330
256
|
}
|
|
331
257
|
async count(object, query, options) {
|
|
332
|
-
let
|
|
258
|
+
let records = this.getTable(object);
|
|
333
259
|
if (query?.where) {
|
|
334
|
-
|
|
260
|
+
const mongoQuery = this.convertToMongoQuery(query.where);
|
|
261
|
+
if (mongoQuery && Object.keys(mongoQuery).length > 0) {
|
|
262
|
+
const mingoQuery = new import_mingo.Query(mongoQuery);
|
|
263
|
+
records = mingoQuery.find(records).all();
|
|
264
|
+
}
|
|
335
265
|
}
|
|
336
|
-
const count =
|
|
266
|
+
const count = records.length;
|
|
337
267
|
this.logger.debug("Count operation", { object, count });
|
|
338
268
|
return count;
|
|
339
269
|
}
|
|
@@ -351,7 +281,11 @@ var InMemoryDriver = class {
|
|
|
351
281
|
const table = this.getTable(object);
|
|
352
282
|
let targetRecords = table;
|
|
353
283
|
if (query && query.where) {
|
|
354
|
-
|
|
284
|
+
const mongoQuery = this.convertToMongoQuery(query.where);
|
|
285
|
+
if (mongoQuery && Object.keys(mongoQuery).length > 0) {
|
|
286
|
+
const mingoQuery = new import_mingo.Query(mongoQuery);
|
|
287
|
+
targetRecords = mingoQuery.find(targetRecords).all();
|
|
288
|
+
}
|
|
355
289
|
}
|
|
356
290
|
const count = targetRecords.length;
|
|
357
291
|
for (const record of targetRecords) {
|
|
@@ -360,7 +294,7 @@ var InMemoryDriver = class {
|
|
|
360
294
|
const updated = {
|
|
361
295
|
...table[index],
|
|
362
296
|
...data,
|
|
363
|
-
updated_at: /* @__PURE__ */ new Date()
|
|
297
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
364
298
|
};
|
|
365
299
|
table[index] = updated;
|
|
366
300
|
}
|
|
@@ -372,13 +306,20 @@ var InMemoryDriver = class {
|
|
|
372
306
|
this.logger.debug("DeleteMany operation", { object, query });
|
|
373
307
|
const table = this.getTable(object);
|
|
374
308
|
const initialLength = table.length;
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
309
|
+
if (query && query.where) {
|
|
310
|
+
const mongoQuery = this.convertToMongoQuery(query.where);
|
|
311
|
+
if (mongoQuery && Object.keys(mongoQuery).length > 0) {
|
|
312
|
+
const mingoQuery = new import_mingo.Query(mongoQuery);
|
|
313
|
+
const matched = mingoQuery.find(table).all();
|
|
314
|
+
const matchedIds = new Set(matched.map((r) => r.id));
|
|
315
|
+
this.db[object] = table.filter((r) => !matchedIds.has(r.id));
|
|
316
|
+
} else {
|
|
317
|
+
this.db[object] = [];
|
|
318
|
+
}
|
|
319
|
+
} else {
|
|
320
|
+
this.db[object] = [];
|
|
321
|
+
}
|
|
322
|
+
const count = initialLength - this.db[object].length;
|
|
382
323
|
this.logger.debug("DeleteMany completed", { object, count });
|
|
383
324
|
return { count };
|
|
384
325
|
}
|
|
@@ -395,27 +336,213 @@ var InMemoryDriver = class {
|
|
|
395
336
|
this.logger.debug("BulkDelete completed", { object, count: ids.length });
|
|
396
337
|
}
|
|
397
338
|
// ===================================
|
|
398
|
-
//
|
|
339
|
+
// Transaction Management
|
|
399
340
|
// ===================================
|
|
400
|
-
async
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
341
|
+
async beginTransaction() {
|
|
342
|
+
const txId = `tx_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
343
|
+
const snapshot = {};
|
|
344
|
+
for (const [table, records] of Object.entries(this.db)) {
|
|
345
|
+
snapshot[table] = records.map((r) => ({ ...r }));
|
|
404
346
|
}
|
|
347
|
+
const transaction = { id: txId, snapshot };
|
|
348
|
+
this.transactions.set(txId, transaction);
|
|
349
|
+
this.logger.debug("Transaction started", { txId });
|
|
350
|
+
return { id: txId };
|
|
405
351
|
}
|
|
406
|
-
async
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
352
|
+
async commit(txHandle) {
|
|
353
|
+
const txId = txHandle?.id;
|
|
354
|
+
if (!txId || !this.transactions.has(txId)) {
|
|
355
|
+
this.logger.warn("Commit called with unknown transaction");
|
|
356
|
+
return;
|
|
411
357
|
}
|
|
358
|
+
this.transactions.delete(txId);
|
|
359
|
+
this.logger.debug("Transaction committed", { txId });
|
|
412
360
|
}
|
|
413
|
-
async
|
|
414
|
-
|
|
361
|
+
async rollback(txHandle) {
|
|
362
|
+
const txId = txHandle?.id;
|
|
363
|
+
if (!txId || !this.transactions.has(txId)) {
|
|
364
|
+
this.logger.warn("Rollback called with unknown transaction");
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const tx = this.transactions.get(txId);
|
|
368
|
+
this.db = tx.snapshot;
|
|
369
|
+
this.transactions.delete(txId);
|
|
370
|
+
this.logger.debug("Transaction rolled back", { txId });
|
|
371
|
+
}
|
|
372
|
+
// ===================================
|
|
373
|
+
// Utility Methods
|
|
374
|
+
// ===================================
|
|
375
|
+
/**
|
|
376
|
+
* Remove all data from the store.
|
|
377
|
+
*/
|
|
378
|
+
async clear() {
|
|
379
|
+
this.db = {};
|
|
380
|
+
this.idCounters.clear();
|
|
381
|
+
this.logger.debug("All data cleared");
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Get total number of records across all tables.
|
|
385
|
+
*/
|
|
386
|
+
getSize() {
|
|
387
|
+
return Object.values(this.db).reduce((sum, table) => sum + table.length, 0);
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Get distinct values for a field, optionally filtered.
|
|
391
|
+
*/
|
|
392
|
+
async distinct(object, field, query) {
|
|
393
|
+
let records = this.getTable(object);
|
|
394
|
+
if (query?.where) {
|
|
395
|
+
const mongoQuery = this.convertToMongoQuery(query.where);
|
|
396
|
+
if (mongoQuery && Object.keys(mongoQuery).length > 0) {
|
|
397
|
+
const mingoQuery = new import_mingo.Query(mongoQuery);
|
|
398
|
+
records = mingoQuery.find(records).all();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const values = /* @__PURE__ */ new Set();
|
|
402
|
+
for (const record of records) {
|
|
403
|
+
const value = getValueByPath(record, field);
|
|
404
|
+
if (value !== void 0 && value !== null) {
|
|
405
|
+
values.add(value);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return Array.from(values);
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Execute a MongoDB-style aggregation pipeline using Mingo.
|
|
412
|
+
*
|
|
413
|
+
* Supports all standard MongoDB pipeline stages:
|
|
414
|
+
* - $match, $group, $sort, $project, $unwind, $limit, $skip
|
|
415
|
+
* - $addFields, $replaceRoot, $lookup (limited), $count
|
|
416
|
+
* - Accumulator operators: $sum, $avg, $min, $max, $first, $last, $push, $addToSet
|
|
417
|
+
*
|
|
418
|
+
* @example
|
|
419
|
+
* // Group by status and count
|
|
420
|
+
* const results = await driver.aggregate('orders', [
|
|
421
|
+
* { $match: { status: 'completed' } },
|
|
422
|
+
* { $group: { _id: '$customer', totalAmount: { $sum: '$amount' } } }
|
|
423
|
+
* ]);
|
|
424
|
+
*
|
|
425
|
+
* @example
|
|
426
|
+
* // Calculate average with filter
|
|
427
|
+
* const results = await driver.aggregate('products', [
|
|
428
|
+
* { $match: { category: 'electronics' } },
|
|
429
|
+
* { $group: { _id: null, avgPrice: { $avg: '$price' } } }
|
|
430
|
+
* ]);
|
|
431
|
+
*/
|
|
432
|
+
async aggregate(object, pipeline, options) {
|
|
433
|
+
this.logger.debug("Aggregate operation", { object, stageCount: pipeline.length });
|
|
434
|
+
const records = this.getTable(object).map((r) => ({ ...r }));
|
|
435
|
+
const aggregator = new import_mingo.Aggregator(pipeline);
|
|
436
|
+
const results = aggregator.run(records);
|
|
437
|
+
this.logger.debug("Aggregate completed", { object, resultCount: results.length });
|
|
438
|
+
return results;
|
|
439
|
+
}
|
|
440
|
+
// ===================================
|
|
441
|
+
// Query Conversion (ObjectQL → MongoDB)
|
|
442
|
+
// ===================================
|
|
443
|
+
/**
|
|
444
|
+
* Convert ObjectQL filter format to MongoDB query format for Mingo.
|
|
445
|
+
*
|
|
446
|
+
* Supports:
|
|
447
|
+
* 1. AST Comparison Node: { type: 'comparison', field, operator, value }
|
|
448
|
+
* 2. AST Logical Node: { type: 'logical', operator: 'and'|'or', conditions: [...] }
|
|
449
|
+
* 3. Legacy Array Format: [['field', 'op', value], 'and', ['field2', 'op', value2]]
|
|
450
|
+
* 4. MongoDB Format: { field: value } or { field: { $eq: value } } (passthrough)
|
|
451
|
+
*/
|
|
452
|
+
convertToMongoQuery(filters) {
|
|
453
|
+
if (!filters) return {};
|
|
454
|
+
if (!Array.isArray(filters) && typeof filters === "object") {
|
|
455
|
+
if (filters.type === "comparison") {
|
|
456
|
+
return this.convertConditionToMongo(filters.field, filters.operator, filters.value) || {};
|
|
457
|
+
}
|
|
458
|
+
if (filters.type === "logical") {
|
|
459
|
+
const conditions = filters.conditions?.map((c) => this.convertToMongoQuery(c)) || [];
|
|
460
|
+
if (conditions.length === 0) return {};
|
|
461
|
+
if (conditions.length === 1) return conditions[0];
|
|
462
|
+
const op = filters.operator === "or" ? "$or" : "$and";
|
|
463
|
+
return { [op]: conditions };
|
|
464
|
+
}
|
|
465
|
+
return filters;
|
|
466
|
+
}
|
|
467
|
+
if (!Array.isArray(filters) || filters.length === 0) return {};
|
|
468
|
+
const logicGroups = [
|
|
469
|
+
{ logic: "and", conditions: [] }
|
|
470
|
+
];
|
|
471
|
+
let currentLogic = "and";
|
|
472
|
+
for (const item of filters) {
|
|
473
|
+
if (typeof item === "string") {
|
|
474
|
+
const newLogic = item.toLowerCase();
|
|
475
|
+
if (newLogic !== currentLogic) {
|
|
476
|
+
currentLogic = newLogic;
|
|
477
|
+
logicGroups.push({ logic: currentLogic, conditions: [] });
|
|
478
|
+
}
|
|
479
|
+
} else if (Array.isArray(item)) {
|
|
480
|
+
const [field, operator, value] = item;
|
|
481
|
+
const cond = this.convertConditionToMongo(field, operator, value);
|
|
482
|
+
if (cond) logicGroups[logicGroups.length - 1].conditions.push(cond);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
const allConditions = [];
|
|
486
|
+
for (const group of logicGroups) {
|
|
487
|
+
if (group.conditions.length === 0) continue;
|
|
488
|
+
if (group.conditions.length === 1) {
|
|
489
|
+
allConditions.push(group.conditions[0]);
|
|
490
|
+
} else {
|
|
491
|
+
const op = group.logic === "or" ? "$or" : "$and";
|
|
492
|
+
allConditions.push({ [op]: group.conditions });
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (allConditions.length === 0) return {};
|
|
496
|
+
if (allConditions.length === 1) return allConditions[0];
|
|
497
|
+
return { $and: allConditions };
|
|
415
498
|
}
|
|
416
|
-
|
|
499
|
+
/**
|
|
500
|
+
* Convert a single ObjectQL condition to MongoDB operator format.
|
|
501
|
+
*/
|
|
502
|
+
convertConditionToMongo(field, operator, value) {
|
|
503
|
+
switch (operator) {
|
|
504
|
+
case "=":
|
|
505
|
+
case "==":
|
|
506
|
+
return { [field]: value };
|
|
507
|
+
case "!=":
|
|
508
|
+
case "<>":
|
|
509
|
+
return { [field]: { $ne: value } };
|
|
510
|
+
case ">":
|
|
511
|
+
return { [field]: { $gt: value } };
|
|
512
|
+
case ">=":
|
|
513
|
+
return { [field]: { $gte: value } };
|
|
514
|
+
case "<":
|
|
515
|
+
return { [field]: { $lt: value } };
|
|
516
|
+
case "<=":
|
|
517
|
+
return { [field]: { $lte: value } };
|
|
518
|
+
case "in":
|
|
519
|
+
return { [field]: { $in: value } };
|
|
520
|
+
case "nin":
|
|
521
|
+
case "not in":
|
|
522
|
+
return { [field]: { $nin: value } };
|
|
523
|
+
case "contains":
|
|
524
|
+
case "like":
|
|
525
|
+
return { [field]: { $regex: new RegExp(this.escapeRegex(value), "i") } };
|
|
526
|
+
case "startswith":
|
|
527
|
+
case "starts_with":
|
|
528
|
+
return { [field]: { $regex: new RegExp(`^${this.escapeRegex(value)}`, "i") } };
|
|
529
|
+
case "endswith":
|
|
530
|
+
case "ends_with":
|
|
531
|
+
return { [field]: { $regex: new RegExp(`${this.escapeRegex(value)}$`, "i") } };
|
|
532
|
+
case "between":
|
|
533
|
+
if (Array.isArray(value) && value.length === 2) {
|
|
534
|
+
return { [field]: { $gte: value[0], $lte: value[1] } };
|
|
535
|
+
}
|
|
536
|
+
return null;
|
|
537
|
+
default:
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
417
540
|
}
|
|
418
|
-
|
|
541
|
+
/**
|
|
542
|
+
* Escape special regex characters for safe literal matching.
|
|
543
|
+
*/
|
|
544
|
+
escapeRegex(str) {
|
|
545
|
+
return String(str).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
419
546
|
}
|
|
420
547
|
// ===================================
|
|
421
548
|
// Aggregation Logic
|
|
@@ -436,14 +563,10 @@ var InMemoryDriver = class {
|
|
|
436
563
|
groups.get(key).push(record);
|
|
437
564
|
}
|
|
438
565
|
} else {
|
|
439
|
-
|
|
440
|
-
groups.set("all", records);
|
|
441
|
-
} else {
|
|
442
|
-
groups.set("all", records);
|
|
443
|
-
}
|
|
566
|
+
groups.set("all", records);
|
|
444
567
|
}
|
|
445
568
|
const resultRows = [];
|
|
446
|
-
for (const [
|
|
569
|
+
for (const [_key, groupRecords] of groups.entries()) {
|
|
447
570
|
const row = {};
|
|
448
571
|
if (groupBy && groupBy.length > 0) {
|
|
449
572
|
if (groupRecords.length > 0) {
|
|
@@ -502,16 +625,475 @@ var InMemoryDriver = class {
|
|
|
502
625
|
current[parts[parts.length - 1]] = value;
|
|
503
626
|
}
|
|
504
627
|
// ===================================
|
|
628
|
+
// Schema Management
|
|
629
|
+
// ===================================
|
|
630
|
+
async syncSchema(object, schema, options) {
|
|
631
|
+
if (!this.db[object]) {
|
|
632
|
+
this.db[object] = [];
|
|
633
|
+
this.logger.info("Created in-memory table", { object });
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
async dropTable(object, options) {
|
|
637
|
+
if (this.db[object]) {
|
|
638
|
+
const recordCount = this.db[object].length;
|
|
639
|
+
delete this.db[object];
|
|
640
|
+
this.logger.info("Dropped in-memory table", { object, recordCount });
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
// ===================================
|
|
505
644
|
// Helpers
|
|
506
645
|
// ===================================
|
|
646
|
+
/**
|
|
647
|
+
* Apply manual sorting (Mingo sort has CJS build issues).
|
|
648
|
+
*/
|
|
649
|
+
applySort(records, sortFields) {
|
|
650
|
+
const sorted = [...records];
|
|
651
|
+
for (let i = sortFields.length - 1; i >= 0; i--) {
|
|
652
|
+
const sortItem = sortFields[i];
|
|
653
|
+
let field;
|
|
654
|
+
let direction;
|
|
655
|
+
if (typeof sortItem === "object" && !Array.isArray(sortItem)) {
|
|
656
|
+
field = sortItem.field;
|
|
657
|
+
direction = sortItem.order || sortItem.direction || "asc";
|
|
658
|
+
} else if (Array.isArray(sortItem)) {
|
|
659
|
+
[field, direction] = sortItem;
|
|
660
|
+
} else {
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
sorted.sort((a, b) => {
|
|
664
|
+
const aVal = getValueByPath(a, field);
|
|
665
|
+
const bVal = getValueByPath(b, field);
|
|
666
|
+
if (aVal == null && bVal == null) return 0;
|
|
667
|
+
if (aVal == null) return 1;
|
|
668
|
+
if (bVal == null) return -1;
|
|
669
|
+
if (aVal < bVal) return direction === "desc" ? 1 : -1;
|
|
670
|
+
if (aVal > bVal) return direction === "desc" ? -1 : 1;
|
|
671
|
+
return 0;
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
return sorted;
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Project specific fields from a record.
|
|
678
|
+
*/
|
|
679
|
+
projectFields(record, fields) {
|
|
680
|
+
const result = {};
|
|
681
|
+
for (const field of fields) {
|
|
682
|
+
const value = getValueByPath(record, field);
|
|
683
|
+
if (value !== void 0) {
|
|
684
|
+
result[field] = value;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
if (!fields.includes("id") && record.id !== void 0) {
|
|
688
|
+
result.id = record.id;
|
|
689
|
+
}
|
|
690
|
+
return result;
|
|
691
|
+
}
|
|
507
692
|
getTable(name) {
|
|
508
693
|
if (!this.db[name]) {
|
|
509
694
|
this.db[name] = [];
|
|
510
695
|
}
|
|
511
696
|
return this.db[name];
|
|
512
697
|
}
|
|
513
|
-
generateId() {
|
|
514
|
-
|
|
698
|
+
generateId(objectName) {
|
|
699
|
+
const key = objectName || "_global";
|
|
700
|
+
const counter = (this.idCounters.get(key) || 0) + 1;
|
|
701
|
+
this.idCounters.set(key, counter);
|
|
702
|
+
const timestamp = Date.now();
|
|
703
|
+
return `${key}-${timestamp}-${counter}`;
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
// src/memory-analytics.ts
|
|
708
|
+
var import_core2 = require("@objectstack/core");
|
|
709
|
+
var MemoryAnalyticsService = class {
|
|
710
|
+
constructor(config) {
|
|
711
|
+
this.driver = config.driver;
|
|
712
|
+
this.cubes = new Map(config.cubes.map((c) => [c.name, c]));
|
|
713
|
+
this.logger = config.logger || (0, import_core2.createLogger)({ level: "info", format: "pretty" });
|
|
714
|
+
this.logger.debug("MemoryAnalyticsService initialized", { cubeCount: this.cubes.size });
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Execute an analytical query using the memory driver's aggregation pipeline
|
|
718
|
+
*/
|
|
719
|
+
async query(query) {
|
|
720
|
+
this.logger.debug("Executing analytics query", { cube: query.cube, measures: query.measures });
|
|
721
|
+
if (!query.cube) {
|
|
722
|
+
throw new Error("Cube name is required");
|
|
723
|
+
}
|
|
724
|
+
const cube = this.cubes.get(query.cube);
|
|
725
|
+
if (!cube) {
|
|
726
|
+
throw new Error(`Cube not found: ${query.cube}`);
|
|
727
|
+
}
|
|
728
|
+
const pipeline = [];
|
|
729
|
+
if (query.filters && query.filters.length > 0) {
|
|
730
|
+
const matchStage = {};
|
|
731
|
+
for (const filter of query.filters) {
|
|
732
|
+
const mongoOp = this.convertOperatorToMongo(filter.operator);
|
|
733
|
+
const fieldPath = this.resolveFieldPath(cube, filter.member);
|
|
734
|
+
if (filter.values && filter.values.length > 0) {
|
|
735
|
+
if (mongoOp === "$in") {
|
|
736
|
+
matchStage[fieldPath] = { $in: filter.values };
|
|
737
|
+
} else if (mongoOp === "$nin") {
|
|
738
|
+
matchStage[fieldPath] = { $nin: filter.values };
|
|
739
|
+
} else {
|
|
740
|
+
matchStage[fieldPath] = { [mongoOp]: filter.values[0] };
|
|
741
|
+
}
|
|
742
|
+
} else if (mongoOp === "$exists") {
|
|
743
|
+
matchStage[fieldPath] = { $exists: filter.operator === "set" };
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
if (Object.keys(matchStage).length > 0) {
|
|
747
|
+
pipeline.push({ $match: matchStage });
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
if (query.timeDimensions && query.timeDimensions.length > 0) {
|
|
751
|
+
for (const timeDim of query.timeDimensions) {
|
|
752
|
+
const fieldPath = this.resolveFieldPath(cube, timeDim.dimension);
|
|
753
|
+
if (timeDim.dateRange) {
|
|
754
|
+
const range = Array.isArray(timeDim.dateRange) ? timeDim.dateRange : this.parseDateRangeString(timeDim.dateRange);
|
|
755
|
+
if (range.length === 2) {
|
|
756
|
+
pipeline.push({
|
|
757
|
+
$match: {
|
|
758
|
+
[fieldPath]: {
|
|
759
|
+
$gte: new Date(range[0]),
|
|
760
|
+
$lte: new Date(range[1])
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
const groupStage = { _id: {} };
|
|
769
|
+
if (query.dimensions && query.dimensions.length > 0) {
|
|
770
|
+
for (const dim of query.dimensions) {
|
|
771
|
+
const fieldPath = this.resolveFieldPath(cube, dim);
|
|
772
|
+
const dimName = this.getShortName(dim);
|
|
773
|
+
groupStage._id[dimName] = `$${fieldPath}`;
|
|
774
|
+
}
|
|
775
|
+
} else {
|
|
776
|
+
groupStage._id = null;
|
|
777
|
+
}
|
|
778
|
+
if (query.measures && query.measures.length > 0) {
|
|
779
|
+
for (const measure of query.measures) {
|
|
780
|
+
const measureDef = this.resolveMeasure(cube, measure);
|
|
781
|
+
const measureName = this.getShortName(measure);
|
|
782
|
+
if (measureDef) {
|
|
783
|
+
const aggregator = this.buildAggregator(measureDef);
|
|
784
|
+
groupStage[measureName] = aggregator;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
pipeline.push({ $group: groupStage });
|
|
789
|
+
const projectStage = { _id: 0 };
|
|
790
|
+
if (query.dimensions && query.dimensions.length > 0) {
|
|
791
|
+
for (const dim of query.dimensions) {
|
|
792
|
+
const dimName = this.getShortName(dim);
|
|
793
|
+
projectStage[dimName] = `$_id.${dimName}`;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
if (query.measures && query.measures.length > 0) {
|
|
797
|
+
for (const measure of query.measures) {
|
|
798
|
+
const measureName = this.getShortName(measure);
|
|
799
|
+
projectStage[measureName] = `$${measureName}`;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
pipeline.push({ $project: projectStage });
|
|
803
|
+
if (query.order && Object.keys(query.order).length > 0) {
|
|
804
|
+
const sortStage = {};
|
|
805
|
+
for (const [field, direction] of Object.entries(query.order)) {
|
|
806
|
+
const shortName = this.getShortName(field);
|
|
807
|
+
sortStage[shortName] = direction === "asc" ? 1 : -1;
|
|
808
|
+
}
|
|
809
|
+
pipeline.push({ $sort: sortStage });
|
|
810
|
+
}
|
|
811
|
+
if (query.offset) {
|
|
812
|
+
pipeline.push({ $skip: query.offset });
|
|
813
|
+
}
|
|
814
|
+
if (query.limit) {
|
|
815
|
+
pipeline.push({ $limit: query.limit });
|
|
816
|
+
}
|
|
817
|
+
const tableName = this.extractTableName(cube.sql);
|
|
818
|
+
const rawRows = await this.driver.aggregate(tableName, pipeline);
|
|
819
|
+
const rows = rawRows.map((row) => {
|
|
820
|
+
const renamedRow = {};
|
|
821
|
+
if (query.dimensions) {
|
|
822
|
+
for (const dim of query.dimensions) {
|
|
823
|
+
const shortName = this.getShortName(dim);
|
|
824
|
+
if (shortName in row) {
|
|
825
|
+
renamedRow[dim] = row[shortName];
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
if (query.measures) {
|
|
830
|
+
for (const measure of query.measures) {
|
|
831
|
+
const shortName = this.getShortName(measure);
|
|
832
|
+
if (shortName in row) {
|
|
833
|
+
renamedRow[measure] = row[shortName];
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
return renamedRow;
|
|
838
|
+
});
|
|
839
|
+
const fields = [];
|
|
840
|
+
if (query.dimensions) {
|
|
841
|
+
for (const dim of query.dimensions) {
|
|
842
|
+
const dimension = this.resolveDimension(cube, dim);
|
|
843
|
+
fields.push({
|
|
844
|
+
name: dim,
|
|
845
|
+
type: dimension?.type || "string"
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
if (query.measures) {
|
|
850
|
+
for (const measure of query.measures) {
|
|
851
|
+
const measureDef = this.resolveMeasure(cube, measure);
|
|
852
|
+
fields.push({
|
|
853
|
+
name: measure,
|
|
854
|
+
type: this.measureTypeToFieldType(measureDef?.type || "count")
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
this.logger.debug("Analytics query completed", { rowCount: rows.length });
|
|
859
|
+
return {
|
|
860
|
+
rows,
|
|
861
|
+
fields,
|
|
862
|
+
sql: this.generateSqlFromPipeline(tableName, pipeline)
|
|
863
|
+
// For debugging
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Get available cube metadata for discovery
|
|
868
|
+
*/
|
|
869
|
+
async getMeta(cubeName) {
|
|
870
|
+
const cubes = cubeName ? [this.cubes.get(cubeName)].filter(Boolean) : Array.from(this.cubes.values());
|
|
871
|
+
return cubes.map((cube) => ({
|
|
872
|
+
name: cube.name,
|
|
873
|
+
title: cube.title,
|
|
874
|
+
measures: Object.entries(cube.measures).map(([key, measure]) => ({
|
|
875
|
+
name: `${cube.name}.${key}`,
|
|
876
|
+
type: measure.type,
|
|
877
|
+
title: measure.label
|
|
878
|
+
})),
|
|
879
|
+
dimensions: Object.entries(cube.dimensions).map(([key, dimension]) => ({
|
|
880
|
+
name: `${cube.name}.${key}`,
|
|
881
|
+
type: dimension.type,
|
|
882
|
+
title: dimension.label
|
|
883
|
+
}))
|
|
884
|
+
}));
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Generate SQL representation for debugging/transparency
|
|
888
|
+
*/
|
|
889
|
+
async generateSql(query) {
|
|
890
|
+
if (!query.cube) {
|
|
891
|
+
throw new Error("Cube name is required");
|
|
892
|
+
}
|
|
893
|
+
const cube = this.cubes.get(query.cube);
|
|
894
|
+
if (!cube) {
|
|
895
|
+
throw new Error(`Cube not found: ${query.cube}`);
|
|
896
|
+
}
|
|
897
|
+
const tableName = this.extractTableName(cube.sql);
|
|
898
|
+
const selectClauses = [];
|
|
899
|
+
const groupByClauses = [];
|
|
900
|
+
if (query.dimensions && query.dimensions.length > 0) {
|
|
901
|
+
for (const dim of query.dimensions) {
|
|
902
|
+
const fieldPath = this.resolveFieldPath(cube, dim);
|
|
903
|
+
selectClauses.push(`${fieldPath} AS "${dim}"`);
|
|
904
|
+
groupByClauses.push(fieldPath);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
if (query.measures && query.measures.length > 0) {
|
|
908
|
+
for (const measure of query.measures) {
|
|
909
|
+
const measureDef = this.resolveMeasure(cube, measure);
|
|
910
|
+
if (measureDef) {
|
|
911
|
+
const aggSql = this.measureToSql(measureDef);
|
|
912
|
+
selectClauses.push(`${aggSql} AS "${measure}"`);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
const whereClauses = [];
|
|
917
|
+
if (query.filters && query.filters.length > 0) {
|
|
918
|
+
for (const filter of query.filters) {
|
|
919
|
+
const fieldPath = this.resolveFieldPath(cube, filter.member);
|
|
920
|
+
const sqlOp = this.operatorToSql(filter.operator);
|
|
921
|
+
if (filter.values && filter.values.length > 0) {
|
|
922
|
+
whereClauses.push(`${fieldPath} ${sqlOp} '${filter.values[0]}'`);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
let sql = `SELECT ${selectClauses.join(", ")} FROM ${tableName}`;
|
|
927
|
+
if (whereClauses.length > 0) {
|
|
928
|
+
sql += ` WHERE ${whereClauses.join(" AND ")}`;
|
|
929
|
+
}
|
|
930
|
+
if (groupByClauses.length > 0) {
|
|
931
|
+
sql += ` GROUP BY ${groupByClauses.join(", ")}`;
|
|
932
|
+
}
|
|
933
|
+
if (query.order) {
|
|
934
|
+
const orderClauses = Object.entries(query.order).map(
|
|
935
|
+
([field, dir]) => `"${field}" ${dir.toUpperCase()}`
|
|
936
|
+
);
|
|
937
|
+
sql += ` ORDER BY ${orderClauses.join(", ")}`;
|
|
938
|
+
}
|
|
939
|
+
if (query.limit) {
|
|
940
|
+
sql += ` LIMIT ${query.limit}`;
|
|
941
|
+
}
|
|
942
|
+
if (query.offset) {
|
|
943
|
+
sql += ` OFFSET ${query.offset}`;
|
|
944
|
+
}
|
|
945
|
+
return { sql, params: [] };
|
|
946
|
+
}
|
|
947
|
+
// ===================================
|
|
948
|
+
// Helper Methods
|
|
949
|
+
// ===================================
|
|
950
|
+
resolveFieldPath(cube, member) {
|
|
951
|
+
const parts = member.split(".");
|
|
952
|
+
const fieldName = parts.length > 1 ? parts[1] : parts[0];
|
|
953
|
+
const dimension = cube.dimensions[fieldName];
|
|
954
|
+
if (dimension) {
|
|
955
|
+
return dimension.sql.replace(/^\$/, "");
|
|
956
|
+
}
|
|
957
|
+
const measure = cube.measures[fieldName];
|
|
958
|
+
if (measure) {
|
|
959
|
+
return measure.sql.replace(/^\$/, "");
|
|
960
|
+
}
|
|
961
|
+
return fieldName;
|
|
962
|
+
}
|
|
963
|
+
resolveMeasure(cube, measureName) {
|
|
964
|
+
const parts = measureName.split(".");
|
|
965
|
+
const fieldName = parts.length > 1 ? parts[1] : parts[0];
|
|
966
|
+
return cube.measures[fieldName];
|
|
967
|
+
}
|
|
968
|
+
resolveDimension(cube, dimensionName) {
|
|
969
|
+
const parts = dimensionName.split(".");
|
|
970
|
+
const fieldName = parts.length > 1 ? parts[1] : parts[0];
|
|
971
|
+
return cube.dimensions[fieldName];
|
|
972
|
+
}
|
|
973
|
+
getShortName(fullName) {
|
|
974
|
+
const parts = fullName.split(".");
|
|
975
|
+
return parts.length > 1 ? parts[1] : parts[0];
|
|
976
|
+
}
|
|
977
|
+
buildAggregator(measure) {
|
|
978
|
+
const fieldPath = measure.sql.replace(/^\$/, "");
|
|
979
|
+
switch (measure.type) {
|
|
980
|
+
case "count":
|
|
981
|
+
return { $sum: 1 };
|
|
982
|
+
case "sum":
|
|
983
|
+
return { $sum: `$${fieldPath}` };
|
|
984
|
+
case "avg":
|
|
985
|
+
return { $avg: `$${fieldPath}` };
|
|
986
|
+
case "min":
|
|
987
|
+
return { $min: `$${fieldPath}` };
|
|
988
|
+
case "max":
|
|
989
|
+
return { $max: `$${fieldPath}` };
|
|
990
|
+
case "count_distinct":
|
|
991
|
+
return { $addToSet: `$${fieldPath}` };
|
|
992
|
+
// Will need post-processing for count
|
|
993
|
+
default:
|
|
994
|
+
return { $sum: 1 };
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
measureTypeToFieldType(measureType) {
|
|
998
|
+
switch (measureType) {
|
|
999
|
+
case "count":
|
|
1000
|
+
case "sum":
|
|
1001
|
+
case "count_distinct":
|
|
1002
|
+
return "number";
|
|
1003
|
+
case "avg":
|
|
1004
|
+
case "min":
|
|
1005
|
+
case "max":
|
|
1006
|
+
return "number";
|
|
1007
|
+
case "string":
|
|
1008
|
+
return "string";
|
|
1009
|
+
case "boolean":
|
|
1010
|
+
return "boolean";
|
|
1011
|
+
default:
|
|
1012
|
+
return "number";
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
convertOperatorToMongo(operator) {
|
|
1016
|
+
const opMap = {
|
|
1017
|
+
"equals": "$eq",
|
|
1018
|
+
"notEquals": "$ne",
|
|
1019
|
+
"contains": "$regex",
|
|
1020
|
+
"notContains": "$not",
|
|
1021
|
+
"gt": "$gt",
|
|
1022
|
+
"gte": "$gte",
|
|
1023
|
+
"lt": "$lt",
|
|
1024
|
+
"lte": "$lte",
|
|
1025
|
+
"set": "$exists",
|
|
1026
|
+
"notSet": "$exists",
|
|
1027
|
+
"inDateRange": "$gte"
|
|
1028
|
+
// Will need special handling
|
|
1029
|
+
};
|
|
1030
|
+
return opMap[operator] || "$eq";
|
|
1031
|
+
}
|
|
1032
|
+
operatorToSql(operator) {
|
|
1033
|
+
const opMap = {
|
|
1034
|
+
"equals": "=",
|
|
1035
|
+
"notEquals": "!=",
|
|
1036
|
+
"contains": "LIKE",
|
|
1037
|
+
"notContains": "NOT LIKE",
|
|
1038
|
+
"gt": ">",
|
|
1039
|
+
"gte": ">=",
|
|
1040
|
+
"lt": "<",
|
|
1041
|
+
"lte": "<="
|
|
1042
|
+
};
|
|
1043
|
+
return opMap[operator] || "=";
|
|
1044
|
+
}
|
|
1045
|
+
measureToSql(measure) {
|
|
1046
|
+
const fieldPath = measure.sql.replace(/^\$/, "");
|
|
1047
|
+
switch (measure.type) {
|
|
1048
|
+
case "count":
|
|
1049
|
+
return "COUNT(*)";
|
|
1050
|
+
case "sum":
|
|
1051
|
+
return `SUM(${fieldPath})`;
|
|
1052
|
+
case "avg":
|
|
1053
|
+
return `AVG(${fieldPath})`;
|
|
1054
|
+
case "min":
|
|
1055
|
+
return `MIN(${fieldPath})`;
|
|
1056
|
+
case "max":
|
|
1057
|
+
return `MAX(${fieldPath})`;
|
|
1058
|
+
case "count_distinct":
|
|
1059
|
+
return `COUNT(DISTINCT ${fieldPath})`;
|
|
1060
|
+
default:
|
|
1061
|
+
return "COUNT(*)";
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
extractTableName(sql) {
|
|
1065
|
+
return sql.trim();
|
|
1066
|
+
}
|
|
1067
|
+
parseDateRangeString(range) {
|
|
1068
|
+
const now = /* @__PURE__ */ new Date();
|
|
1069
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
1070
|
+
if (range === "today") {
|
|
1071
|
+
return [today.toISOString(), new Date(today.getTime() + 864e5).toISOString()];
|
|
1072
|
+
} else if (range.startsWith("last ")) {
|
|
1073
|
+
const parts = range.split(" ");
|
|
1074
|
+
const num = parseInt(parts[1]);
|
|
1075
|
+
const unit = parts[2];
|
|
1076
|
+
const start = new Date(today);
|
|
1077
|
+
if (unit.startsWith("day")) {
|
|
1078
|
+
start.setDate(start.getDate() - num);
|
|
1079
|
+
} else if (unit.startsWith("week")) {
|
|
1080
|
+
start.setDate(start.getDate() - num * 7);
|
|
1081
|
+
} else if (unit.startsWith("month")) {
|
|
1082
|
+
start.setMonth(start.getMonth() - num);
|
|
1083
|
+
} else if (unit.startsWith("year")) {
|
|
1084
|
+
start.setFullYear(start.getFullYear() - num);
|
|
1085
|
+
}
|
|
1086
|
+
return [start.toISOString(), now.toISOString()];
|
|
1087
|
+
}
|
|
1088
|
+
return [range, range];
|
|
1089
|
+
}
|
|
1090
|
+
generateSqlFromPipeline(table, pipeline) {
|
|
1091
|
+
const stages = pipeline.map((stage, idx) => {
|
|
1092
|
+
const op = Object.keys(stage)[0];
|
|
1093
|
+
return `/* Stage ${idx + 1}: ${op} */ ${JSON.stringify(stage[op])}`;
|
|
1094
|
+
}).join("\n");
|
|
1095
|
+
return `-- MongoDB Aggregation Pipeline on table: ${table}
|
|
1096
|
+
${stages}`;
|
|
515
1097
|
}
|
|
516
1098
|
};
|
|
517
1099
|
|
|
@@ -533,6 +1115,7 @@ var index_default = {
|
|
|
533
1115
|
};
|
|
534
1116
|
// Annotate the CommonJS export names for ESM import in node:
|
|
535
1117
|
0 && (module.exports = {
|
|
536
|
-
InMemoryDriver
|
|
1118
|
+
InMemoryDriver,
|
|
1119
|
+
MemoryAnalyticsService
|
|
537
1120
|
});
|
|
538
1121
|
//# sourceMappingURL=index.js.map
|