@objectstack/service-analytics 9.7.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.cjs CHANGED
@@ -38,7 +38,7 @@ __export(index_exports, {
38
38
  module.exports = __toCommonJS(index_exports);
39
39
 
40
40
  // src/analytics-service.ts
41
- var import_core = require("@objectstack/core");
41
+ var import_core2 = require("@objectstack/core");
42
42
 
43
43
  // src/cube-registry.ts
44
44
  var CubeRegistry = class {
@@ -675,7 +675,11 @@ var ObjectQLStrategy = class {
675
675
  // contract types groupBy as string[]; the cast carries the richer shape.
676
676
  groupBy: groupBy.length > 0 ? groupBy : void 0,
677
677
  aggregations: aggregations.length > 0 ? aggregations : void 0,
678
- filter: Object.keys(filter).length > 0 ? filter : void 0
678
+ filter: Object.keys(filter).length > 0 ? filter : void 0,
679
+ // ADR-0053 Phase 2 (D2): forward the reference tz so date buckets resolve
680
+ // on that zone's calendar days. A non-UTC zone makes the engine bucket
681
+ // in-memory (uniform across drivers); UTC/unset keeps the DB fast path.
682
+ timezone: query.timezone
679
683
  });
680
684
  const mappedRows = rows.map((row) => {
681
685
  const mapped = {};
@@ -1074,7 +1078,8 @@ var DatasetExecutor = class {
1074
1078
  measures: unfiltered,
1075
1079
  dimensions,
1076
1080
  where: baseFilter,
1077
- selection
1081
+ selection,
1082
+ contextTimezone: context?.timezone
1078
1083
  }), context);
1079
1084
  } else {
1080
1085
  result = { rows: [], fields: [] };
@@ -1085,7 +1090,8 @@ var DatasetExecutor = class {
1085
1090
  measures: [m],
1086
1091
  dimensions,
1087
1092
  where: mFilter,
1088
- selection
1093
+ selection,
1094
+ contextTimezone: context?.timezone
1089
1095
  }), context);
1090
1096
  result.rows = mergeByDimensions(result.rows, sub.rows, dimensions, [m]);
1091
1097
  result.fields.push({ name: m, type: "number" });
@@ -1109,7 +1115,9 @@ var DatasetExecutor = class {
1109
1115
  cube: compiled.cube.name,
1110
1116
  measures: opts.measures,
1111
1117
  dimensions: opts.dimensions,
1112
- timezone: opts.selection.timezone ?? "UTC"
1118
+ // Precedence: explicit selection tz → request's reference tz
1119
+ // (ExecutionContext.timezone, ADR-0053 Phase 2) → UTC.
1120
+ timezone: opts.selection.timezone ?? opts.contextTimezone ?? "UTC"
1113
1121
  };
1114
1122
  if (opts.where) q.where = opts.where;
1115
1123
  const selTimeDims = opts.selection.timeDimensions ?? [];
@@ -1147,7 +1155,7 @@ var DatasetExecutor = class {
1147
1155
  dimensions,
1148
1156
  where: baseFilter,
1149
1157
  timeDimensions: shiftedTd,
1150
- timezone: selection.timezone ?? "UTC"
1158
+ timezone: selection.timezone ?? context?.timezone ?? "UTC"
1151
1159
  }, context);
1152
1160
  return sub.rows.map((row) => {
1153
1161
  const out = {};
@@ -1260,6 +1268,7 @@ function pickDisplayField(fields) {
1260
1268
  }
1261
1269
 
1262
1270
  // src/preview-evaluator.ts
1271
+ var import_core = require("@objectstack/core");
1263
1272
  function compare(a, b) {
1264
1273
  if (typeof a === "number" && typeof b === "number") return a - b;
1265
1274
  return String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0;
@@ -1307,23 +1316,23 @@ function matchesWhere(row, where) {
1307
1316
  }
1308
1317
  return true;
1309
1318
  }
1310
- function bucketDate(value, granularity) {
1319
+ function bucketDate(value, granularity, timezone) {
1311
1320
  const d = new Date(String(value));
1312
1321
  if (Number.isNaN(d.getTime())) return null;
1313
- const y = d.getUTCFullYear();
1314
- const m = `${d.getUTCMonth() + 1}`.padStart(2, "0");
1315
- const day = `${d.getUTCDate()}`.padStart(2, "0");
1322
+ const { year: y, month, day: dayNum } = (0, import_core.calendarPartsInTzOrUtc)(d, timezone);
1323
+ const m = `${month}`.padStart(2, "0");
1324
+ const day = `${dayNum}`.padStart(2, "0");
1316
1325
  switch (granularity) {
1317
1326
  case "year":
1318
1327
  return `${y}`;
1319
1328
  case "quarter":
1320
- return `${y}-Q${Math.floor(d.getUTCMonth() / 3) + 1}`;
1329
+ return `${y}-Q${Math.floor((month - 1) / 3) + 1}`;
1321
1330
  case "month":
1322
1331
  return `${y}-${m}`;
1323
1332
  case "week": {
1324
- const monday = new Date(d);
1325
- const dow = (d.getUTCDay() + 6) % 7;
1326
- monday.setUTCDate(d.getUTCDate() - dow);
1333
+ const monday = new Date(Date.UTC(y, month - 1, dayNum));
1334
+ const dow = (monday.getUTCDay() + 6) % 7;
1335
+ monday.setUTCDate(monday.getUTCDate() - dow);
1327
1336
  return monday.toISOString().slice(0, 10);
1328
1337
  }
1329
1338
  case "day":
@@ -1368,6 +1377,7 @@ function evaluateAnalyticsQueryOverRows(query, cube, rows) {
1368
1377
  });
1369
1378
  }
1370
1379
  const dimensions = query.dimensions ?? [];
1380
+ const timezone = query.timezone;
1371
1381
  const granByDim = new Map(timeDims.filter((t) => t.granularity).map((t) => [t.dimension, t.granularity]));
1372
1382
  const keyOf = (r) => {
1373
1383
  const values = {};
@@ -1376,7 +1386,7 @@ function evaluateAnalyticsQueryOverRows(query, cube, rows) {
1376
1386
  const field = String(dim?.sql ?? name);
1377
1387
  const raw = r[field];
1378
1388
  const gran = granByDim.get(name) ?? (dim?.type === "time" && dim.granularities?.length === 1 ? String(dim.granularities[0]) : void 0);
1379
- values[name] = gran ? bucketDate(raw, gran) : raw ?? null;
1389
+ values[name] = gran ? bucketDate(raw, gran, timezone) : raw ?? null;
1380
1390
  }
1381
1391
  return { key: JSON.stringify(values), values };
1382
1392
  };
@@ -1431,7 +1441,7 @@ var AnalyticsService = class {
1431
1441
  constructor(config = {}) {
1432
1442
  /** Compiled datasets by name — feeds the join allowlist (D-C) and queryDataset. */
1433
1443
  this.datasetRegistry = /* @__PURE__ */ new Map();
1434
- this.logger = config.logger || (0, import_core.createLogger)({ level: "info", format: "pretty" });
1444
+ this.logger = config.logger || (0, import_core2.createLogger)({ level: "info", format: "pretty" });
1435
1445
  this.cubeRegistry = new CubeRegistry();
1436
1446
  if (config.cubes) {
1437
1447
  this.cubeRegistry.registerAll(config.cubes);
@@ -1857,7 +1867,7 @@ var AnalyticsServicePlugin = class {
1857
1867
  '[Analytics] No "data" service registered yet at init; will retry per-query. Register ObjectQLPlugin or pass executeAggregate.'
1858
1868
  );
1859
1869
  }
1860
- executeAggregate = async (objectName, { groupBy, aggregations, filter }) => {
1870
+ executeAggregate = async (objectName, { groupBy, aggregations, filter, timezone }) => {
1861
1871
  const engine = tryGetDataEngine();
1862
1872
  if (!engine) {
1863
1873
  throw new Error(
@@ -1871,7 +1881,10 @@ var AnalyticsServicePlugin = class {
1871
1881
  function: a.method,
1872
1882
  field: a.field,
1873
1883
  alias: a.alias
1874
- }))
1884
+ })),
1885
+ // ADR-0053 Phase 2: thread the reference tz so date buckets resolve on
1886
+ // that zone's calendar days (engine buckets in-memory when non-UTC).
1887
+ timezone
1875
1888
  });
1876
1889
  return rows;
1877
1890
  };