@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/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.1";
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: false,
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", { config: this.config });
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.logger.info("InMemory Database Connected (Virtual)");
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
- results = results.filter((record) => match(record, query.where));
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.sort((a, b) => {
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.logger.warn("Record not found for update", { object, id });
291
- throw new Error(`Record with ID ${id} not found in ${object}`);
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
- updated_at: /* @__PURE__ */ new Date()
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 results = this.getTable(object);
258
+ let records = this.getTable(object);
333
259
  if (query?.where) {
334
- results = results.filter((record) => match(record, query.where));
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 = results.length;
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
- targetRecords = targetRecords.filter((r) => match(r, query.where));
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
- const remaining = table.filter((r) => {
376
- if (!query || !query.where) return false;
377
- const matches = match(r, query.where);
378
- return !matches;
379
- });
380
- this.db[object] = remaining;
381
- const count = initialLength - remaining.length;
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
- // Schema & Transactions
339
+ // Transaction Management
399
340
  // ===================================
400
- async syncSchema(object, schema, options) {
401
- if (!this.db[object]) {
402
- this.db[object] = [];
403
- this.logger.info("Created in-memory table", { object });
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 dropTable(object, options) {
407
- if (this.db[object]) {
408
- const recordCount = this.db[object].length;
409
- delete this.db[object];
410
- this.logger.info("Dropped in-memory table", { object, recordCount });
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 beginTransaction() {
414
- throw new Error("Transactions not supported in InMemoryDriver");
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
- async commit() {
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
- async rollback() {
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
- if (aggregations && aggregations.length > 0) {
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 [key, groupRecords] of groups.entries()) {
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
- return Math.random().toString(36).substring(2, 15);
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