@objectstack/service-analytics 9.9.0 → 9.10.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.cjs +66 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +66 -6
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -358,6 +358,7 @@ var NativeSQLStrategy = class {
|
|
|
358
358
|
}
|
|
359
359
|
canHandle(query, ctx) {
|
|
360
360
|
if (!query.cube) return false;
|
|
361
|
+
if (query.timeDimensions?.some((td) => !!td.granularity)) return false;
|
|
361
362
|
const caps = ctx.queryCapabilities(query.cube);
|
|
362
363
|
return caps.nativeSql && typeof ctx.executeRawSql === "function";
|
|
363
364
|
}
|
|
@@ -397,7 +398,8 @@ var NativeSQLStrategy = class {
|
|
|
397
398
|
if (normalizedFilters.length > 0) {
|
|
398
399
|
for (const filter of normalizedFilters) {
|
|
399
400
|
const colExpr = this.resolveFieldSql(cube, filter.member, tableName, joins);
|
|
400
|
-
const
|
|
401
|
+
const target = this.resolveStorageTarget(cube, filter.member, tableName);
|
|
402
|
+
const clause = this.buildFilterClause(colExpr, filter.operator, filter.values, params, ctx, target);
|
|
401
403
|
if (clause) whereClauses.push(clause);
|
|
402
404
|
}
|
|
403
405
|
}
|
|
@@ -407,7 +409,11 @@ var NativeSQLStrategy = class {
|
|
|
407
409
|
if (td.dateRange) {
|
|
408
410
|
const range = Array.isArray(td.dateRange) ? td.dateRange : [td.dateRange, td.dateRange];
|
|
409
411
|
if (range.length === 2) {
|
|
410
|
-
|
|
412
|
+
const td2 = this.resolveStorageTarget(cube, td.dimension, tableName);
|
|
413
|
+
params.push(
|
|
414
|
+
this.coerceTemporal(ctx, td2, range[0]),
|
|
415
|
+
this.coerceTemporal(ctx, td2, range[1])
|
|
416
|
+
);
|
|
411
417
|
whereClauses.push(`${colExpr} BETWEEN $${params.length - 1} AND $${params.length}`);
|
|
412
418
|
}
|
|
413
419
|
}
|
|
@@ -571,7 +577,48 @@ var NativeSQLStrategy = class {
|
|
|
571
577
|
const fieldName = member.includes(".") ? member.split(".")[1] : member;
|
|
572
578
|
return fieldName;
|
|
573
579
|
}
|
|
574
|
-
|
|
580
|
+
/**
|
|
581
|
+
* Resolve the (object, column) a filter member binds against, so its
|
|
582
|
+
* comparand can be coerced to that column's on-disk storage form.
|
|
583
|
+
*
|
|
584
|
+
* Mirrors `resolveFieldSql`'s `sql` resolution but yields the *logical*
|
|
585
|
+
* target rather than the qualified SQL:
|
|
586
|
+
* - A dotted column (`account.region`, emitted for a relation traversal)
|
|
587
|
+
* belongs to the JOINED object — resolve the alias → target table via the
|
|
588
|
+
* cube's `joins` map (alias `account` → object `crm_account` when
|
|
589
|
+
* namespaced) and take the tail as the column.
|
|
590
|
+
* - Otherwise the column lives on the cube's BASE table. Use the dimension's
|
|
591
|
+
* resolved `sql` (the real column, which may differ from the member name,
|
|
592
|
+
* e.g. dimension `assessed` → column `assessed_at`) rather than the member.
|
|
593
|
+
*/
|
|
594
|
+
resolveStorageTarget(cube, member, baseTable) {
|
|
595
|
+
const dim = this.lookupMember(cube, member, "dimension");
|
|
596
|
+
const measure = dim ? void 0 : this.lookupMember(cube, member, "measure");
|
|
597
|
+
const rawSql = dim?.sql ?? measure?.sql ?? (member.includes(".") ? member.split(".").slice(1).join(".") : member);
|
|
598
|
+
if (rawSql.includes(".")) {
|
|
599
|
+
const [alias, ...rest] = rawSql.split(".");
|
|
600
|
+
const object = cube.joins?.[alias]?.name ?? alias;
|
|
601
|
+
return { object, field: rest.join(".") };
|
|
602
|
+
}
|
|
603
|
+
return { object: baseTable, field: rawSql };
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Apply the storage-form coercion for a single comparand. Prefers the
|
|
607
|
+
* driver-backed `coerceTemporalFilterValue` hook (single source of truth for
|
|
608
|
+
* the date/datetime storage convention — see StrategyContext); when the hook
|
|
609
|
+
* is absent, or returns the value unchanged (the field is not a temporal
|
|
610
|
+
* column, or the dialect stores it as a native timestamp), falls back to the
|
|
611
|
+
* generic boolean/number recovery so non-temporal typed columns still bind
|
|
612
|
+
* correctly.
|
|
613
|
+
*/
|
|
614
|
+
coerceTemporal(ctx, target, value) {
|
|
615
|
+
if (typeof ctx.coerceTemporalFilterValue === "function") {
|
|
616
|
+
const coerced = ctx.coerceTemporalFilterValue(target.object, target.field, value);
|
|
617
|
+
if (coerced !== value) return coerced;
|
|
618
|
+
}
|
|
619
|
+
return coerceFilterValueForSql(value);
|
|
620
|
+
}
|
|
621
|
+
buildFilterClause(col, operator, values, params, ctx, target) {
|
|
575
622
|
const opMap = {
|
|
576
623
|
equals: "=",
|
|
577
624
|
notEquals: "!=",
|
|
@@ -587,7 +634,7 @@ var NativeSQLStrategy = class {
|
|
|
587
634
|
if (operator === "in" || operator === "notIn") {
|
|
588
635
|
if (!values || values.length === 0) return null;
|
|
589
636
|
const placeholders = values.map((v) => {
|
|
590
|
-
params.push(
|
|
637
|
+
params.push(this.coerceTemporal(ctx, target, v));
|
|
591
638
|
return `$${params.length}`;
|
|
592
639
|
}).join(", ");
|
|
593
640
|
return `${col} ${operator === "in" ? "IN" : "NOT IN"} (${placeholders})`;
|
|
@@ -597,7 +644,7 @@ var NativeSQLStrategy = class {
|
|
|
597
644
|
if (operator === "contains" || operator === "notContains") {
|
|
598
645
|
params.push(`%${values[0]}%`);
|
|
599
646
|
} else {
|
|
600
|
-
params.push(
|
|
647
|
+
params.push(this.coerceTemporal(ctx, target, values[0]));
|
|
601
648
|
}
|
|
602
649
|
return `${col} ${sqlOp} $${params.length}`;
|
|
603
650
|
}
|
|
@@ -1467,7 +1514,8 @@ var AnalyticsService = class {
|
|
|
1467
1514
|
fallbackService: config.fallbackService,
|
|
1468
1515
|
// Prefer a compiled dataset's declared relationships (D-C join allowlist);
|
|
1469
1516
|
// fall back to any explicitly-configured provider for legacy cubes.
|
|
1470
|
-
getAllowedRelationships: (cubeName) => this.datasetRegistry.get(cubeName)?.allowedRelationships ?? config.getAllowedRelationships?.(cubeName)
|
|
1517
|
+
getAllowedRelationships: (cubeName) => this.datasetRegistry.get(cubeName)?.allowedRelationships ?? config.getAllowedRelationships?.(cubeName),
|
|
1518
|
+
coerceTemporalFilterValue: config.coerceTemporalFilterValue
|
|
1471
1519
|
};
|
|
1472
1520
|
const builtIn = [
|
|
1473
1521
|
new NativeSQLStrategy(),
|
|
@@ -2012,6 +2060,17 @@ var AnalyticsServicePlugin = class {
|
|
|
2012
2060
|
}
|
|
2013
2061
|
return pending ? rows : null;
|
|
2014
2062
|
};
|
|
2063
|
+
const coerceTemporalFilterValue = (objectName, fieldName, value) => {
|
|
2064
|
+
try {
|
|
2065
|
+
const svc = ctx.getService("data");
|
|
2066
|
+
const driver = svc?.getDriverForObject?.(objectName);
|
|
2067
|
+
if (driver && typeof driver.temporalFilterValue === "function") {
|
|
2068
|
+
return driver.temporalFilterValue(objectName, fieldName, value);
|
|
2069
|
+
}
|
|
2070
|
+
} catch {
|
|
2071
|
+
}
|
|
2072
|
+
return value;
|
|
2073
|
+
};
|
|
2015
2074
|
const config = {
|
|
2016
2075
|
cubes: this.options.cubes,
|
|
2017
2076
|
logger: ctx.logger,
|
|
@@ -2021,6 +2080,7 @@ var AnalyticsServicePlugin = class {
|
|
|
2021
2080
|
fallbackService,
|
|
2022
2081
|
getReadScope,
|
|
2023
2082
|
getAllowedRelationships: this.options.getAllowedRelationships,
|
|
2083
|
+
coerceTemporalFilterValue,
|
|
2024
2084
|
relationshipResolver,
|
|
2025
2085
|
labelResolver,
|
|
2026
2086
|
draftRowsResolver
|