@objectstack/service-analytics 9.8.0 → 9.9.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
@@ -381,6 +381,8 @@ interface AnalyticsServicePluginOptions {
381
381
  alias: string;
382
382
  }>;
383
383
  filter?: Record<string, unknown>;
384
+ /** Reference timezone (IANA) for date bucketing — ADR-0053 Phase 2. */
385
+ timezone?: string;
384
386
  }) => Promise<Record<string, unknown>[]>;
385
387
  /**
386
388
  * ADR-0021 D-C — context-aware per-object read scope (tenant + RLS). The
package/dist/index.d.ts CHANGED
@@ -381,6 +381,8 @@ interface AnalyticsServicePluginOptions {
381
381
  alias: string;
382
382
  }>;
383
383
  filter?: Record<string, unknown>;
384
+ /** Reference timezone (IANA) for date bucketing — ADR-0053 Phase 2. */
385
+ timezone?: string;
384
386
  }) => Promise<Record<string, unknown>[]>;
385
387
  /**
386
388
  * ADR-0021 D-C — context-aware per-object read scope (tenant + RLS). The
package/dist/index.js CHANGED
@@ -636,7 +636,11 @@ var ObjectQLStrategy = class {
636
636
  // contract types groupBy as string[]; the cast carries the richer shape.
637
637
  groupBy: groupBy.length > 0 ? groupBy : void 0,
638
638
  aggregations: aggregations.length > 0 ? aggregations : void 0,
639
- filter: Object.keys(filter).length > 0 ? filter : void 0
639
+ filter: Object.keys(filter).length > 0 ? filter : void 0,
640
+ // ADR-0053 Phase 2 (D2): forward the reference tz so date buckets resolve
641
+ // on that zone's calendar days. A non-UTC zone makes the engine bucket
642
+ // in-memory (uniform across drivers); UTC/unset keeps the DB fast path.
643
+ timezone: query.timezone
640
644
  });
641
645
  const mappedRows = rows.map((row) => {
642
646
  const mapped = {};
@@ -1035,7 +1039,8 @@ var DatasetExecutor = class {
1035
1039
  measures: unfiltered,
1036
1040
  dimensions,
1037
1041
  where: baseFilter,
1038
- selection
1042
+ selection,
1043
+ contextTimezone: context?.timezone
1039
1044
  }), context);
1040
1045
  } else {
1041
1046
  result = { rows: [], fields: [] };
@@ -1046,7 +1051,8 @@ var DatasetExecutor = class {
1046
1051
  measures: [m],
1047
1052
  dimensions,
1048
1053
  where: mFilter,
1049
- selection
1054
+ selection,
1055
+ contextTimezone: context?.timezone
1050
1056
  }), context);
1051
1057
  result.rows = mergeByDimensions(result.rows, sub.rows, dimensions, [m]);
1052
1058
  result.fields.push({ name: m, type: "number" });
@@ -1070,7 +1076,9 @@ var DatasetExecutor = class {
1070
1076
  cube: compiled.cube.name,
1071
1077
  measures: opts.measures,
1072
1078
  dimensions: opts.dimensions,
1073
- timezone: opts.selection.timezone ?? "UTC"
1079
+ // Precedence: explicit selection tz → request's reference tz
1080
+ // (ExecutionContext.timezone, ADR-0053 Phase 2) → UTC.
1081
+ timezone: opts.selection.timezone ?? opts.contextTimezone ?? "UTC"
1074
1082
  };
1075
1083
  if (opts.where) q.where = opts.where;
1076
1084
  const selTimeDims = opts.selection.timeDimensions ?? [];
@@ -1108,7 +1116,7 @@ var DatasetExecutor = class {
1108
1116
  dimensions,
1109
1117
  where: baseFilter,
1110
1118
  timeDimensions: shiftedTd,
1111
- timezone: selection.timezone ?? "UTC"
1119
+ timezone: selection.timezone ?? context?.timezone ?? "UTC"
1112
1120
  }, context);
1113
1121
  return sub.rows.map((row) => {
1114
1122
  const out = {};
@@ -1221,6 +1229,7 @@ function pickDisplayField(fields) {
1221
1229
  }
1222
1230
 
1223
1231
  // src/preview-evaluator.ts
1232
+ import { calendarPartsInTzOrUtc } from "@objectstack/core";
1224
1233
  function compare(a, b) {
1225
1234
  if (typeof a === "number" && typeof b === "number") return a - b;
1226
1235
  return String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0;
@@ -1268,23 +1277,23 @@ function matchesWhere(row, where) {
1268
1277
  }
1269
1278
  return true;
1270
1279
  }
1271
- function bucketDate(value, granularity) {
1280
+ function bucketDate(value, granularity, timezone) {
1272
1281
  const d = new Date(String(value));
1273
1282
  if (Number.isNaN(d.getTime())) return null;
1274
- const y = d.getUTCFullYear();
1275
- const m = `${d.getUTCMonth() + 1}`.padStart(2, "0");
1276
- const day = `${d.getUTCDate()}`.padStart(2, "0");
1283
+ const { year: y, month, day: dayNum } = calendarPartsInTzOrUtc(d, timezone);
1284
+ const m = `${month}`.padStart(2, "0");
1285
+ const day = `${dayNum}`.padStart(2, "0");
1277
1286
  switch (granularity) {
1278
1287
  case "year":
1279
1288
  return `${y}`;
1280
1289
  case "quarter":
1281
- return `${y}-Q${Math.floor(d.getUTCMonth() / 3) + 1}`;
1290
+ return `${y}-Q${Math.floor((month - 1) / 3) + 1}`;
1282
1291
  case "month":
1283
1292
  return `${y}-${m}`;
1284
1293
  case "week": {
1285
- const monday = new Date(d);
1286
- const dow = (d.getUTCDay() + 6) % 7;
1287
- monday.setUTCDate(d.getUTCDate() - dow);
1294
+ const monday = new Date(Date.UTC(y, month - 1, dayNum));
1295
+ const dow = (monday.getUTCDay() + 6) % 7;
1296
+ monday.setUTCDate(monday.getUTCDate() - dow);
1288
1297
  return monday.toISOString().slice(0, 10);
1289
1298
  }
1290
1299
  case "day":
@@ -1329,6 +1338,7 @@ function evaluateAnalyticsQueryOverRows(query, cube, rows) {
1329
1338
  });
1330
1339
  }
1331
1340
  const dimensions = query.dimensions ?? [];
1341
+ const timezone = query.timezone;
1332
1342
  const granByDim = new Map(timeDims.filter((t) => t.granularity).map((t) => [t.dimension, t.granularity]));
1333
1343
  const keyOf = (r) => {
1334
1344
  const values = {};
@@ -1337,7 +1347,7 @@ function evaluateAnalyticsQueryOverRows(query, cube, rows) {
1337
1347
  const field = String(dim?.sql ?? name);
1338
1348
  const raw = r[field];
1339
1349
  const gran = granByDim.get(name) ?? (dim?.type === "time" && dim.granularities?.length === 1 ? String(dim.granularities[0]) : void 0);
1340
- values[name] = gran ? bucketDate(raw, gran) : raw ?? null;
1350
+ values[name] = gran ? bucketDate(raw, gran, timezone) : raw ?? null;
1341
1351
  }
1342
1352
  return { key: JSON.stringify(values), values };
1343
1353
  };
@@ -1818,7 +1828,7 @@ var AnalyticsServicePlugin = class {
1818
1828
  '[Analytics] No "data" service registered yet at init; will retry per-query. Register ObjectQLPlugin or pass executeAggregate.'
1819
1829
  );
1820
1830
  }
1821
- executeAggregate = async (objectName, { groupBy, aggregations, filter }) => {
1831
+ executeAggregate = async (objectName, { groupBy, aggregations, filter, timezone }) => {
1822
1832
  const engine = tryGetDataEngine();
1823
1833
  if (!engine) {
1824
1834
  throw new Error(
@@ -1832,7 +1842,10 @@ var AnalyticsServicePlugin = class {
1832
1842
  function: a.method,
1833
1843
  field: a.field,
1834
1844
  alias: a.alias
1835
- }))
1845
+ })),
1846
+ // ADR-0053 Phase 2: thread the reference tz so date buckets resolve on
1847
+ // that zone's calendar days (engine buckets in-memory when non-UTC).
1848
+ timezone
1836
1849
  });
1837
1850
  return rows;
1838
1851
  };