@objectstack/service-analytics 9.9.1 → 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.d.cts
CHANGED
|
@@ -212,6 +212,15 @@ interface AnalyticsServiceConfig {
|
|
|
212
212
|
* config hook is a fallback for legacy hand-authored cubes.
|
|
213
213
|
*/
|
|
214
214
|
getAllowedRelationships?: (cubeName: string) => Set<string> | undefined;
|
|
215
|
+
/**
|
|
216
|
+
* Coerce a filter comparand to a temporal column's storage form so a
|
|
217
|
+
* relative-date / ISO-string value compares correctly on the active driver
|
|
218
|
+
* (SQLite `Field.datetime` → epoch ms; `Field.date` / native timestamp →
|
|
219
|
+
* unchanged). Threaded into the StrategyContext and consulted by
|
|
220
|
+
* `NativeSQLStrategy` when binding filter values. See the contract docs on
|
|
221
|
+
* `StrategyContext.coerceTemporalFilterValue` for the full rationale.
|
|
222
|
+
*/
|
|
223
|
+
coerceTemporalFilterValue?: (objectName: string, fieldName: string, value: unknown) => unknown;
|
|
215
224
|
/**
|
|
216
225
|
* ADR-0021 — optional object-graph resolver used when compiling datasets:
|
|
217
226
|
* `(baseObject, relationshipName) => relatedObjectName | undefined`. When
|
|
@@ -561,6 +570,31 @@ declare class NativeSQLStrategy implements AnalyticsStrategy {
|
|
|
561
570
|
private resolveDimensionSql;
|
|
562
571
|
private resolveMeasureSql;
|
|
563
572
|
private resolveFieldSql;
|
|
573
|
+
/**
|
|
574
|
+
* Resolve the (object, column) a filter member binds against, so its
|
|
575
|
+
* comparand can be coerced to that column's on-disk storage form.
|
|
576
|
+
*
|
|
577
|
+
* Mirrors `resolveFieldSql`'s `sql` resolution but yields the *logical*
|
|
578
|
+
* target rather than the qualified SQL:
|
|
579
|
+
* - A dotted column (`account.region`, emitted for a relation traversal)
|
|
580
|
+
* belongs to the JOINED object — resolve the alias → target table via the
|
|
581
|
+
* cube's `joins` map (alias `account` → object `crm_account` when
|
|
582
|
+
* namespaced) and take the tail as the column.
|
|
583
|
+
* - Otherwise the column lives on the cube's BASE table. Use the dimension's
|
|
584
|
+
* resolved `sql` (the real column, which may differ from the member name,
|
|
585
|
+
* e.g. dimension `assessed` → column `assessed_at`) rather than the member.
|
|
586
|
+
*/
|
|
587
|
+
private resolveStorageTarget;
|
|
588
|
+
/**
|
|
589
|
+
* Apply the storage-form coercion for a single comparand. Prefers the
|
|
590
|
+
* driver-backed `coerceTemporalFilterValue` hook (single source of truth for
|
|
591
|
+
* the date/datetime storage convention — see StrategyContext); when the hook
|
|
592
|
+
* is absent, or returns the value unchanged (the field is not a temporal
|
|
593
|
+
* column, or the dialect stores it as a native timestamp), falls back to the
|
|
594
|
+
* generic boolean/number recovery so non-temporal typed columns still bind
|
|
595
|
+
* correctly.
|
|
596
|
+
*/
|
|
597
|
+
private coerceTemporal;
|
|
564
598
|
private buildFilterClause;
|
|
565
599
|
private extractObjectName;
|
|
566
600
|
private buildFieldMeta;
|
package/dist/index.d.ts
CHANGED
|
@@ -212,6 +212,15 @@ interface AnalyticsServiceConfig {
|
|
|
212
212
|
* config hook is a fallback for legacy hand-authored cubes.
|
|
213
213
|
*/
|
|
214
214
|
getAllowedRelationships?: (cubeName: string) => Set<string> | undefined;
|
|
215
|
+
/**
|
|
216
|
+
* Coerce a filter comparand to a temporal column's storage form so a
|
|
217
|
+
* relative-date / ISO-string value compares correctly on the active driver
|
|
218
|
+
* (SQLite `Field.datetime` → epoch ms; `Field.date` / native timestamp →
|
|
219
|
+
* unchanged). Threaded into the StrategyContext and consulted by
|
|
220
|
+
* `NativeSQLStrategy` when binding filter values. See the contract docs on
|
|
221
|
+
* `StrategyContext.coerceTemporalFilterValue` for the full rationale.
|
|
222
|
+
*/
|
|
223
|
+
coerceTemporalFilterValue?: (objectName: string, fieldName: string, value: unknown) => unknown;
|
|
215
224
|
/**
|
|
216
225
|
* ADR-0021 — optional object-graph resolver used when compiling datasets:
|
|
217
226
|
* `(baseObject, relationshipName) => relatedObjectName | undefined`. When
|
|
@@ -561,6 +570,31 @@ declare class NativeSQLStrategy implements AnalyticsStrategy {
|
|
|
561
570
|
private resolveDimensionSql;
|
|
562
571
|
private resolveMeasureSql;
|
|
563
572
|
private resolveFieldSql;
|
|
573
|
+
/**
|
|
574
|
+
* Resolve the (object, column) a filter member binds against, so its
|
|
575
|
+
* comparand can be coerced to that column's on-disk storage form.
|
|
576
|
+
*
|
|
577
|
+
* Mirrors `resolveFieldSql`'s `sql` resolution but yields the *logical*
|
|
578
|
+
* target rather than the qualified SQL:
|
|
579
|
+
* - A dotted column (`account.region`, emitted for a relation traversal)
|
|
580
|
+
* belongs to the JOINED object — resolve the alias → target table via the
|
|
581
|
+
* cube's `joins` map (alias `account` → object `crm_account` when
|
|
582
|
+
* namespaced) and take the tail as the column.
|
|
583
|
+
* - Otherwise the column lives on the cube's BASE table. Use the dimension's
|
|
584
|
+
* resolved `sql` (the real column, which may differ from the member name,
|
|
585
|
+
* e.g. dimension `assessed` → column `assessed_at`) rather than the member.
|
|
586
|
+
*/
|
|
587
|
+
private resolveStorageTarget;
|
|
588
|
+
/**
|
|
589
|
+
* Apply the storage-form coercion for a single comparand. Prefers the
|
|
590
|
+
* driver-backed `coerceTemporalFilterValue` hook (single source of truth for
|
|
591
|
+
* the date/datetime storage convention — see StrategyContext); when the hook
|
|
592
|
+
* is absent, or returns the value unchanged (the field is not a temporal
|
|
593
|
+
* column, or the dialect stores it as a native timestamp), falls back to the
|
|
594
|
+
* generic boolean/number recovery so non-temporal typed columns still bind
|
|
595
|
+
* correctly.
|
|
596
|
+
*/
|
|
597
|
+
private coerceTemporal;
|
|
564
598
|
private buildFilterClause;
|
|
565
599
|
private extractObjectName;
|
|
566
600
|
private buildFieldMeta;
|
package/dist/index.js
CHANGED
|
@@ -319,6 +319,7 @@ var NativeSQLStrategy = class {
|
|
|
319
319
|
}
|
|
320
320
|
canHandle(query, ctx) {
|
|
321
321
|
if (!query.cube) return false;
|
|
322
|
+
if (query.timeDimensions?.some((td) => !!td.granularity)) return false;
|
|
322
323
|
const caps = ctx.queryCapabilities(query.cube);
|
|
323
324
|
return caps.nativeSql && typeof ctx.executeRawSql === "function";
|
|
324
325
|
}
|
|
@@ -358,7 +359,8 @@ var NativeSQLStrategy = class {
|
|
|
358
359
|
if (normalizedFilters.length > 0) {
|
|
359
360
|
for (const filter of normalizedFilters) {
|
|
360
361
|
const colExpr = this.resolveFieldSql(cube, filter.member, tableName, joins);
|
|
361
|
-
const
|
|
362
|
+
const target = this.resolveStorageTarget(cube, filter.member, tableName);
|
|
363
|
+
const clause = this.buildFilterClause(colExpr, filter.operator, filter.values, params, ctx, target);
|
|
362
364
|
if (clause) whereClauses.push(clause);
|
|
363
365
|
}
|
|
364
366
|
}
|
|
@@ -368,7 +370,11 @@ var NativeSQLStrategy = class {
|
|
|
368
370
|
if (td.dateRange) {
|
|
369
371
|
const range = Array.isArray(td.dateRange) ? td.dateRange : [td.dateRange, td.dateRange];
|
|
370
372
|
if (range.length === 2) {
|
|
371
|
-
|
|
373
|
+
const td2 = this.resolveStorageTarget(cube, td.dimension, tableName);
|
|
374
|
+
params.push(
|
|
375
|
+
this.coerceTemporal(ctx, td2, range[0]),
|
|
376
|
+
this.coerceTemporal(ctx, td2, range[1])
|
|
377
|
+
);
|
|
372
378
|
whereClauses.push(`${colExpr} BETWEEN $${params.length - 1} AND $${params.length}`);
|
|
373
379
|
}
|
|
374
380
|
}
|
|
@@ -532,7 +538,48 @@ var NativeSQLStrategy = class {
|
|
|
532
538
|
const fieldName = member.includes(".") ? member.split(".")[1] : member;
|
|
533
539
|
return fieldName;
|
|
534
540
|
}
|
|
535
|
-
|
|
541
|
+
/**
|
|
542
|
+
* Resolve the (object, column) a filter member binds against, so its
|
|
543
|
+
* comparand can be coerced to that column's on-disk storage form.
|
|
544
|
+
*
|
|
545
|
+
* Mirrors `resolveFieldSql`'s `sql` resolution but yields the *logical*
|
|
546
|
+
* target rather than the qualified SQL:
|
|
547
|
+
* - A dotted column (`account.region`, emitted for a relation traversal)
|
|
548
|
+
* belongs to the JOINED object — resolve the alias → target table via the
|
|
549
|
+
* cube's `joins` map (alias `account` → object `crm_account` when
|
|
550
|
+
* namespaced) and take the tail as the column.
|
|
551
|
+
* - Otherwise the column lives on the cube's BASE table. Use the dimension's
|
|
552
|
+
* resolved `sql` (the real column, which may differ from the member name,
|
|
553
|
+
* e.g. dimension `assessed` → column `assessed_at`) rather than the member.
|
|
554
|
+
*/
|
|
555
|
+
resolveStorageTarget(cube, member, baseTable) {
|
|
556
|
+
const dim = this.lookupMember(cube, member, "dimension");
|
|
557
|
+
const measure = dim ? void 0 : this.lookupMember(cube, member, "measure");
|
|
558
|
+
const rawSql = dim?.sql ?? measure?.sql ?? (member.includes(".") ? member.split(".").slice(1).join(".") : member);
|
|
559
|
+
if (rawSql.includes(".")) {
|
|
560
|
+
const [alias, ...rest] = rawSql.split(".");
|
|
561
|
+
const object = cube.joins?.[alias]?.name ?? alias;
|
|
562
|
+
return { object, field: rest.join(".") };
|
|
563
|
+
}
|
|
564
|
+
return { object: baseTable, field: rawSql };
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Apply the storage-form coercion for a single comparand. Prefers the
|
|
568
|
+
* driver-backed `coerceTemporalFilterValue` hook (single source of truth for
|
|
569
|
+
* the date/datetime storage convention — see StrategyContext); when the hook
|
|
570
|
+
* is absent, or returns the value unchanged (the field is not a temporal
|
|
571
|
+
* column, or the dialect stores it as a native timestamp), falls back to the
|
|
572
|
+
* generic boolean/number recovery so non-temporal typed columns still bind
|
|
573
|
+
* correctly.
|
|
574
|
+
*/
|
|
575
|
+
coerceTemporal(ctx, target, value) {
|
|
576
|
+
if (typeof ctx.coerceTemporalFilterValue === "function") {
|
|
577
|
+
const coerced = ctx.coerceTemporalFilterValue(target.object, target.field, value);
|
|
578
|
+
if (coerced !== value) return coerced;
|
|
579
|
+
}
|
|
580
|
+
return coerceFilterValueForSql(value);
|
|
581
|
+
}
|
|
582
|
+
buildFilterClause(col, operator, values, params, ctx, target) {
|
|
536
583
|
const opMap = {
|
|
537
584
|
equals: "=",
|
|
538
585
|
notEquals: "!=",
|
|
@@ -548,7 +595,7 @@ var NativeSQLStrategy = class {
|
|
|
548
595
|
if (operator === "in" || operator === "notIn") {
|
|
549
596
|
if (!values || values.length === 0) return null;
|
|
550
597
|
const placeholders = values.map((v) => {
|
|
551
|
-
params.push(
|
|
598
|
+
params.push(this.coerceTemporal(ctx, target, v));
|
|
552
599
|
return `$${params.length}`;
|
|
553
600
|
}).join(", ");
|
|
554
601
|
return `${col} ${operator === "in" ? "IN" : "NOT IN"} (${placeholders})`;
|
|
@@ -558,7 +605,7 @@ var NativeSQLStrategy = class {
|
|
|
558
605
|
if (operator === "contains" || operator === "notContains") {
|
|
559
606
|
params.push(`%${values[0]}%`);
|
|
560
607
|
} else {
|
|
561
|
-
params.push(
|
|
608
|
+
params.push(this.coerceTemporal(ctx, target, values[0]));
|
|
562
609
|
}
|
|
563
610
|
return `${col} ${sqlOp} $${params.length}`;
|
|
564
611
|
}
|
|
@@ -1428,7 +1475,8 @@ var AnalyticsService = class {
|
|
|
1428
1475
|
fallbackService: config.fallbackService,
|
|
1429
1476
|
// Prefer a compiled dataset's declared relationships (D-C join allowlist);
|
|
1430
1477
|
// fall back to any explicitly-configured provider for legacy cubes.
|
|
1431
|
-
getAllowedRelationships: (cubeName) => this.datasetRegistry.get(cubeName)?.allowedRelationships ?? config.getAllowedRelationships?.(cubeName)
|
|
1478
|
+
getAllowedRelationships: (cubeName) => this.datasetRegistry.get(cubeName)?.allowedRelationships ?? config.getAllowedRelationships?.(cubeName),
|
|
1479
|
+
coerceTemporalFilterValue: config.coerceTemporalFilterValue
|
|
1432
1480
|
};
|
|
1433
1481
|
const builtIn = [
|
|
1434
1482
|
new NativeSQLStrategy(),
|
|
@@ -1973,6 +2021,17 @@ var AnalyticsServicePlugin = class {
|
|
|
1973
2021
|
}
|
|
1974
2022
|
return pending ? rows : null;
|
|
1975
2023
|
};
|
|
2024
|
+
const coerceTemporalFilterValue = (objectName, fieldName, value) => {
|
|
2025
|
+
try {
|
|
2026
|
+
const svc = ctx.getService("data");
|
|
2027
|
+
const driver = svc?.getDriverForObject?.(objectName);
|
|
2028
|
+
if (driver && typeof driver.temporalFilterValue === "function") {
|
|
2029
|
+
return driver.temporalFilterValue(objectName, fieldName, value);
|
|
2030
|
+
}
|
|
2031
|
+
} catch {
|
|
2032
|
+
}
|
|
2033
|
+
return value;
|
|
2034
|
+
};
|
|
1976
2035
|
const config = {
|
|
1977
2036
|
cubes: this.options.cubes,
|
|
1978
2037
|
logger: ctx.logger,
|
|
@@ -1982,6 +2041,7 @@ var AnalyticsServicePlugin = class {
|
|
|
1982
2041
|
fallbackService,
|
|
1983
2042
|
getReadScope,
|
|
1984
2043
|
getAllowedRelationships: this.options.getAllowedRelationships,
|
|
2044
|
+
coerceTemporalFilterValue,
|
|
1985
2045
|
relationshipResolver,
|
|
1986
2046
|
labelResolver,
|
|
1987
2047
|
draftRowsResolver
|