@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 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 clause = this.buildFilterClause(colExpr, filter.operator, filter.values, params);
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
- params.push(range[0], range[1]);
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
- buildFilterClause(col, operator, values, params) {
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(coerceFilterValueForSql(v));
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(coerceFilterValueForSql(values[0]));
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