@objectstack/driver-memory 4.0.5 → 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.d.mts CHANGED
@@ -395,6 +395,50 @@ declare class MemoryAnalyticsService implements IAnalyticsService {
395
395
  sql: string;
396
396
  params: unknown[];
397
397
  }>;
398
+ /**
399
+ * Normalize filters into a cube-style array regardless of input shape.
400
+ *
401
+ * Accepts:
402
+ * - undefined / null → []
403
+ * - cube-style array `[{member, operator, values}]` → returned as-is
404
+ * - MongoDB FilterCondition object (per spec/data/filter.zod.ts):
405
+ * * implicit equality: `{is_active: true}`
406
+ * * operator wrapper: `{stage: {$nin: [...]}}`
407
+ * * mixed: `{stage: 'won', amount: {$gte: 100}}`
408
+ * → flattened into one cube-style entry per (field, operator) pair
409
+ *
410
+ * Logical combinators (`$and`, `$or`, `$not`) are not yet expanded into
411
+ * the cube pipeline; for current dashboard widget metadata the implicit
412
+ * top-level AND of fields is sufficient. `$and` clauses are flattened
413
+ * into the same AND list.
414
+ */
415
+ private normalizeFilters;
416
+ private flattenFilterCondition;
417
+ /**
418
+ * Map MongoDB-style `$op` keys (from FilterCondition) to the cube-style
419
+ * operator names accepted by `convertOperatorToMongo` / `operatorToSql`.
420
+ */
421
+ private mongoOperatorToCubeOperator;
422
+ /**
423
+ * Stringify a filter value for cube-style storage. Booleans become
424
+ * `'1'/'0'` so that downstream consumers expecting SQLite-style
425
+ * numeric booleans match correctly. The in-memory pipeline uses
426
+ * {@link coerceFilterValue} to recover real JS types from these
427
+ * strings.
428
+ */
429
+ private stringifyForCube;
430
+ /**
431
+ * Recover a runtime value from its cube-stringified form for in-memory
432
+ * comparison. Booleans, integers, floats and ISO-date-like strings are
433
+ * coerced; everything else stays as a string.
434
+ */
435
+ private coerceFilterValue;
436
+ /**
437
+ * Type-aware SQL literal formatter. Booleans and numbers are emitted
438
+ * unquoted; everything else is single-quoted with embedded quotes
439
+ * escaped.
440
+ */
441
+ private toSqlLiteral;
398
442
  private resolveFieldPath;
399
443
  private resolveMeasure;
400
444
  private resolveDimension;
package/dist/index.d.ts CHANGED
@@ -395,6 +395,50 @@ declare class MemoryAnalyticsService implements IAnalyticsService {
395
395
  sql: string;
396
396
  params: unknown[];
397
397
  }>;
398
+ /**
399
+ * Normalize filters into a cube-style array regardless of input shape.
400
+ *
401
+ * Accepts:
402
+ * - undefined / null → []
403
+ * - cube-style array `[{member, operator, values}]` → returned as-is
404
+ * - MongoDB FilterCondition object (per spec/data/filter.zod.ts):
405
+ * * implicit equality: `{is_active: true}`
406
+ * * operator wrapper: `{stage: {$nin: [...]}}`
407
+ * * mixed: `{stage: 'won', amount: {$gte: 100}}`
408
+ * → flattened into one cube-style entry per (field, operator) pair
409
+ *
410
+ * Logical combinators (`$and`, `$or`, `$not`) are not yet expanded into
411
+ * the cube pipeline; for current dashboard widget metadata the implicit
412
+ * top-level AND of fields is sufficient. `$and` clauses are flattened
413
+ * into the same AND list.
414
+ */
415
+ private normalizeFilters;
416
+ private flattenFilterCondition;
417
+ /**
418
+ * Map MongoDB-style `$op` keys (from FilterCondition) to the cube-style
419
+ * operator names accepted by `convertOperatorToMongo` / `operatorToSql`.
420
+ */
421
+ private mongoOperatorToCubeOperator;
422
+ /**
423
+ * Stringify a filter value for cube-style storage. Booleans become
424
+ * `'1'/'0'` so that downstream consumers expecting SQLite-style
425
+ * numeric booleans match correctly. The in-memory pipeline uses
426
+ * {@link coerceFilterValue} to recover real JS types from these
427
+ * strings.
428
+ */
429
+ private stringifyForCube;
430
+ /**
431
+ * Recover a runtime value from its cube-stringified form for in-memory
432
+ * comparison. Booleans, integers, floats and ISO-date-like strings are
433
+ * coerced; everything else stays as a string.
434
+ */
435
+ private coerceFilterValue;
436
+ /**
437
+ * Type-aware SQL literal formatter. Booleans and numbers are emitted
438
+ * unquoted; everything else is single-quoted with embedded quotes
439
+ * escaped.
440
+ */
441
+ private toSqlLiteral;
398
442
  private resolveFieldPath;
399
443
  private resolveMeasure;
400
444
  private resolveDimension;
package/dist/index.js CHANGED
@@ -876,9 +876,14 @@ var _InMemoryDriver = class _InMemoryDriver {
876
876
  performAggregation(records, query) {
877
877
  const { groupBy, aggregations } = query;
878
878
  const groups = /* @__PURE__ */ new Map();
879
+ const normalizeGroupBy = (node) => {
880
+ if (typeof node === "string") return { field: node, alias: node };
881
+ return { field: node.field, alias: node.alias ?? node.field };
882
+ };
879
883
  if (groupBy && groupBy.length > 0) {
880
884
  for (const record of records) {
881
- const keyParts = groupBy.map((field) => {
885
+ const keyParts = groupBy.map((node) => {
886
+ const { field } = normalizeGroupBy(node);
882
887
  const val = getValueByPath(record, field);
883
888
  return val === void 0 || val === null ? "null" : String(val);
884
889
  });
@@ -897,8 +902,9 @@ var _InMemoryDriver = class _InMemoryDriver {
897
902
  if (groupBy && groupBy.length > 0) {
898
903
  if (groupRecords.length > 0) {
899
904
  const firstRecord = groupRecords[0];
900
- for (const field of groupBy) {
901
- this.setValueByPath(row, field, getValueByPath(firstRecord, field));
905
+ for (const node of groupBy) {
906
+ const { field, alias } = normalizeGroupBy(node);
907
+ this.setValueByPath(row, alias, getValueByPath(firstRecord, field));
902
908
  }
903
909
  }
904
910
  }
@@ -1179,18 +1185,20 @@ var MemoryAnalyticsService = class {
1179
1185
  throw new Error(`Cube not found: ${query.cube}`);
1180
1186
  }
1181
1187
  const pipeline = [];
1182
- if (query.filters && query.filters.length > 0) {
1188
+ const normalizedFilters = this.normalizeFilters(query);
1189
+ if (normalizedFilters.length > 0) {
1183
1190
  const matchStage = {};
1184
- for (const filter of query.filters) {
1191
+ for (const filter of normalizedFilters) {
1185
1192
  const mongoOp = this.convertOperatorToMongo(filter.operator);
1186
1193
  const fieldPath = this.resolveFieldPath(cube, filter.member);
1187
1194
  if (filter.values && filter.values.length > 0) {
1195
+ const coerced = filter.values.map((v) => this.coerceFilterValue(v));
1188
1196
  if (mongoOp === "$in") {
1189
- matchStage[fieldPath] = { $in: filter.values };
1197
+ matchStage[fieldPath] = { $in: coerced };
1190
1198
  } else if (mongoOp === "$nin") {
1191
- matchStage[fieldPath] = { $nin: filter.values };
1199
+ matchStage[fieldPath] = { $nin: coerced };
1192
1200
  } else {
1193
- matchStage[fieldPath] = { [mongoOp]: filter.values[0] };
1201
+ matchStage[fieldPath] = { [mongoOp]: coerced[0] };
1194
1202
  }
1195
1203
  } else if (mongoOp === "$exists") {
1196
1204
  matchStage[fieldPath] = { $exists: filter.operator === "set" };
@@ -1367,12 +1375,14 @@ var MemoryAnalyticsService = class {
1367
1375
  }
1368
1376
  }
1369
1377
  const whereClauses = [];
1370
- if (query.filters && query.filters.length > 0) {
1371
- for (const filter of query.filters) {
1378
+ const normalizedFilters = this.normalizeFilters(query);
1379
+ if (normalizedFilters.length > 0) {
1380
+ for (const filter of normalizedFilters) {
1372
1381
  const fieldPath = this.resolveFieldPath(cube, filter.member);
1373
1382
  const sqlOp = this.operatorToSql(filter.operator);
1374
1383
  if (filter.values && filter.values.length > 0) {
1375
- whereClauses.push(`${fieldPath} ${sqlOp} '${filter.values[0]}'`);
1384
+ const literal = this.toSqlLiteral(filter.values[0]);
1385
+ whereClauses.push(`${fieldPath} ${sqlOp} ${literal}`);
1376
1386
  }
1377
1387
  }
1378
1388
  }
@@ -1400,6 +1410,147 @@ var MemoryAnalyticsService = class {
1400
1410
  // ===================================
1401
1411
  // Helper Methods
1402
1412
  // ===================================
1413
+ /**
1414
+ * Normalize filters into a cube-style array regardless of input shape.
1415
+ *
1416
+ * Accepts:
1417
+ * - undefined / null → []
1418
+ * - cube-style array `[{member, operator, values}]` → returned as-is
1419
+ * - MongoDB FilterCondition object (per spec/data/filter.zod.ts):
1420
+ * * implicit equality: `{is_active: true}`
1421
+ * * operator wrapper: `{stage: {$nin: [...]}}`
1422
+ * * mixed: `{stage: 'won', amount: {$gte: 100}}`
1423
+ * → flattened into one cube-style entry per (field, operator) pair
1424
+ *
1425
+ * Logical combinators (`$and`, `$or`, `$not`) are not yet expanded into
1426
+ * the cube pipeline; for current dashboard widget metadata the implicit
1427
+ * top-level AND of fields is sufficient. `$and` clauses are flattened
1428
+ * into the same AND list.
1429
+ */
1430
+ normalizeFilters(query) {
1431
+ if (!query || typeof query !== "object") return [];
1432
+ const out = [];
1433
+ const where = query.where;
1434
+ if (where && typeof where === "object" && !Array.isArray(where)) {
1435
+ this.flattenFilterCondition(where, out);
1436
+ }
1437
+ return out;
1438
+ }
1439
+ flattenFilterCondition(cond, out) {
1440
+ for (const [key, raw] of Object.entries(cond)) {
1441
+ if (raw == null) continue;
1442
+ if (key === "$and" && Array.isArray(raw)) {
1443
+ for (const sub of raw) {
1444
+ if (sub && typeof sub === "object") {
1445
+ this.flattenFilterCondition(sub, out);
1446
+ }
1447
+ }
1448
+ continue;
1449
+ }
1450
+ if (key === "$or" || key === "$not") continue;
1451
+ if (typeof raw === "object" && !Array.isArray(raw) && !(raw instanceof Date)) {
1452
+ const wrapper = raw;
1453
+ const opEntries = Object.keys(wrapper).filter((k) => k.startsWith("$"));
1454
+ if (opEntries.length > 0) {
1455
+ for (const opKey of opEntries) {
1456
+ const cubeOp = this.mongoOperatorToCubeOperator(opKey);
1457
+ if (!cubeOp) continue;
1458
+ const v = wrapper[opKey];
1459
+ const values2 = Array.isArray(v) ? v.map((x) => this.stringifyForCube(x)) : [this.stringifyForCube(v)];
1460
+ out.push({ member: key, operator: cubeOp, values: values2 });
1461
+ }
1462
+ continue;
1463
+ }
1464
+ for (const [nestedKey, nestedVal] of Object.entries(wrapper)) {
1465
+ this.flattenFilterCondition({ [`${key}.${nestedKey}`]: nestedVal }, out);
1466
+ }
1467
+ continue;
1468
+ }
1469
+ const values = Array.isArray(raw) ? raw.map((x) => this.stringifyForCube(x)) : [this.stringifyForCube(raw)];
1470
+ out.push({
1471
+ member: key,
1472
+ operator: Array.isArray(raw) ? "in" : "equals",
1473
+ values
1474
+ });
1475
+ }
1476
+ }
1477
+ /**
1478
+ * Map MongoDB-style `$op` keys (from FilterCondition) to the cube-style
1479
+ * operator names accepted by `convertOperatorToMongo` / `operatorToSql`.
1480
+ */
1481
+ mongoOperatorToCubeOperator(op) {
1482
+ switch (op) {
1483
+ case "$eq":
1484
+ return "equals";
1485
+ case "$ne":
1486
+ return "notEquals";
1487
+ case "$gt":
1488
+ return "gt";
1489
+ case "$gte":
1490
+ return "gte";
1491
+ case "$lt":
1492
+ return "lt";
1493
+ case "$lte":
1494
+ return "lte";
1495
+ case "$in":
1496
+ return "in";
1497
+ case "$nin":
1498
+ return "notIn";
1499
+ case "$contains":
1500
+ return "contains";
1501
+ case "$notContains":
1502
+ return "notContains";
1503
+ case "$exists":
1504
+ return "set";
1505
+ default:
1506
+ return null;
1507
+ }
1508
+ }
1509
+ /**
1510
+ * Stringify a filter value for cube-style storage. Booleans become
1511
+ * `'1'/'0'` so that downstream consumers expecting SQLite-style
1512
+ * numeric booleans match correctly. The in-memory pipeline uses
1513
+ * {@link coerceFilterValue} to recover real JS types from these
1514
+ * strings.
1515
+ */
1516
+ stringifyForCube(v) {
1517
+ if (v == null) return "";
1518
+ if (typeof v === "boolean") return v ? "1" : "0";
1519
+ if (v instanceof Date) return v.toISOString();
1520
+ if (typeof v === "object") return JSON.stringify(v);
1521
+ return String(v);
1522
+ }
1523
+ /**
1524
+ * Recover a runtime value from its cube-stringified form for in-memory
1525
+ * comparison. Booleans, integers, floats and ISO-date-like strings are
1526
+ * coerced; everything else stays as a string.
1527
+ */
1528
+ coerceFilterValue(s) {
1529
+ if (s === "true") return true;
1530
+ if (s === "false") return false;
1531
+ if (s === "null") return null;
1532
+ if (/^-?\d+$/.test(s)) {
1533
+ const n = Number(s);
1534
+ if (Number.isFinite(n)) return n;
1535
+ }
1536
+ if (/^-?\d+\.\d+$/.test(s)) {
1537
+ const n = Number(s);
1538
+ if (Number.isFinite(n)) return n;
1539
+ }
1540
+ return s;
1541
+ }
1542
+ /**
1543
+ * Type-aware SQL literal formatter. Booleans and numbers are emitted
1544
+ * unquoted; everything else is single-quoted with embedded quotes
1545
+ * escaped.
1546
+ */
1547
+ toSqlLiteral(s) {
1548
+ if (s === "true") return "1";
1549
+ if (s === "false") return "0";
1550
+ if (s === "null") return "NULL";
1551
+ if (/^-?\d+(\.\d+)?$/.test(s)) return s;
1552
+ return `'${s.replace(/'/g, "''")}'`;
1553
+ }
1403
1554
  resolveFieldPath(cube, member) {
1404
1555
  const parts = member.split(".");
1405
1556
  const fieldName = parts.length > 1 ? parts[1] : parts[0];
@@ -1488,6 +1639,8 @@ var MemoryAnalyticsService = class {
1488
1639
  "gte": "$gte",
1489
1640
  "lt": "$lt",
1490
1641
  "lte": "$lte",
1642
+ "in": "$in",
1643
+ "notIn": "$nin",
1491
1644
  "set": "$exists",
1492
1645
  "notSet": "$exists",
1493
1646
  "inDateRange": "$gte"