@objectstack/driver-memory 4.0.5 → 4.1.1
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 +44 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +164 -11
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +164 -11
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
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((
|
|
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
|
|
901
|
-
|
|
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
|
-
|
|
1188
|
+
const normalizedFilters = this.normalizeFilters(query);
|
|
1189
|
+
if (normalizedFilters.length > 0) {
|
|
1183
1190
|
const matchStage = {};
|
|
1184
|
-
for (const filter of
|
|
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:
|
|
1197
|
+
matchStage[fieldPath] = { $in: coerced };
|
|
1190
1198
|
} else if (mongoOp === "$nin") {
|
|
1191
|
-
matchStage[fieldPath] = { $nin:
|
|
1199
|
+
matchStage[fieldPath] = { $nin: coerced };
|
|
1192
1200
|
} else {
|
|
1193
|
-
matchStage[fieldPath] = { [mongoOp]:
|
|
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
|
-
|
|
1371
|
-
|
|
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
|
-
|
|
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"
|