@objectstack/service-analytics 9.2.0 → 9.3.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.cts CHANGED
@@ -452,7 +452,10 @@ type CompareTo = DatasetCompareTo;
452
452
  * - applies measure-scoped filters via supplementary grouped queries,
453
453
  * - evaluates derived measures (ratio/sum/difference/product) row-by-row (Q1),
454
454
  * - shifts the query for `compareTo` (previousPeriod / previousYear) and
455
- * attaches `<measure>__compare` columns.
455
+ * attaches `<measure>__compare` columns,
456
+ * - computes server-side totals (`selection.totals.groupings`, #1753) by
457
+ * re-running the selection per dimension subset, so matrix subtotals and
458
+ * the grand total use each measure's true aggregate.
456
459
  *
457
460
  * RLS/tenant scoping is NOT handled here — it is enforced inside the strategy
458
461
  * via the StrategyContext read-scope hook (D-C). This layer is pure query
@@ -478,6 +481,7 @@ declare class DatasetExecutor {
478
481
  * applied per request (ADR-0021 D-C).
479
482
  */
480
483
  execute(compiled: CompiledDataset, selection: DatasetSelection, context?: ExecutionContext): Promise<AnalyticsResult>;
484
+ private executeSelection;
481
485
  private buildQuery;
482
486
  private runCompare;
483
487
  }
package/dist/index.d.ts CHANGED
@@ -452,7 +452,10 @@ type CompareTo = DatasetCompareTo;
452
452
  * - applies measure-scoped filters via supplementary grouped queries,
453
453
  * - evaluates derived measures (ratio/sum/difference/product) row-by-row (Q1),
454
454
  * - shifts the query for `compareTo` (previousPeriod / previousYear) and
455
- * attaches `<measure>__compare` columns.
455
+ * attaches `<measure>__compare` columns,
456
+ * - computes server-side totals (`selection.totals.groupings`, #1753) by
457
+ * re-running the selection per dimension subset, so matrix subtotals and
458
+ * the grand total use each measure's true aggregate.
456
459
  *
457
460
  * RLS/tenant scoping is NOT handled here — it is enforced inside the strategy
458
461
  * via the StrategyContext read-scope hook (D-C). This layer is pure query
@@ -478,6 +481,7 @@ declare class DatasetExecutor {
478
481
  * applied per request (ADR-0021 D-C).
479
482
  */
480
483
  execute(compiled: CompiledDataset, selection: DatasetSelection, context?: ExecutionContext): Promise<AnalyticsResult>;
484
+ private executeSelection;
481
485
  private buildQuery;
482
486
  private runCompare;
483
487
  }
package/dist/index.js CHANGED
@@ -986,6 +986,33 @@ var DatasetExecutor = class {
986
986
  * applied per request (ADR-0021 D-C).
987
987
  */
988
988
  async execute(compiled, selection, context) {
989
+ const result = await this.executeSelection(compiled, selection, context);
990
+ const groupings = selection.totals?.groupings;
991
+ if (groupings?.length) {
992
+ const selected = new Set(selection.dimensions ?? []);
993
+ const totals = [];
994
+ for (const grouping of groupings) {
995
+ const unknown = grouping.filter((d) => !selected.has(d));
996
+ if (unknown.length) {
997
+ throw new Error(
998
+ `[dataset-executor] totals grouping [${grouping.join(", ")}] is not a subset of the selected dimensions \u2014 unknown: ${unknown.join(", ")}.`
999
+ );
1000
+ }
1001
+ const sub = await this.executeSelection(compiled, {
1002
+ ...selection,
1003
+ dimensions: grouping,
1004
+ totals: void 0,
1005
+ order: void 0,
1006
+ limit: void 0,
1007
+ offset: void 0
1008
+ }, context);
1009
+ totals.push({ dimensions: grouping, rows: sub.rows });
1010
+ }
1011
+ result.totals = totals;
1012
+ }
1013
+ return result;
1014
+ }
1015
+ async executeSelection(compiled, selection, context) {
989
1016
  const derivedByName = new Map(compiled.derived.map((d) => [d.name, d]));
990
1017
  const selectedDerived = selection.measures.map((m) => derivedByName.get(m)).filter((d) => !!d);
991
1018
  const baseMeasures = /* @__PURE__ */ new Set();
@@ -1534,6 +1561,12 @@ var AnalyticsService = class {
1534
1561
  if (dims.length) {
1535
1562
  try {
1536
1563
  await resolveDimensionLabels(dataset.object, dims, result.rows, this.labelResolver);
1564
+ for (const total of result.totals ?? []) {
1565
+ const subset = dims.filter((d) => total.dimensions.includes(d.name));
1566
+ if (subset.length) {
1567
+ await resolveDimensionLabels(dataset.object, subset, total.rows, this.labelResolver);
1568
+ }
1569
+ }
1537
1570
  } catch (e) {
1538
1571
  this.logger?.warn?.(`[Analytics] dimension label resolution failed for "${dataset.name}": ${String(e?.message ?? e)}`);
1539
1572
  }