@objectstack/driver-memory 4.0.4 → 4.1.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.mjs CHANGED
@@ -842,9 +842,14 @@ var _InMemoryDriver = class _InMemoryDriver {
842
842
  performAggregation(records, query) {
843
843
  const { groupBy, aggregations } = query;
844
844
  const groups = /* @__PURE__ */ new Map();
845
+ const normalizeGroupBy = (node) => {
846
+ if (typeof node === "string") return { field: node, alias: node };
847
+ return { field: node.field, alias: node.alias ?? node.field };
848
+ };
845
849
  if (groupBy && groupBy.length > 0) {
846
850
  for (const record of records) {
847
- const keyParts = groupBy.map((field) => {
851
+ const keyParts = groupBy.map((node) => {
852
+ const { field } = normalizeGroupBy(node);
848
853
  const val = getValueByPath(record, field);
849
854
  return val === void 0 || val === null ? "null" : String(val);
850
855
  });
@@ -863,8 +868,9 @@ var _InMemoryDriver = class _InMemoryDriver {
863
868
  if (groupBy && groupBy.length > 0) {
864
869
  if (groupRecords.length > 0) {
865
870
  const firstRecord = groupRecords[0];
866
- for (const field of groupBy) {
867
- this.setValueByPath(row, field, getValueByPath(firstRecord, field));
871
+ for (const node of groupBy) {
872
+ const { field, alias } = normalizeGroupBy(node);
873
+ this.setValueByPath(row, alias, getValueByPath(firstRecord, field));
868
874
  }
869
875
  }
870
876
  }
@@ -1145,18 +1151,20 @@ var MemoryAnalyticsService = class {
1145
1151
  throw new Error(`Cube not found: ${query.cube}`);
1146
1152
  }
1147
1153
  const pipeline = [];
1148
- if (query.filters && query.filters.length > 0) {
1154
+ const normalizedFilters = this.normalizeFilters(query);
1155
+ if (normalizedFilters.length > 0) {
1149
1156
  const matchStage = {};
1150
- for (const filter of query.filters) {
1157
+ for (const filter of normalizedFilters) {
1151
1158
  const mongoOp = this.convertOperatorToMongo(filter.operator);
1152
1159
  const fieldPath = this.resolveFieldPath(cube, filter.member);
1153
1160
  if (filter.values && filter.values.length > 0) {
1161
+ const coerced = filter.values.map((v) => this.coerceFilterValue(v));
1154
1162
  if (mongoOp === "$in") {
1155
- matchStage[fieldPath] = { $in: filter.values };
1163
+ matchStage[fieldPath] = { $in: coerced };
1156
1164
  } else if (mongoOp === "$nin") {
1157
- matchStage[fieldPath] = { $nin: filter.values };
1165
+ matchStage[fieldPath] = { $nin: coerced };
1158
1166
  } else {
1159
- matchStage[fieldPath] = { [mongoOp]: filter.values[0] };
1167
+ matchStage[fieldPath] = { [mongoOp]: coerced[0] };
1160
1168
  }
1161
1169
  } else if (mongoOp === "$exists") {
1162
1170
  matchStage[fieldPath] = { $exists: filter.operator === "set" };
@@ -1333,12 +1341,14 @@ var MemoryAnalyticsService = class {
1333
1341
  }
1334
1342
  }
1335
1343
  const whereClauses = [];
1336
- if (query.filters && query.filters.length > 0) {
1337
- for (const filter of query.filters) {
1344
+ const normalizedFilters = this.normalizeFilters(query);
1345
+ if (normalizedFilters.length > 0) {
1346
+ for (const filter of normalizedFilters) {
1338
1347
  const fieldPath = this.resolveFieldPath(cube, filter.member);
1339
1348
  const sqlOp = this.operatorToSql(filter.operator);
1340
1349
  if (filter.values && filter.values.length > 0) {
1341
- whereClauses.push(`${fieldPath} ${sqlOp} '${filter.values[0]}'`);
1350
+ const literal = this.toSqlLiteral(filter.values[0]);
1351
+ whereClauses.push(`${fieldPath} ${sqlOp} ${literal}`);
1342
1352
  }
1343
1353
  }
1344
1354
  }
@@ -1366,6 +1376,147 @@ var MemoryAnalyticsService = class {
1366
1376
  // ===================================
1367
1377
  // Helper Methods
1368
1378
  // ===================================
1379
+ /**
1380
+ * Normalize filters into a cube-style array regardless of input shape.
1381
+ *
1382
+ * Accepts:
1383
+ * - undefined / null → []
1384
+ * - cube-style array `[{member, operator, values}]` → returned as-is
1385
+ * - MongoDB FilterCondition object (per spec/data/filter.zod.ts):
1386
+ * * implicit equality: `{is_active: true}`
1387
+ * * operator wrapper: `{stage: {$nin: [...]}}`
1388
+ * * mixed: `{stage: 'won', amount: {$gte: 100}}`
1389
+ * → flattened into one cube-style entry per (field, operator) pair
1390
+ *
1391
+ * Logical combinators (`$and`, `$or`, `$not`) are not yet expanded into
1392
+ * the cube pipeline; for current dashboard widget metadata the implicit
1393
+ * top-level AND of fields is sufficient. `$and` clauses are flattened
1394
+ * into the same AND list.
1395
+ */
1396
+ normalizeFilters(query) {
1397
+ if (!query || typeof query !== "object") return [];
1398
+ const out = [];
1399
+ const where = query.where;
1400
+ if (where && typeof where === "object" && !Array.isArray(where)) {
1401
+ this.flattenFilterCondition(where, out);
1402
+ }
1403
+ return out;
1404
+ }
1405
+ flattenFilterCondition(cond, out) {
1406
+ for (const [key, raw] of Object.entries(cond)) {
1407
+ if (raw == null) continue;
1408
+ if (key === "$and" && Array.isArray(raw)) {
1409
+ for (const sub of raw) {
1410
+ if (sub && typeof sub === "object") {
1411
+ this.flattenFilterCondition(sub, out);
1412
+ }
1413
+ }
1414
+ continue;
1415
+ }
1416
+ if (key === "$or" || key === "$not") continue;
1417
+ if (typeof raw === "object" && !Array.isArray(raw) && !(raw instanceof Date)) {
1418
+ const wrapper = raw;
1419
+ const opEntries = Object.keys(wrapper).filter((k) => k.startsWith("$"));
1420
+ if (opEntries.length > 0) {
1421
+ for (const opKey of opEntries) {
1422
+ const cubeOp = this.mongoOperatorToCubeOperator(opKey);
1423
+ if (!cubeOp) continue;
1424
+ const v = wrapper[opKey];
1425
+ const values2 = Array.isArray(v) ? v.map((x) => this.stringifyForCube(x)) : [this.stringifyForCube(v)];
1426
+ out.push({ member: key, operator: cubeOp, values: values2 });
1427
+ }
1428
+ continue;
1429
+ }
1430
+ for (const [nestedKey, nestedVal] of Object.entries(wrapper)) {
1431
+ this.flattenFilterCondition({ [`${key}.${nestedKey}`]: nestedVal }, out);
1432
+ }
1433
+ continue;
1434
+ }
1435
+ const values = Array.isArray(raw) ? raw.map((x) => this.stringifyForCube(x)) : [this.stringifyForCube(raw)];
1436
+ out.push({
1437
+ member: key,
1438
+ operator: Array.isArray(raw) ? "in" : "equals",
1439
+ values
1440
+ });
1441
+ }
1442
+ }
1443
+ /**
1444
+ * Map MongoDB-style `$op` keys (from FilterCondition) to the cube-style
1445
+ * operator names accepted by `convertOperatorToMongo` / `operatorToSql`.
1446
+ */
1447
+ mongoOperatorToCubeOperator(op) {
1448
+ switch (op) {
1449
+ case "$eq":
1450
+ return "equals";
1451
+ case "$ne":
1452
+ return "notEquals";
1453
+ case "$gt":
1454
+ return "gt";
1455
+ case "$gte":
1456
+ return "gte";
1457
+ case "$lt":
1458
+ return "lt";
1459
+ case "$lte":
1460
+ return "lte";
1461
+ case "$in":
1462
+ return "in";
1463
+ case "$nin":
1464
+ return "notIn";
1465
+ case "$contains":
1466
+ return "contains";
1467
+ case "$notContains":
1468
+ return "notContains";
1469
+ case "$exists":
1470
+ return "set";
1471
+ default:
1472
+ return null;
1473
+ }
1474
+ }
1475
+ /**
1476
+ * Stringify a filter value for cube-style storage. Booleans become
1477
+ * `'1'/'0'` so that downstream consumers expecting SQLite-style
1478
+ * numeric booleans match correctly. The in-memory pipeline uses
1479
+ * {@link coerceFilterValue} to recover real JS types from these
1480
+ * strings.
1481
+ */
1482
+ stringifyForCube(v) {
1483
+ if (v == null) return "";
1484
+ if (typeof v === "boolean") return v ? "1" : "0";
1485
+ if (v instanceof Date) return v.toISOString();
1486
+ if (typeof v === "object") return JSON.stringify(v);
1487
+ return String(v);
1488
+ }
1489
+ /**
1490
+ * Recover a runtime value from its cube-stringified form for in-memory
1491
+ * comparison. Booleans, integers, floats and ISO-date-like strings are
1492
+ * coerced; everything else stays as a string.
1493
+ */
1494
+ coerceFilterValue(s) {
1495
+ if (s === "true") return true;
1496
+ if (s === "false") return false;
1497
+ if (s === "null") return null;
1498
+ if (/^-?\d+$/.test(s)) {
1499
+ const n = Number(s);
1500
+ if (Number.isFinite(n)) return n;
1501
+ }
1502
+ if (/^-?\d+\.\d+$/.test(s)) {
1503
+ const n = Number(s);
1504
+ if (Number.isFinite(n)) return n;
1505
+ }
1506
+ return s;
1507
+ }
1508
+ /**
1509
+ * Type-aware SQL literal formatter. Booleans and numbers are emitted
1510
+ * unquoted; everything else is single-quoted with embedded quotes
1511
+ * escaped.
1512
+ */
1513
+ toSqlLiteral(s) {
1514
+ if (s === "true") return "1";
1515
+ if (s === "false") return "0";
1516
+ if (s === "null") return "NULL";
1517
+ if (/^-?\d+(\.\d+)?$/.test(s)) return s;
1518
+ return `'${s.replace(/'/g, "''")}'`;
1519
+ }
1369
1520
  resolveFieldPath(cube, member) {
1370
1521
  const parts = member.split(".");
1371
1522
  const fieldName = parts.length > 1 ? parts[1] : parts[0];
@@ -1382,7 +1533,20 @@ var MemoryAnalyticsService = class {
1382
1533
  resolveMeasure(cube, measureName) {
1383
1534
  const parts = measureName.split(".");
1384
1535
  const fieldName = parts.length > 1 ? parts[1] : parts[0];
1385
- return cube.measures[fieldName];
1536
+ const direct = cube.measures[fieldName];
1537
+ if (direct) return direct;
1538
+ const aggTypes = ["count", "sum", "avg", "min", "max", "count_distinct"];
1539
+ for (const type of aggTypes) {
1540
+ const suffix = `_${type}`;
1541
+ if (fieldName.endsWith(suffix)) {
1542
+ const baseField = fieldName.slice(0, -suffix.length);
1543
+ const candidate = cube.measures[baseField];
1544
+ if (candidate && candidate.type === type) {
1545
+ return candidate;
1546
+ }
1547
+ }
1548
+ }
1549
+ return void 0;
1386
1550
  }
1387
1551
  resolveDimension(cube, dimensionName) {
1388
1552
  const parts = dimensionName.split(".");
@@ -1441,6 +1605,8 @@ var MemoryAnalyticsService = class {
1441
1605
  "gte": "$gte",
1442
1606
  "lt": "$lt",
1443
1607
  "lte": "$lte",
1608
+ "in": "$in",
1609
+ "notIn": "$nin",
1444
1610
  "set": "$exists",
1445
1611
  "notSet": "$exists",
1446
1612
  "inDateRange": "$gte"