@objectstack/service-analytics 8.0.1 → 9.0.1

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
@@ -31,6 +31,8 @@ __export(index_exports, {
31
31
  compileScopedFilterToSql: () => compileScopedFilterToSql,
32
32
  evaluateDerivedMeasures: () => evaluateDerivedMeasures,
33
33
  mergeByDimensions: () => mergeByDimensions,
34
+ pickDisplayField: () => pickDisplayField,
35
+ resolveDimensionLabels: () => resolveDimensionLabels,
34
36
  shiftRange: () => shiftRange
35
37
  });
36
38
  module.exports = __toCommonJS(index_exports);
@@ -633,12 +635,22 @@ var ObjectQLStrategy = class {
633
635
  async execute(query, ctx) {
634
636
  const cube = ctx.getCube(query.cube);
635
637
  const objectName = this.extractObjectName(cube);
638
+ const granByDim = /* @__PURE__ */ new Map();
639
+ for (const td of query.timeDimensions ?? []) {
640
+ if (td.granularity) granByDim.set(td.dimension, td.granularity);
641
+ }
636
642
  const groupBy = [];
637
643
  if (query.dimensions && query.dimensions.length > 0) {
638
644
  for (const dim of query.dimensions) {
639
- groupBy.push(this.resolveFieldName(cube, dim, "dimension"));
645
+ const field = this.resolveFieldName(cube, dim, "dimension");
646
+ const gran = granByDim.get(dim);
647
+ groupBy.push(gran ? { field, dateGranularity: gran } : field);
648
+ granByDim.delete(dim);
640
649
  }
641
650
  }
651
+ for (const [dim, gran] of granByDim) {
652
+ groupBy.push({ field: this.resolveFieldName(cube, dim, "dimension"), dateGranularity: gran });
653
+ }
642
654
  const aggregations = [];
643
655
  if (query.measures && query.measures.length > 0) {
644
656
  for (const measure of query.measures) {
@@ -651,10 +663,16 @@ var ObjectQLStrategy = class {
651
663
  if (normalizedFilters.length > 0) {
652
664
  for (const f of normalizedFilters) {
653
665
  const fieldName = this.resolveFieldName(cube, f.member, "any");
654
- filter[fieldName] = this.convertFilter(f.operator, f.values);
666
+ const converted = this.convertFilter(f.operator, f.values);
667
+ const existing = filter[fieldName];
668
+ const mergeable = (v) => !!v && typeof v === "object" && !Array.isArray(v);
669
+ filter[fieldName] = mergeable(existing) && mergeable(converted) ? { ...existing, ...converted } : converted;
655
670
  }
656
671
  }
657
672
  const rows = await ctx.executeAggregate(objectName, {
673
+ // Structured groupBy items ({field, dateGranularity}) pass through the
674
+ // executeAggregate bridge to engine.aggregate, which buckets them. The
675
+ // contract types groupBy as string[]; the cast carries the richer shape.
658
676
  groupBy: groupBy.length > 0 ? groupBy : void 0,
659
677
  aggregations: aggregations.length > 0 ? aggregations : void 0,
660
678
  filter: Object.keys(filter).length > 0 ? filter : void 0
@@ -1067,7 +1085,17 @@ var DatasetExecutor = class {
1067
1085
  timezone: opts.selection.timezone ?? "UTC"
1068
1086
  };
1069
1087
  if (opts.where) q.where = opts.where;
1070
- if (opts.selection.timeDimensions) q.timeDimensions = opts.selection.timeDimensions;
1088
+ const selTimeDims = opts.selection.timeDimensions ?? [];
1089
+ const selDims = new Set(selTimeDims.map((t) => t.dimension));
1090
+ const explicitTimeDims = [];
1091
+ for (const name of opts.dimensions) {
1092
+ const cd = compiled.cube.dimensions[name];
1093
+ if (cd?.type === "time" && cd.granularities?.length === 1 && !selDims.has(name)) {
1094
+ explicitTimeDims.push({ dimension: name, granularity: String(cd.granularities[0]) });
1095
+ }
1096
+ }
1097
+ const mergedTimeDims = [...selTimeDims, ...explicitTimeDims];
1098
+ if (mergedTimeDims.length > 0) q.timeDimensions = mergedTimeDims;
1071
1099
  if (opts.selection.order) q.order = opts.selection.order;
1072
1100
  if (opts.selection.limit != null) q.limit = opts.selection.limit;
1073
1101
  if (opts.selection.offset != null) q.offset = opts.selection.offset;
@@ -1122,6 +1150,242 @@ function mergeByDimensions(base, extra, dimensions, valueColumns) {
1122
1150
  return base;
1123
1151
  }
1124
1152
 
1153
+ // src/dimension-labels.ts
1154
+ var LOOKUP_TYPES = /* @__PURE__ */ new Set(["lookup", "master_detail"]);
1155
+ var pad = (n) => String(n).padStart(2, "0");
1156
+ function formatDateBucket(value, granularity) {
1157
+ if (value == null || value instanceof Date === false) {
1158
+ if (typeof value !== "number" && typeof value !== "string") return value;
1159
+ }
1160
+ let d;
1161
+ if (value instanceof Date) d = value;
1162
+ else if (typeof value === "number") d = new Date(value);
1163
+ else {
1164
+ const s = String(value).trim();
1165
+ d = /^\d+$/.test(s) ? new Date(Number(s) < 1e12 ? Number(s) * 1e3 : Number(s)) : new Date(s);
1166
+ }
1167
+ if (Number.isNaN(d.getTime())) return value;
1168
+ const y = d.getUTCFullYear();
1169
+ const m = d.getUTCMonth();
1170
+ switch (granularity) {
1171
+ case "year":
1172
+ return String(y);
1173
+ case "quarter":
1174
+ return `${y}-Q${Math.floor(m / 3) + 1}`;
1175
+ case "month":
1176
+ return `${y}-${pad(m + 1)}`;
1177
+ case "week":
1178
+ case "day":
1179
+ default:
1180
+ return `${y}-${pad(m + 1)}-${pad(d.getUTCDate())}`;
1181
+ }
1182
+ }
1183
+ async function resolveDimensionLabels(baseObject, dims, rows, deps) {
1184
+ if (!rows.length || !dims.length) return;
1185
+ const fields = deps.getObjectFields(baseObject);
1186
+ if (!fields) return;
1187
+ for (const dim of dims) {
1188
+ const meta = fields[dim.field];
1189
+ if (dim.type === "date" || meta && meta.type === "date") {
1190
+ for (const row of rows) {
1191
+ const formatted = formatDateBucket(row[dim.name], dim.dateGranularity);
1192
+ if (formatted != null) row[dim.name] = formatted;
1193
+ }
1194
+ continue;
1195
+ }
1196
+ if (!meta) continue;
1197
+ if (Array.isArray(meta.options) && meta.options.length > 0) {
1198
+ const labelByValue = /* @__PURE__ */ new Map();
1199
+ for (const opt of meta.options) {
1200
+ if (opt && opt.label != null) labelByValue.set(opt.value, String(opt.label));
1201
+ }
1202
+ if (labelByValue.size === 0) continue;
1203
+ for (const row of rows) {
1204
+ const raw = row[dim.name];
1205
+ const label = labelByValue.get(raw);
1206
+ if (label != null) row[dim.name] = label;
1207
+ }
1208
+ continue;
1209
+ }
1210
+ if (meta.type && LOOKUP_TYPES.has(meta.type) && meta.reference) {
1211
+ const ids = Array.from(
1212
+ new Set(rows.map((r) => r[dim.name]).filter((v) => v != null))
1213
+ );
1214
+ if (ids.length === 0) continue;
1215
+ const labelById = await deps.fetchRecordLabels(meta.reference, ids);
1216
+ if (!labelById || labelById.size === 0) continue;
1217
+ for (const row of rows) {
1218
+ const label = labelById.get(row[dim.name]);
1219
+ if (label != null) row[dim.name] = label;
1220
+ }
1221
+ }
1222
+ }
1223
+ }
1224
+ function pickDisplayField(fields) {
1225
+ if (!fields) return void 0;
1226
+ for (const preferred of ["name", "title", "label"]) {
1227
+ if (fields[preferred]) return preferred;
1228
+ }
1229
+ for (const [name, meta] of Object.entries(fields)) {
1230
+ if (meta.type === "text" || meta.type === "string") return name;
1231
+ }
1232
+ return void 0;
1233
+ }
1234
+
1235
+ // src/preview-evaluator.ts
1236
+ function compare(a, b) {
1237
+ if (typeof a === "number" && typeof b === "number") return a - b;
1238
+ return String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0;
1239
+ }
1240
+ function matchOp(value, op, expected) {
1241
+ switch (op) {
1242
+ case "$eq":
1243
+ return value === expected || String(value) === String(expected);
1244
+ case "$ne":
1245
+ return !(value === expected || String(value) === String(expected));
1246
+ case "$gt":
1247
+ return value != null && compare(value, expected) > 0;
1248
+ case "$gte":
1249
+ return value != null && compare(value, expected) >= 0;
1250
+ case "$lt":
1251
+ return value != null && compare(value, expected) < 0;
1252
+ case "$lte":
1253
+ return value != null && compare(value, expected) <= 0;
1254
+ case "$in":
1255
+ return Array.isArray(expected) && expected.some((e) => value === e || String(value) === String(e));
1256
+ case "$nin":
1257
+ return Array.isArray(expected) && !expected.some((e) => value === e || String(value) === String(e));
1258
+ case "$contains":
1259
+ return String(value ?? "").toLowerCase().includes(String(expected ?? "").toLowerCase());
1260
+ default:
1261
+ return true;
1262
+ }
1263
+ }
1264
+ function matchesWhere(row, where) {
1265
+ if (!where) return true;
1266
+ for (const [key, cond] of Object.entries(where)) {
1267
+ if (key === "$and") {
1268
+ if (!cond.every((c) => matchesWhere(row, c))) return false;
1269
+ } else if (key === "$or") {
1270
+ if (!cond.some((c) => matchesWhere(row, c))) return false;
1271
+ } else if (key === "$not") {
1272
+ if (matchesWhere(row, cond)) return false;
1273
+ } else if (cond !== null && typeof cond === "object" && !Array.isArray(cond)) {
1274
+ for (const [op, expected] of Object.entries(cond)) {
1275
+ if (!matchOp(row[key], op, expected)) return false;
1276
+ }
1277
+ } else if (!(row[key] === cond || String(row[key]) === String(cond))) {
1278
+ return false;
1279
+ }
1280
+ }
1281
+ return true;
1282
+ }
1283
+ function bucketDate(value, granularity) {
1284
+ const d = new Date(String(value));
1285
+ if (Number.isNaN(d.getTime())) return null;
1286
+ const y = d.getUTCFullYear();
1287
+ const m = `${d.getUTCMonth() + 1}`.padStart(2, "0");
1288
+ const day = `${d.getUTCDate()}`.padStart(2, "0");
1289
+ switch (granularity) {
1290
+ case "year":
1291
+ return `${y}`;
1292
+ case "quarter":
1293
+ return `${y}-Q${Math.floor(d.getUTCMonth() / 3) + 1}`;
1294
+ case "month":
1295
+ return `${y}-${m}`;
1296
+ case "week": {
1297
+ const monday = new Date(d);
1298
+ const dow = (d.getUTCDay() + 6) % 7;
1299
+ monday.setUTCDate(d.getUTCDate() - dow);
1300
+ return monday.toISOString().slice(0, 10);
1301
+ }
1302
+ case "day":
1303
+ default:
1304
+ return `${y}-${m}-${day}`;
1305
+ }
1306
+ }
1307
+ function aggregate(rows, metricType, field) {
1308
+ if (metricType === "count" || field === "*") {
1309
+ if (metricType === "countDistinct") {
1310
+ return new Set(rows.map((r) => r[field]).filter((v) => v != null)).size;
1311
+ }
1312
+ return rows.length;
1313
+ }
1314
+ const nums = rows.map((r) => Number(r[field])).filter((n) => Number.isFinite(n));
1315
+ switch (metricType) {
1316
+ case "countDistinct":
1317
+ return new Set(rows.map((r) => r[field]).filter((v) => v != null)).size;
1318
+ case "sum":
1319
+ return nums.reduce((a, b) => a + b, 0);
1320
+ case "avg":
1321
+ return nums.length ? nums.reduce((a, b) => a + b, 0) / nums.length : 0;
1322
+ case "min":
1323
+ return nums.length ? Math.min(...nums) : 0;
1324
+ case "max":
1325
+ return nums.length ? Math.max(...nums) : 0;
1326
+ default:
1327
+ return nums.length ? nums.reduce((a, b) => a + b, 0) : rows.length;
1328
+ }
1329
+ }
1330
+ function evaluateAnalyticsQueryOverRows(query, cube, rows) {
1331
+ let filtered = rows.filter((r) => matchesWhere(r, query.where));
1332
+ const timeDims = query.timeDimensions ?? [];
1333
+ for (const td of timeDims) {
1334
+ const dim = cube.dimensions?.[td.dimension];
1335
+ const field = String(dim?.sql ?? td.dimension);
1336
+ if (!td.dateRange) continue;
1337
+ const [start, end] = Array.isArray(td.dateRange) ? td.dateRange : [td.dateRange, td.dateRange];
1338
+ filtered = filtered.filter((r) => {
1339
+ const v = String(r[field] ?? "");
1340
+ return v >= String(start) && v <= `${end}~`;
1341
+ });
1342
+ }
1343
+ const dimensions = query.dimensions ?? [];
1344
+ const granByDim = new Map(timeDims.filter((t) => t.granularity).map((t) => [t.dimension, t.granularity]));
1345
+ const keyOf = (r) => {
1346
+ const values = {};
1347
+ for (const name of dimensions) {
1348
+ const dim = cube.dimensions?.[name];
1349
+ const field = String(dim?.sql ?? name);
1350
+ const raw = r[field];
1351
+ const gran = granByDim.get(name) ?? (dim?.type === "time" && dim.granularities?.length === 1 ? String(dim.granularities[0]) : void 0);
1352
+ values[name] = gran ? bucketDate(raw, gran) : raw ?? null;
1353
+ }
1354
+ return { key: JSON.stringify(values), values };
1355
+ };
1356
+ const groups = /* @__PURE__ */ new Map();
1357
+ for (const r of filtered) {
1358
+ const { key, values } = keyOf(r);
1359
+ const g = groups.get(key) ?? { values, rows: [] };
1360
+ g.rows.push(r);
1361
+ groups.set(key, g);
1362
+ }
1363
+ if (dimensions.length === 0 && groups.size === 0) {
1364
+ groups.set("{}", { values: {}, rows: [] });
1365
+ }
1366
+ const out = [];
1367
+ for (const g of groups.values()) {
1368
+ const row = { ...g.values };
1369
+ for (const m of query.measures) {
1370
+ const metric = cube.measures?.[m];
1371
+ row[m] = aggregate(g.rows, String(metric?.type ?? "count"), String(metric?.sql ?? "*"));
1372
+ }
1373
+ out.push(row);
1374
+ }
1375
+ for (const [col, dir] of Object.entries(query.order ?? {}).reverse()) {
1376
+ out.sort((a, b) => (dir === "desc" ? -1 : 1) * compare(a[col], b[col]));
1377
+ }
1378
+ const offset = query.offset ?? 0;
1379
+ const limited = out.slice(offset, query.limit != null ? offset + query.limit : void 0);
1380
+ return {
1381
+ rows: limited,
1382
+ fields: [
1383
+ ...dimensions.map((d) => ({ name: d, type: "string" })),
1384
+ ...query.measures.map((m) => ({ name: m, type: "number" }))
1385
+ ]
1386
+ };
1387
+ }
1388
+
1125
1389
  // src/analytics-service.ts
1126
1390
  var DEFAULT_CAPABILITIES = {
1127
1391
  nativeSql: false,
@@ -1139,6 +1403,8 @@ var AnalyticsService = class {
1139
1403
  }
1140
1404
  this.readScopeProvider = config.getReadScope;
1141
1405
  this.relationshipResolver = config.relationshipResolver;
1406
+ this.labelResolver = config.labelResolver;
1407
+ this.draftRowsResolver = config.draftRowsResolver;
1142
1408
  if (config.datasets) {
1143
1409
  for (const ds of config.datasets) {
1144
1410
  try {
@@ -1232,6 +1498,14 @@ var AnalyticsService = class {
1232
1498
  }
1233
1499
  /**
1234
1500
  * Execute an analytical query by delegating to the first capable strategy.
1501
+ *
1502
+ * A strategy can discover only AT EXECUTION TIME that the underlying driver
1503
+ * cannot serve it — the canonical case is NativeSQLStrategy on an in-memory
1504
+ * driver, whose `execute()` returns null for raw SQL (the auto-bridge throws
1505
+ * `RAW_SQL_UNSUPPORTED`). That is a capability miss, not a query error: fall
1506
+ * back to the next capable strategy (e.g. ObjectQLStrategy over the
1507
+ * aggregate bridge) instead of failing — or worse, fabricating empty rows.
1508
+ * Any other error propagates untouched.
1235
1509
  */
1236
1510
  async query(query, context) {
1237
1511
  if (!query.cube) {
@@ -1239,9 +1513,23 @@ var AnalyticsService = class {
1239
1513
  }
1240
1514
  this.ensureCube(query);
1241
1515
  const ctx = await this.callCtx(query, context);
1242
- const strategy = this.resolveStrategy(query, ctx);
1243
- this.logger.debug(`[Analytics] Query on cube "${query.cube}" \u2192 ${strategy.name}`);
1244
- return strategy.execute(query, ctx);
1516
+ let skip;
1517
+ for (; ; ) {
1518
+ const strategy = this.resolveStrategy(query, ctx, skip);
1519
+ this.logger.debug(`[Analytics] Query on cube "${query.cube}" \u2192 ${strategy.name}`);
1520
+ try {
1521
+ return await strategy.execute(query, ctx);
1522
+ } catch (e) {
1523
+ if (e?.code === "RAW_SQL_UNSUPPORTED") {
1524
+ this.logger.warn(
1525
+ `[Analytics] ${strategy.name} cannot run on this driver (raw SQL unsupported) \u2014 falling back to the next strategy.`
1526
+ );
1527
+ (skip ?? (skip = /* @__PURE__ */ new Set())).add(strategy);
1528
+ continue;
1529
+ }
1530
+ throw e;
1531
+ }
1532
+ }
1245
1533
  }
1246
1534
  /**
1247
1535
  * Compile a `dataset` (ADR-0021) and register its Cube + join allowlist so it
@@ -1260,10 +1548,46 @@ var AnalyticsService = class {
1260
1548
  * runs the selection through the `DatasetExecutor` with the request context so
1261
1549
  * tenant/RLS scoping (D-C) is applied. See {@link IAnalyticsService.queryDataset}.
1262
1550
  */
1263
- async queryDataset(dataset, selection, context) {
1551
+ async queryDataset(dataset, selection, context, options) {
1264
1552
  const compiled = this.registerDataset(dataset);
1265
1553
  this.logger.debug(`[Analytics] queryDataset "${dataset.name}" (object=${dataset.object}, include=${(dataset.include ?? []).join(",") || "\u2014"})`);
1266
- return new DatasetExecutor(this).execute(compiled, selection, context);
1554
+ if (options?.previewDrafts && this.draftRowsResolver) {
1555
+ let seedRows = null;
1556
+ try {
1557
+ seedRows = await this.draftRowsResolver(dataset.object, context);
1558
+ } catch (e) {
1559
+ this.logger.warn(`[Analytics] draft preview resolver failed for "${dataset.object}" \u2014 falling back to live data: ${String(e?.message ?? e)}`);
1560
+ }
1561
+ if (seedRows) {
1562
+ this.logger.debug(`[Analytics] queryDataset "${dataset.name}" \u2192 preview over ${seedRows.length} drafted seed row(s)`);
1563
+ const previewService = {
1564
+ query: async (q) => evaluateAnalyticsQueryOverRows(q, compiled.cube, seedRows)
1565
+ };
1566
+ const previewResult = await new DatasetExecutor(previewService).execute(compiled, selection, context);
1567
+ return previewResult;
1568
+ }
1569
+ }
1570
+ const result = await new DatasetExecutor(this).execute(compiled, selection, context);
1571
+ if (this.labelResolver && selection.dimensions?.length) {
1572
+ const dims = selection.dimensions.map((name) => dataset.dimensions?.find((d) => d.name === name)).filter((d) => !!d?.field).map((d) => ({ name: d.name, field: d.field, type: d.type, dateGranularity: d.dateGranularity }));
1573
+ if (dims.length) {
1574
+ try {
1575
+ await resolveDimensionLabels(dataset.object, dims, result.rows, this.labelResolver);
1576
+ } catch (e) {
1577
+ this.logger?.warn?.(`[Analytics] dimension label resolution failed for "${dataset.name}": ${String(e?.message ?? e)}`);
1578
+ }
1579
+ }
1580
+ }
1581
+ if (result.fields?.length && dataset.measures?.length) {
1582
+ const measureByName = new Map(dataset.measures.map((m) => [m.name, m]));
1583
+ for (const f of result.fields) {
1584
+ const m = measureByName.get(f.name) ?? measureByName.get(f.name.replace(/__compare$/, ""));
1585
+ if (!m) continue;
1586
+ if (f.label == null && typeof m.label === "string") f.label = m.label;
1587
+ if (f.format == null && m.format) f.format = m.format;
1588
+ }
1589
+ }
1590
+ return result;
1267
1591
  }
1268
1592
  /**
1269
1593
  * Get cube metadata for discovery.
@@ -1387,16 +1711,19 @@ var AnalyticsService = class {
1387
1711
  };
1388
1712
  }
1389
1713
  /**
1390
- * Walk the strategy chain and return the first strategy that can handle the query.
1714
+ * Walk the strategy chain and return the first strategy that can handle the
1715
+ * query. `skip` excludes strategies that already proved incapable at
1716
+ * execution time (see {@link query}'s RAW_SQL_UNSUPPORTED fallback).
1391
1717
  */
1392
- resolveStrategy(query, ctx) {
1718
+ resolveStrategy(query, ctx, skip) {
1393
1719
  for (const strategy of this.strategies) {
1720
+ if (skip?.has(strategy)) continue;
1394
1721
  if (strategy.canHandle(query, ctx)) {
1395
1722
  return strategy;
1396
1723
  }
1397
1724
  }
1398
1725
  throw new Error(
1399
- `[Analytics] No strategy can handle query for cube "${query.cube}". Checked: ${this.strategies.map((s) => s.name).join(", ")}. Ensure a compatible driver is configured or a fallback service is registered.`
1726
+ `[Analytics] No strategy can handle query for cube "${query.cube}". Checked: ${this.strategies.map((s) => s.name).join(", ")}${skip?.size ? ` (skipped at runtime: ${[...skip].map((s) => s.name).join(", ")})` : ""}. Ensure a compatible driver is configured or a fallback service is registered.`
1400
1727
  );
1401
1728
  }
1402
1729
  };
@@ -1518,8 +1845,15 @@ var AnalyticsServicePlugin = class {
1518
1845
  }
1519
1846
  const knexSql = sql.replace(/\$(\d+)/g, "?");
1520
1847
  const result = await engine.execute(knexSql, { args: params });
1848
+ if (result === null || result === void 0) {
1849
+ const err = new Error(
1850
+ `[Analytics] The "data" engine's driver returned null for raw SQL \u2014 this driver does not support SQL execution. The query will fall back to an aggregate-based strategy when one is available.`
1851
+ );
1852
+ err.code = "RAW_SQL_UNSUPPORTED";
1853
+ throw err;
1854
+ }
1521
1855
  if (Array.isArray(result)) return result;
1522
- if (result && typeof result === "object" && "rows" in result) {
1856
+ if (typeof result === "object" && "rows" in result) {
1523
1857
  return result.rows;
1524
1858
  }
1525
1859
  return [];
@@ -1563,6 +1897,56 @@ var AnalyticsServicePlugin = class {
1563
1897
  }
1564
1898
  return engine ? void 0 : relationshipName;
1565
1899
  };
1900
+ const dataEngine = () => {
1901
+ try {
1902
+ const svc = ctx.getService("data");
1903
+ return svc && typeof svc.getObject === "function" ? svc : void 0;
1904
+ } catch {
1905
+ return void 0;
1906
+ }
1907
+ };
1908
+ const labelResolver = {
1909
+ getObjectFields: (objectName) => dataEngine()?.getObject?.(objectName)?.fields,
1910
+ fetchRecordLabels: async (targetObject, ids) => {
1911
+ const map = /* @__PURE__ */ new Map();
1912
+ const displayField = pickDisplayField(dataEngine()?.getObject?.(targetObject)?.fields);
1913
+ if (!displayField || !executeAggregate || ids.length === 0) return map;
1914
+ const rows = await executeAggregate(targetObject, {
1915
+ groupBy: ["id", displayField],
1916
+ aggregations: [{ field: "id", method: "count", alias: "_c" }],
1917
+ filter: { id: { $in: ids } }
1918
+ });
1919
+ for (const r of rows) {
1920
+ if (r.id != null && r[displayField] != null) map.set(r.id, String(r[displayField]));
1921
+ }
1922
+ return map;
1923
+ }
1924
+ };
1925
+ const draftRowsResolver = async (objectName) => {
1926
+ let protocol;
1927
+ try {
1928
+ protocol = ctx.getService("protocol");
1929
+ } catch {
1930
+ return null;
1931
+ }
1932
+ if (!protocol?.getMetaItems || !protocol.getMetaItem) return null;
1933
+ const res = await protocol.getMetaItems({ type: "seed", previewDrafts: true }).catch(() => null);
1934
+ const list = Array.isArray(res) ? res : res && typeof res === "object" && Array.isArray(res.items) ? res.items : [];
1935
+ const rows = [];
1936
+ let pending = false;
1937
+ for (const entry of list) {
1938
+ const body = entry?.item ?? entry;
1939
+ if (!body?.name || body.object !== objectName) continue;
1940
+ const draft = await protocol.getMetaItem({ type: "seed", name: body.name, state: "draft" }).catch(() => null);
1941
+ const draftBody = draft?.item;
1942
+ if (!draftBody) continue;
1943
+ pending = true;
1944
+ for (const r of Array.isArray(draftBody.records) ? draftBody.records : []) {
1945
+ if (r && typeof r === "object") rows.push(r);
1946
+ }
1947
+ }
1948
+ return pending ? rows : null;
1949
+ };
1566
1950
  const config = {
1567
1951
  cubes: this.options.cubes,
1568
1952
  logger: ctx.logger,
@@ -1572,7 +1956,9 @@ var AnalyticsServicePlugin = class {
1572
1956
  fallbackService,
1573
1957
  getReadScope,
1574
1958
  getAllowedRelationships: this.options.getAllowedRelationships,
1575
- relationshipResolver
1959
+ relationshipResolver,
1960
+ labelResolver,
1961
+ draftRowsResolver
1576
1962
  };
1577
1963
  if (autoBridgedReadScope) {
1578
1964
  ctx.logger.info('[Analytics] Auto-bridged getReadScope \u2192 "security" service (getReadFilter)');
@@ -1624,6 +2010,8 @@ var AnalyticsServicePlugin = class {
1624
2010
  compileScopedFilterToSql,
1625
2011
  evaluateDerivedMeasures,
1626
2012
  mergeByDimensions,
2013
+ pickDisplayField,
2014
+ resolveDimensionLabels,
1627
2015
  shiftRange
1628
2016
  });
1629
2017
  //# sourceMappingURL=index.cjs.map