@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.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
|
}
|
|
319
|
+
const transaction = { id: txId, snapshot };
|
|
320
|
+
this.transactions.set(txId, transaction);
|
|
321
|
+
this.logger.debug("Transaction started", { txId });
|
|
322
|
+
return { id: txId };
|
|
378
323
|
}
|
|
379
|
-
async
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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;
|
|
384
329
|
}
|
|
330
|
+
this.transactions.delete(txId);
|
|
331
|
+
this.logger.debug("Transaction committed", { txId });
|
|
385
332
|
}
|
|
386
|
-
async
|
|
387
|
-
|
|
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;
|
|
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 });
|
|
343
|
+
}
|
|
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;
|
|
411
|
+
}
|
|
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 };
|
|
388
470
|
}
|
|
389
|
-
|
|
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,475 @@ 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}`;
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
// src/memory-analytics.ts
|
|
680
|
+
import { createLogger as createLogger2 } from "@objectstack/core";
|
|
681
|
+
var MemoryAnalyticsService = class {
|
|
682
|
+
constructor(config) {
|
|
683
|
+
this.driver = config.driver;
|
|
684
|
+
this.cubes = new Map(config.cubes.map((c) => [c.name, c]));
|
|
685
|
+
this.logger = config.logger || createLogger2({ level: "info", format: "pretty" });
|
|
686
|
+
this.logger.debug("MemoryAnalyticsService initialized", { cubeCount: this.cubes.size });
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Execute an analytical query using the memory driver's aggregation pipeline
|
|
690
|
+
*/
|
|
691
|
+
async query(query) {
|
|
692
|
+
this.logger.debug("Executing analytics query", { cube: query.cube, measures: query.measures });
|
|
693
|
+
if (!query.cube) {
|
|
694
|
+
throw new Error("Cube name is required");
|
|
695
|
+
}
|
|
696
|
+
const cube = this.cubes.get(query.cube);
|
|
697
|
+
if (!cube) {
|
|
698
|
+
throw new Error(`Cube not found: ${query.cube}`);
|
|
699
|
+
}
|
|
700
|
+
const pipeline = [];
|
|
701
|
+
if (query.filters && query.filters.length > 0) {
|
|
702
|
+
const matchStage = {};
|
|
703
|
+
for (const filter of query.filters) {
|
|
704
|
+
const mongoOp = this.convertOperatorToMongo(filter.operator);
|
|
705
|
+
const fieldPath = this.resolveFieldPath(cube, filter.member);
|
|
706
|
+
if (filter.values && filter.values.length > 0) {
|
|
707
|
+
if (mongoOp === "$in") {
|
|
708
|
+
matchStage[fieldPath] = { $in: filter.values };
|
|
709
|
+
} else if (mongoOp === "$nin") {
|
|
710
|
+
matchStage[fieldPath] = { $nin: filter.values };
|
|
711
|
+
} else {
|
|
712
|
+
matchStage[fieldPath] = { [mongoOp]: filter.values[0] };
|
|
713
|
+
}
|
|
714
|
+
} else if (mongoOp === "$exists") {
|
|
715
|
+
matchStage[fieldPath] = { $exists: filter.operator === "set" };
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
if (Object.keys(matchStage).length > 0) {
|
|
719
|
+
pipeline.push({ $match: matchStage });
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
if (query.timeDimensions && query.timeDimensions.length > 0) {
|
|
723
|
+
for (const timeDim of query.timeDimensions) {
|
|
724
|
+
const fieldPath = this.resolveFieldPath(cube, timeDim.dimension);
|
|
725
|
+
if (timeDim.dateRange) {
|
|
726
|
+
const range = Array.isArray(timeDim.dateRange) ? timeDim.dateRange : this.parseDateRangeString(timeDim.dateRange);
|
|
727
|
+
if (range.length === 2) {
|
|
728
|
+
pipeline.push({
|
|
729
|
+
$match: {
|
|
730
|
+
[fieldPath]: {
|
|
731
|
+
$gte: new Date(range[0]),
|
|
732
|
+
$lte: new Date(range[1])
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
const groupStage = { _id: {} };
|
|
741
|
+
if (query.dimensions && query.dimensions.length > 0) {
|
|
742
|
+
for (const dim of query.dimensions) {
|
|
743
|
+
const fieldPath = this.resolveFieldPath(cube, dim);
|
|
744
|
+
const dimName = this.getShortName(dim);
|
|
745
|
+
groupStage._id[dimName] = `$${fieldPath}`;
|
|
746
|
+
}
|
|
747
|
+
} else {
|
|
748
|
+
groupStage._id = null;
|
|
749
|
+
}
|
|
750
|
+
if (query.measures && query.measures.length > 0) {
|
|
751
|
+
for (const measure of query.measures) {
|
|
752
|
+
const measureDef = this.resolveMeasure(cube, measure);
|
|
753
|
+
const measureName = this.getShortName(measure);
|
|
754
|
+
if (measureDef) {
|
|
755
|
+
const aggregator = this.buildAggregator(measureDef);
|
|
756
|
+
groupStage[measureName] = aggregator;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
pipeline.push({ $group: groupStage });
|
|
761
|
+
const projectStage = { _id: 0 };
|
|
762
|
+
if (query.dimensions && query.dimensions.length > 0) {
|
|
763
|
+
for (const dim of query.dimensions) {
|
|
764
|
+
const dimName = this.getShortName(dim);
|
|
765
|
+
projectStage[dimName] = `$_id.${dimName}`;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
if (query.measures && query.measures.length > 0) {
|
|
769
|
+
for (const measure of query.measures) {
|
|
770
|
+
const measureName = this.getShortName(measure);
|
|
771
|
+
projectStage[measureName] = `$${measureName}`;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
pipeline.push({ $project: projectStage });
|
|
775
|
+
if (query.order && Object.keys(query.order).length > 0) {
|
|
776
|
+
const sortStage = {};
|
|
777
|
+
for (const [field, direction] of Object.entries(query.order)) {
|
|
778
|
+
const shortName = this.getShortName(field);
|
|
779
|
+
sortStage[shortName] = direction === "asc" ? 1 : -1;
|
|
780
|
+
}
|
|
781
|
+
pipeline.push({ $sort: sortStage });
|
|
782
|
+
}
|
|
783
|
+
if (query.offset) {
|
|
784
|
+
pipeline.push({ $skip: query.offset });
|
|
785
|
+
}
|
|
786
|
+
if (query.limit) {
|
|
787
|
+
pipeline.push({ $limit: query.limit });
|
|
788
|
+
}
|
|
789
|
+
const tableName = this.extractTableName(cube.sql);
|
|
790
|
+
const rawRows = await this.driver.aggregate(tableName, pipeline);
|
|
791
|
+
const rows = rawRows.map((row) => {
|
|
792
|
+
const renamedRow = {};
|
|
793
|
+
if (query.dimensions) {
|
|
794
|
+
for (const dim of query.dimensions) {
|
|
795
|
+
const shortName = this.getShortName(dim);
|
|
796
|
+
if (shortName in row) {
|
|
797
|
+
renamedRow[dim] = row[shortName];
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
if (query.measures) {
|
|
802
|
+
for (const measure of query.measures) {
|
|
803
|
+
const shortName = this.getShortName(measure);
|
|
804
|
+
if (shortName in row) {
|
|
805
|
+
renamedRow[measure] = row[shortName];
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return renamedRow;
|
|
810
|
+
});
|
|
811
|
+
const fields = [];
|
|
812
|
+
if (query.dimensions) {
|
|
813
|
+
for (const dim of query.dimensions) {
|
|
814
|
+
const dimension = this.resolveDimension(cube, dim);
|
|
815
|
+
fields.push({
|
|
816
|
+
name: dim,
|
|
817
|
+
type: dimension?.type || "string"
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
if (query.measures) {
|
|
822
|
+
for (const measure of query.measures) {
|
|
823
|
+
const measureDef = this.resolveMeasure(cube, measure);
|
|
824
|
+
fields.push({
|
|
825
|
+
name: measure,
|
|
826
|
+
type: this.measureTypeToFieldType(measureDef?.type || "count")
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
this.logger.debug("Analytics query completed", { rowCount: rows.length });
|
|
831
|
+
return {
|
|
832
|
+
rows,
|
|
833
|
+
fields,
|
|
834
|
+
sql: this.generateSqlFromPipeline(tableName, pipeline)
|
|
835
|
+
// For debugging
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Get available cube metadata for discovery
|
|
840
|
+
*/
|
|
841
|
+
async getMeta(cubeName) {
|
|
842
|
+
const cubes = cubeName ? [this.cubes.get(cubeName)].filter(Boolean) : Array.from(this.cubes.values());
|
|
843
|
+
return cubes.map((cube) => ({
|
|
844
|
+
name: cube.name,
|
|
845
|
+
title: cube.title,
|
|
846
|
+
measures: Object.entries(cube.measures).map(([key, measure]) => ({
|
|
847
|
+
name: `${cube.name}.${key}`,
|
|
848
|
+
type: measure.type,
|
|
849
|
+
title: measure.label
|
|
850
|
+
})),
|
|
851
|
+
dimensions: Object.entries(cube.dimensions).map(([key, dimension]) => ({
|
|
852
|
+
name: `${cube.name}.${key}`,
|
|
853
|
+
type: dimension.type,
|
|
854
|
+
title: dimension.label
|
|
855
|
+
}))
|
|
856
|
+
}));
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Generate SQL representation for debugging/transparency
|
|
860
|
+
*/
|
|
861
|
+
async generateSql(query) {
|
|
862
|
+
if (!query.cube) {
|
|
863
|
+
throw new Error("Cube name is required");
|
|
864
|
+
}
|
|
865
|
+
const cube = this.cubes.get(query.cube);
|
|
866
|
+
if (!cube) {
|
|
867
|
+
throw new Error(`Cube not found: ${query.cube}`);
|
|
868
|
+
}
|
|
869
|
+
const tableName = this.extractTableName(cube.sql);
|
|
870
|
+
const selectClauses = [];
|
|
871
|
+
const groupByClauses = [];
|
|
872
|
+
if (query.dimensions && query.dimensions.length > 0) {
|
|
873
|
+
for (const dim of query.dimensions) {
|
|
874
|
+
const fieldPath = this.resolveFieldPath(cube, dim);
|
|
875
|
+
selectClauses.push(`${fieldPath} AS "${dim}"`);
|
|
876
|
+
groupByClauses.push(fieldPath);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
if (query.measures && query.measures.length > 0) {
|
|
880
|
+
for (const measure of query.measures) {
|
|
881
|
+
const measureDef = this.resolveMeasure(cube, measure);
|
|
882
|
+
if (measureDef) {
|
|
883
|
+
const aggSql = this.measureToSql(measureDef);
|
|
884
|
+
selectClauses.push(`${aggSql} AS "${measure}"`);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
const whereClauses = [];
|
|
889
|
+
if (query.filters && query.filters.length > 0) {
|
|
890
|
+
for (const filter of query.filters) {
|
|
891
|
+
const fieldPath = this.resolveFieldPath(cube, filter.member);
|
|
892
|
+
const sqlOp = this.operatorToSql(filter.operator);
|
|
893
|
+
if (filter.values && filter.values.length > 0) {
|
|
894
|
+
whereClauses.push(`${fieldPath} ${sqlOp} '${filter.values[0]}'`);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
let sql = `SELECT ${selectClauses.join(", ")} FROM ${tableName}`;
|
|
899
|
+
if (whereClauses.length > 0) {
|
|
900
|
+
sql += ` WHERE ${whereClauses.join(" AND ")}`;
|
|
901
|
+
}
|
|
902
|
+
if (groupByClauses.length > 0) {
|
|
903
|
+
sql += ` GROUP BY ${groupByClauses.join(", ")}`;
|
|
904
|
+
}
|
|
905
|
+
if (query.order) {
|
|
906
|
+
const orderClauses = Object.entries(query.order).map(
|
|
907
|
+
([field, dir]) => `"${field}" ${dir.toUpperCase()}`
|
|
908
|
+
);
|
|
909
|
+
sql += ` ORDER BY ${orderClauses.join(", ")}`;
|
|
910
|
+
}
|
|
911
|
+
if (query.limit) {
|
|
912
|
+
sql += ` LIMIT ${query.limit}`;
|
|
913
|
+
}
|
|
914
|
+
if (query.offset) {
|
|
915
|
+
sql += ` OFFSET ${query.offset}`;
|
|
916
|
+
}
|
|
917
|
+
return { sql, params: [] };
|
|
918
|
+
}
|
|
919
|
+
// ===================================
|
|
920
|
+
// Helper Methods
|
|
921
|
+
// ===================================
|
|
922
|
+
resolveFieldPath(cube, member) {
|
|
923
|
+
const parts = member.split(".");
|
|
924
|
+
const fieldName = parts.length > 1 ? parts[1] : parts[0];
|
|
925
|
+
const dimension = cube.dimensions[fieldName];
|
|
926
|
+
if (dimension) {
|
|
927
|
+
return dimension.sql.replace(/^\$/, "");
|
|
928
|
+
}
|
|
929
|
+
const measure = cube.measures[fieldName];
|
|
930
|
+
if (measure) {
|
|
931
|
+
return measure.sql.replace(/^\$/, "");
|
|
932
|
+
}
|
|
933
|
+
return fieldName;
|
|
934
|
+
}
|
|
935
|
+
resolveMeasure(cube, measureName) {
|
|
936
|
+
const parts = measureName.split(".");
|
|
937
|
+
const fieldName = parts.length > 1 ? parts[1] : parts[0];
|
|
938
|
+
return cube.measures[fieldName];
|
|
939
|
+
}
|
|
940
|
+
resolveDimension(cube, dimensionName) {
|
|
941
|
+
const parts = dimensionName.split(".");
|
|
942
|
+
const fieldName = parts.length > 1 ? parts[1] : parts[0];
|
|
943
|
+
return cube.dimensions[fieldName];
|
|
944
|
+
}
|
|
945
|
+
getShortName(fullName) {
|
|
946
|
+
const parts = fullName.split(".");
|
|
947
|
+
return parts.length > 1 ? parts[1] : parts[0];
|
|
948
|
+
}
|
|
949
|
+
buildAggregator(measure) {
|
|
950
|
+
const fieldPath = measure.sql.replace(/^\$/, "");
|
|
951
|
+
switch (measure.type) {
|
|
952
|
+
case "count":
|
|
953
|
+
return { $sum: 1 };
|
|
954
|
+
case "sum":
|
|
955
|
+
return { $sum: `$${fieldPath}` };
|
|
956
|
+
case "avg":
|
|
957
|
+
return { $avg: `$${fieldPath}` };
|
|
958
|
+
case "min":
|
|
959
|
+
return { $min: `$${fieldPath}` };
|
|
960
|
+
case "max":
|
|
961
|
+
return { $max: `$${fieldPath}` };
|
|
962
|
+
case "count_distinct":
|
|
963
|
+
return { $addToSet: `$${fieldPath}` };
|
|
964
|
+
// Will need post-processing for count
|
|
965
|
+
default:
|
|
966
|
+
return { $sum: 1 };
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
measureTypeToFieldType(measureType) {
|
|
970
|
+
switch (measureType) {
|
|
971
|
+
case "count":
|
|
972
|
+
case "sum":
|
|
973
|
+
case "count_distinct":
|
|
974
|
+
return "number";
|
|
975
|
+
case "avg":
|
|
976
|
+
case "min":
|
|
977
|
+
case "max":
|
|
978
|
+
return "number";
|
|
979
|
+
case "string":
|
|
980
|
+
return "string";
|
|
981
|
+
case "boolean":
|
|
982
|
+
return "boolean";
|
|
983
|
+
default:
|
|
984
|
+
return "number";
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
convertOperatorToMongo(operator) {
|
|
988
|
+
const opMap = {
|
|
989
|
+
"equals": "$eq",
|
|
990
|
+
"notEquals": "$ne",
|
|
991
|
+
"contains": "$regex",
|
|
992
|
+
"notContains": "$not",
|
|
993
|
+
"gt": "$gt",
|
|
994
|
+
"gte": "$gte",
|
|
995
|
+
"lt": "$lt",
|
|
996
|
+
"lte": "$lte",
|
|
997
|
+
"set": "$exists",
|
|
998
|
+
"notSet": "$exists",
|
|
999
|
+
"inDateRange": "$gte"
|
|
1000
|
+
// Will need special handling
|
|
1001
|
+
};
|
|
1002
|
+
return opMap[operator] || "$eq";
|
|
1003
|
+
}
|
|
1004
|
+
operatorToSql(operator) {
|
|
1005
|
+
const opMap = {
|
|
1006
|
+
"equals": "=",
|
|
1007
|
+
"notEquals": "!=",
|
|
1008
|
+
"contains": "LIKE",
|
|
1009
|
+
"notContains": "NOT LIKE",
|
|
1010
|
+
"gt": ">",
|
|
1011
|
+
"gte": ">=",
|
|
1012
|
+
"lt": "<",
|
|
1013
|
+
"lte": "<="
|
|
1014
|
+
};
|
|
1015
|
+
return opMap[operator] || "=";
|
|
1016
|
+
}
|
|
1017
|
+
measureToSql(measure) {
|
|
1018
|
+
const fieldPath = measure.sql.replace(/^\$/, "");
|
|
1019
|
+
switch (measure.type) {
|
|
1020
|
+
case "count":
|
|
1021
|
+
return "COUNT(*)";
|
|
1022
|
+
case "sum":
|
|
1023
|
+
return `SUM(${fieldPath})`;
|
|
1024
|
+
case "avg":
|
|
1025
|
+
return `AVG(${fieldPath})`;
|
|
1026
|
+
case "min":
|
|
1027
|
+
return `MIN(${fieldPath})`;
|
|
1028
|
+
case "max":
|
|
1029
|
+
return `MAX(${fieldPath})`;
|
|
1030
|
+
case "count_distinct":
|
|
1031
|
+
return `COUNT(DISTINCT ${fieldPath})`;
|
|
1032
|
+
default:
|
|
1033
|
+
return "COUNT(*)";
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
extractTableName(sql) {
|
|
1037
|
+
return sql.trim();
|
|
1038
|
+
}
|
|
1039
|
+
parseDateRangeString(range) {
|
|
1040
|
+
const now = /* @__PURE__ */ new Date();
|
|
1041
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
1042
|
+
if (range === "today") {
|
|
1043
|
+
return [today.toISOString(), new Date(today.getTime() + 864e5).toISOString()];
|
|
1044
|
+
} else if (range.startsWith("last ")) {
|
|
1045
|
+
const parts = range.split(" ");
|
|
1046
|
+
const num = parseInt(parts[1]);
|
|
1047
|
+
const unit = parts[2];
|
|
1048
|
+
const start = new Date(today);
|
|
1049
|
+
if (unit.startsWith("day")) {
|
|
1050
|
+
start.setDate(start.getDate() - num);
|
|
1051
|
+
} else if (unit.startsWith("week")) {
|
|
1052
|
+
start.setDate(start.getDate() - num * 7);
|
|
1053
|
+
} else if (unit.startsWith("month")) {
|
|
1054
|
+
start.setMonth(start.getMonth() - num);
|
|
1055
|
+
} else if (unit.startsWith("year")) {
|
|
1056
|
+
start.setFullYear(start.getFullYear() - num);
|
|
1057
|
+
}
|
|
1058
|
+
return [start.toISOString(), now.toISOString()];
|
|
1059
|
+
}
|
|
1060
|
+
return [range, range];
|
|
1061
|
+
}
|
|
1062
|
+
generateSqlFromPipeline(table, pipeline) {
|
|
1063
|
+
const stages = pipeline.map((stage, idx) => {
|
|
1064
|
+
const op = Object.keys(stage)[0];
|
|
1065
|
+
return `/* Stage ${idx + 1}: ${op} */ ${JSON.stringify(stage[op])}`;
|
|
1066
|
+
}).join("\n");
|
|
1067
|
+
return `-- MongoDB Aggregation Pipeline on table: ${table}
|
|
1068
|
+
${stages}`;
|
|
488
1069
|
}
|
|
489
1070
|
};
|
|
490
1071
|
|
|
@@ -506,6 +1087,7 @@ var index_default = {
|
|
|
506
1087
|
};
|
|
507
1088
|
export {
|
|
508
1089
|
InMemoryDriver,
|
|
1090
|
+
MemoryAnalyticsService,
|
|
509
1091
|
index_default as default
|
|
510
1092
|
};
|
|
511
1093
|
//# sourceMappingURL=index.mjs.map
|