@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.js CHANGED
@@ -596,12 +596,22 @@ var ObjectQLStrategy = class {
596
596
  async execute(query, ctx) {
597
597
  const cube = ctx.getCube(query.cube);
598
598
  const objectName = this.extractObjectName(cube);
599
+ const granByDim = /* @__PURE__ */ new Map();
600
+ for (const td of query.timeDimensions ?? []) {
601
+ if (td.granularity) granByDim.set(td.dimension, td.granularity);
602
+ }
599
603
  const groupBy = [];
600
604
  if (query.dimensions && query.dimensions.length > 0) {
601
605
  for (const dim of query.dimensions) {
602
- groupBy.push(this.resolveFieldName(cube, dim, "dimension"));
606
+ const field = this.resolveFieldName(cube, dim, "dimension");
607
+ const gran = granByDim.get(dim);
608
+ groupBy.push(gran ? { field, dateGranularity: gran } : field);
609
+ granByDim.delete(dim);
603
610
  }
604
611
  }
612
+ for (const [dim, gran] of granByDim) {
613
+ groupBy.push({ field: this.resolveFieldName(cube, dim, "dimension"), dateGranularity: gran });
614
+ }
605
615
  const aggregations = [];
606
616
  if (query.measures && query.measures.length > 0) {
607
617
  for (const measure of query.measures) {
@@ -614,10 +624,16 @@ var ObjectQLStrategy = class {
614
624
  if (normalizedFilters.length > 0) {
615
625
  for (const f of normalizedFilters) {
616
626
  const fieldName = this.resolveFieldName(cube, f.member, "any");
617
- filter[fieldName] = this.convertFilter(f.operator, f.values);
627
+ const converted = this.convertFilter(f.operator, f.values);
628
+ const existing = filter[fieldName];
629
+ const mergeable = (v) => !!v && typeof v === "object" && !Array.isArray(v);
630
+ filter[fieldName] = mergeable(existing) && mergeable(converted) ? { ...existing, ...converted } : converted;
618
631
  }
619
632
  }
620
633
  const rows = await ctx.executeAggregate(objectName, {
634
+ // Structured groupBy items ({field, dateGranularity}) pass through the
635
+ // executeAggregate bridge to engine.aggregate, which buckets them. The
636
+ // contract types groupBy as string[]; the cast carries the richer shape.
621
637
  groupBy: groupBy.length > 0 ? groupBy : void 0,
622
638
  aggregations: aggregations.length > 0 ? aggregations : void 0,
623
639
  filter: Object.keys(filter).length > 0 ? filter : void 0
@@ -1030,7 +1046,17 @@ var DatasetExecutor = class {
1030
1046
  timezone: opts.selection.timezone ?? "UTC"
1031
1047
  };
1032
1048
  if (opts.where) q.where = opts.where;
1033
- if (opts.selection.timeDimensions) q.timeDimensions = opts.selection.timeDimensions;
1049
+ const selTimeDims = opts.selection.timeDimensions ?? [];
1050
+ const selDims = new Set(selTimeDims.map((t) => t.dimension));
1051
+ const explicitTimeDims = [];
1052
+ for (const name of opts.dimensions) {
1053
+ const cd = compiled.cube.dimensions[name];
1054
+ if (cd?.type === "time" && cd.granularities?.length === 1 && !selDims.has(name)) {
1055
+ explicitTimeDims.push({ dimension: name, granularity: String(cd.granularities[0]) });
1056
+ }
1057
+ }
1058
+ const mergedTimeDims = [...selTimeDims, ...explicitTimeDims];
1059
+ if (mergedTimeDims.length > 0) q.timeDimensions = mergedTimeDims;
1034
1060
  if (opts.selection.order) q.order = opts.selection.order;
1035
1061
  if (opts.selection.limit != null) q.limit = opts.selection.limit;
1036
1062
  if (opts.selection.offset != null) q.offset = opts.selection.offset;
@@ -1085,6 +1111,242 @@ function mergeByDimensions(base, extra, dimensions, valueColumns) {
1085
1111
  return base;
1086
1112
  }
1087
1113
 
1114
+ // src/dimension-labels.ts
1115
+ var LOOKUP_TYPES = /* @__PURE__ */ new Set(["lookup", "master_detail"]);
1116
+ var pad = (n) => String(n).padStart(2, "0");
1117
+ function formatDateBucket(value, granularity) {
1118
+ if (value == null || value instanceof Date === false) {
1119
+ if (typeof value !== "number" && typeof value !== "string") return value;
1120
+ }
1121
+ let d;
1122
+ if (value instanceof Date) d = value;
1123
+ else if (typeof value === "number") d = new Date(value);
1124
+ else {
1125
+ const s = String(value).trim();
1126
+ d = /^\d+$/.test(s) ? new Date(Number(s) < 1e12 ? Number(s) * 1e3 : Number(s)) : new Date(s);
1127
+ }
1128
+ if (Number.isNaN(d.getTime())) return value;
1129
+ const y = d.getUTCFullYear();
1130
+ const m = d.getUTCMonth();
1131
+ switch (granularity) {
1132
+ case "year":
1133
+ return String(y);
1134
+ case "quarter":
1135
+ return `${y}-Q${Math.floor(m / 3) + 1}`;
1136
+ case "month":
1137
+ return `${y}-${pad(m + 1)}`;
1138
+ case "week":
1139
+ case "day":
1140
+ default:
1141
+ return `${y}-${pad(m + 1)}-${pad(d.getUTCDate())}`;
1142
+ }
1143
+ }
1144
+ async function resolveDimensionLabels(baseObject, dims, rows, deps) {
1145
+ if (!rows.length || !dims.length) return;
1146
+ const fields = deps.getObjectFields(baseObject);
1147
+ if (!fields) return;
1148
+ for (const dim of dims) {
1149
+ const meta = fields[dim.field];
1150
+ if (dim.type === "date" || meta && meta.type === "date") {
1151
+ for (const row of rows) {
1152
+ const formatted = formatDateBucket(row[dim.name], dim.dateGranularity);
1153
+ if (formatted != null) row[dim.name] = formatted;
1154
+ }
1155
+ continue;
1156
+ }
1157
+ if (!meta) continue;
1158
+ if (Array.isArray(meta.options) && meta.options.length > 0) {
1159
+ const labelByValue = /* @__PURE__ */ new Map();
1160
+ for (const opt of meta.options) {
1161
+ if (opt && opt.label != null) labelByValue.set(opt.value, String(opt.label));
1162
+ }
1163
+ if (labelByValue.size === 0) continue;
1164
+ for (const row of rows) {
1165
+ const raw = row[dim.name];
1166
+ const label = labelByValue.get(raw);
1167
+ if (label != null) row[dim.name] = label;
1168
+ }
1169
+ continue;
1170
+ }
1171
+ if (meta.type && LOOKUP_TYPES.has(meta.type) && meta.reference) {
1172
+ const ids = Array.from(
1173
+ new Set(rows.map((r) => r[dim.name]).filter((v) => v != null))
1174
+ );
1175
+ if (ids.length === 0) continue;
1176
+ const labelById = await deps.fetchRecordLabels(meta.reference, ids);
1177
+ if (!labelById || labelById.size === 0) continue;
1178
+ for (const row of rows) {
1179
+ const label = labelById.get(row[dim.name]);
1180
+ if (label != null) row[dim.name] = label;
1181
+ }
1182
+ }
1183
+ }
1184
+ }
1185
+ function pickDisplayField(fields) {
1186
+ if (!fields) return void 0;
1187
+ for (const preferred of ["name", "title", "label"]) {
1188
+ if (fields[preferred]) return preferred;
1189
+ }
1190
+ for (const [name, meta] of Object.entries(fields)) {
1191
+ if (meta.type === "text" || meta.type === "string") return name;
1192
+ }
1193
+ return void 0;
1194
+ }
1195
+
1196
+ // src/preview-evaluator.ts
1197
+ function compare(a, b) {
1198
+ if (typeof a === "number" && typeof b === "number") return a - b;
1199
+ return String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0;
1200
+ }
1201
+ function matchOp(value, op, expected) {
1202
+ switch (op) {
1203
+ case "$eq":
1204
+ return value === expected || String(value) === String(expected);
1205
+ case "$ne":
1206
+ return !(value === expected || String(value) === String(expected));
1207
+ case "$gt":
1208
+ return value != null && compare(value, expected) > 0;
1209
+ case "$gte":
1210
+ return value != null && compare(value, expected) >= 0;
1211
+ case "$lt":
1212
+ return value != null && compare(value, expected) < 0;
1213
+ case "$lte":
1214
+ return value != null && compare(value, expected) <= 0;
1215
+ case "$in":
1216
+ return Array.isArray(expected) && expected.some((e) => value === e || String(value) === String(e));
1217
+ case "$nin":
1218
+ return Array.isArray(expected) && !expected.some((e) => value === e || String(value) === String(e));
1219
+ case "$contains":
1220
+ return String(value ?? "").toLowerCase().includes(String(expected ?? "").toLowerCase());
1221
+ default:
1222
+ return true;
1223
+ }
1224
+ }
1225
+ function matchesWhere(row, where) {
1226
+ if (!where) return true;
1227
+ for (const [key, cond] of Object.entries(where)) {
1228
+ if (key === "$and") {
1229
+ if (!cond.every((c) => matchesWhere(row, c))) return false;
1230
+ } else if (key === "$or") {
1231
+ if (!cond.some((c) => matchesWhere(row, c))) return false;
1232
+ } else if (key === "$not") {
1233
+ if (matchesWhere(row, cond)) return false;
1234
+ } else if (cond !== null && typeof cond === "object" && !Array.isArray(cond)) {
1235
+ for (const [op, expected] of Object.entries(cond)) {
1236
+ if (!matchOp(row[key], op, expected)) return false;
1237
+ }
1238
+ } else if (!(row[key] === cond || String(row[key]) === String(cond))) {
1239
+ return false;
1240
+ }
1241
+ }
1242
+ return true;
1243
+ }
1244
+ function bucketDate(value, granularity) {
1245
+ const d = new Date(String(value));
1246
+ if (Number.isNaN(d.getTime())) return null;
1247
+ const y = d.getUTCFullYear();
1248
+ const m = `${d.getUTCMonth() + 1}`.padStart(2, "0");
1249
+ const day = `${d.getUTCDate()}`.padStart(2, "0");
1250
+ switch (granularity) {
1251
+ case "year":
1252
+ return `${y}`;
1253
+ case "quarter":
1254
+ return `${y}-Q${Math.floor(d.getUTCMonth() / 3) + 1}`;
1255
+ case "month":
1256
+ return `${y}-${m}`;
1257
+ case "week": {
1258
+ const monday = new Date(d);
1259
+ const dow = (d.getUTCDay() + 6) % 7;
1260
+ monday.setUTCDate(d.getUTCDate() - dow);
1261
+ return monday.toISOString().slice(0, 10);
1262
+ }
1263
+ case "day":
1264
+ default:
1265
+ return `${y}-${m}-${day}`;
1266
+ }
1267
+ }
1268
+ function aggregate(rows, metricType, field) {
1269
+ if (metricType === "count" || field === "*") {
1270
+ if (metricType === "countDistinct") {
1271
+ return new Set(rows.map((r) => r[field]).filter((v) => v != null)).size;
1272
+ }
1273
+ return rows.length;
1274
+ }
1275
+ const nums = rows.map((r) => Number(r[field])).filter((n) => Number.isFinite(n));
1276
+ switch (metricType) {
1277
+ case "countDistinct":
1278
+ return new Set(rows.map((r) => r[field]).filter((v) => v != null)).size;
1279
+ case "sum":
1280
+ return nums.reduce((a, b) => a + b, 0);
1281
+ case "avg":
1282
+ return nums.length ? nums.reduce((a, b) => a + b, 0) / nums.length : 0;
1283
+ case "min":
1284
+ return nums.length ? Math.min(...nums) : 0;
1285
+ case "max":
1286
+ return nums.length ? Math.max(...nums) : 0;
1287
+ default:
1288
+ return nums.length ? nums.reduce((a, b) => a + b, 0) : rows.length;
1289
+ }
1290
+ }
1291
+ function evaluateAnalyticsQueryOverRows(query, cube, rows) {
1292
+ let filtered = rows.filter((r) => matchesWhere(r, query.where));
1293
+ const timeDims = query.timeDimensions ?? [];
1294
+ for (const td of timeDims) {
1295
+ const dim = cube.dimensions?.[td.dimension];
1296
+ const field = String(dim?.sql ?? td.dimension);
1297
+ if (!td.dateRange) continue;
1298
+ const [start, end] = Array.isArray(td.dateRange) ? td.dateRange : [td.dateRange, td.dateRange];
1299
+ filtered = filtered.filter((r) => {
1300
+ const v = String(r[field] ?? "");
1301
+ return v >= String(start) && v <= `${end}~`;
1302
+ });
1303
+ }
1304
+ const dimensions = query.dimensions ?? [];
1305
+ const granByDim = new Map(timeDims.filter((t) => t.granularity).map((t) => [t.dimension, t.granularity]));
1306
+ const keyOf = (r) => {
1307
+ const values = {};
1308
+ for (const name of dimensions) {
1309
+ const dim = cube.dimensions?.[name];
1310
+ const field = String(dim?.sql ?? name);
1311
+ const raw = r[field];
1312
+ const gran = granByDim.get(name) ?? (dim?.type === "time" && dim.granularities?.length === 1 ? String(dim.granularities[0]) : void 0);
1313
+ values[name] = gran ? bucketDate(raw, gran) : raw ?? null;
1314
+ }
1315
+ return { key: JSON.stringify(values), values };
1316
+ };
1317
+ const groups = /* @__PURE__ */ new Map();
1318
+ for (const r of filtered) {
1319
+ const { key, values } = keyOf(r);
1320
+ const g = groups.get(key) ?? { values, rows: [] };
1321
+ g.rows.push(r);
1322
+ groups.set(key, g);
1323
+ }
1324
+ if (dimensions.length === 0 && groups.size === 0) {
1325
+ groups.set("{}", { values: {}, rows: [] });
1326
+ }
1327
+ const out = [];
1328
+ for (const g of groups.values()) {
1329
+ const row = { ...g.values };
1330
+ for (const m of query.measures) {
1331
+ const metric = cube.measures?.[m];
1332
+ row[m] = aggregate(g.rows, String(metric?.type ?? "count"), String(metric?.sql ?? "*"));
1333
+ }
1334
+ out.push(row);
1335
+ }
1336
+ for (const [col, dir] of Object.entries(query.order ?? {}).reverse()) {
1337
+ out.sort((a, b) => (dir === "desc" ? -1 : 1) * compare(a[col], b[col]));
1338
+ }
1339
+ const offset = query.offset ?? 0;
1340
+ const limited = out.slice(offset, query.limit != null ? offset + query.limit : void 0);
1341
+ return {
1342
+ rows: limited,
1343
+ fields: [
1344
+ ...dimensions.map((d) => ({ name: d, type: "string" })),
1345
+ ...query.measures.map((m) => ({ name: m, type: "number" }))
1346
+ ]
1347
+ };
1348
+ }
1349
+
1088
1350
  // src/analytics-service.ts
1089
1351
  var DEFAULT_CAPABILITIES = {
1090
1352
  nativeSql: false,
@@ -1102,6 +1364,8 @@ var AnalyticsService = class {
1102
1364
  }
1103
1365
  this.readScopeProvider = config.getReadScope;
1104
1366
  this.relationshipResolver = config.relationshipResolver;
1367
+ this.labelResolver = config.labelResolver;
1368
+ this.draftRowsResolver = config.draftRowsResolver;
1105
1369
  if (config.datasets) {
1106
1370
  for (const ds of config.datasets) {
1107
1371
  try {
@@ -1195,6 +1459,14 @@ var AnalyticsService = class {
1195
1459
  }
1196
1460
  /**
1197
1461
  * Execute an analytical query by delegating to the first capable strategy.
1462
+ *
1463
+ * A strategy can discover only AT EXECUTION TIME that the underlying driver
1464
+ * cannot serve it — the canonical case is NativeSQLStrategy on an in-memory
1465
+ * driver, whose `execute()` returns null for raw SQL (the auto-bridge throws
1466
+ * `RAW_SQL_UNSUPPORTED`). That is a capability miss, not a query error: fall
1467
+ * back to the next capable strategy (e.g. ObjectQLStrategy over the
1468
+ * aggregate bridge) instead of failing — or worse, fabricating empty rows.
1469
+ * Any other error propagates untouched.
1198
1470
  */
1199
1471
  async query(query, context) {
1200
1472
  if (!query.cube) {
@@ -1202,9 +1474,23 @@ var AnalyticsService = class {
1202
1474
  }
1203
1475
  this.ensureCube(query);
1204
1476
  const ctx = await this.callCtx(query, context);
1205
- const strategy = this.resolveStrategy(query, ctx);
1206
- this.logger.debug(`[Analytics] Query on cube "${query.cube}" \u2192 ${strategy.name}`);
1207
- return strategy.execute(query, ctx);
1477
+ let skip;
1478
+ for (; ; ) {
1479
+ const strategy = this.resolveStrategy(query, ctx, skip);
1480
+ this.logger.debug(`[Analytics] Query on cube "${query.cube}" \u2192 ${strategy.name}`);
1481
+ try {
1482
+ return await strategy.execute(query, ctx);
1483
+ } catch (e) {
1484
+ if (e?.code === "RAW_SQL_UNSUPPORTED") {
1485
+ this.logger.warn(
1486
+ `[Analytics] ${strategy.name} cannot run on this driver (raw SQL unsupported) \u2014 falling back to the next strategy.`
1487
+ );
1488
+ (skip ?? (skip = /* @__PURE__ */ new Set())).add(strategy);
1489
+ continue;
1490
+ }
1491
+ throw e;
1492
+ }
1493
+ }
1208
1494
  }
1209
1495
  /**
1210
1496
  * Compile a `dataset` (ADR-0021) and register its Cube + join allowlist so it
@@ -1223,10 +1509,46 @@ var AnalyticsService = class {
1223
1509
  * runs the selection through the `DatasetExecutor` with the request context so
1224
1510
  * tenant/RLS scoping (D-C) is applied. See {@link IAnalyticsService.queryDataset}.
1225
1511
  */
1226
- async queryDataset(dataset, selection, context) {
1512
+ async queryDataset(dataset, selection, context, options) {
1227
1513
  const compiled = this.registerDataset(dataset);
1228
1514
  this.logger.debug(`[Analytics] queryDataset "${dataset.name}" (object=${dataset.object}, include=${(dataset.include ?? []).join(",") || "\u2014"})`);
1229
- return new DatasetExecutor(this).execute(compiled, selection, context);
1515
+ if (options?.previewDrafts && this.draftRowsResolver) {
1516
+ let seedRows = null;
1517
+ try {
1518
+ seedRows = await this.draftRowsResolver(dataset.object, context);
1519
+ } catch (e) {
1520
+ this.logger.warn(`[Analytics] draft preview resolver failed for "${dataset.object}" \u2014 falling back to live data: ${String(e?.message ?? e)}`);
1521
+ }
1522
+ if (seedRows) {
1523
+ this.logger.debug(`[Analytics] queryDataset "${dataset.name}" \u2192 preview over ${seedRows.length} drafted seed row(s)`);
1524
+ const previewService = {
1525
+ query: async (q) => evaluateAnalyticsQueryOverRows(q, compiled.cube, seedRows)
1526
+ };
1527
+ const previewResult = await new DatasetExecutor(previewService).execute(compiled, selection, context);
1528
+ return previewResult;
1529
+ }
1530
+ }
1531
+ const result = await new DatasetExecutor(this).execute(compiled, selection, context);
1532
+ if (this.labelResolver && selection.dimensions?.length) {
1533
+ 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 }));
1534
+ if (dims.length) {
1535
+ try {
1536
+ await resolveDimensionLabels(dataset.object, dims, result.rows, this.labelResolver);
1537
+ } catch (e) {
1538
+ this.logger?.warn?.(`[Analytics] dimension label resolution failed for "${dataset.name}": ${String(e?.message ?? e)}`);
1539
+ }
1540
+ }
1541
+ }
1542
+ if (result.fields?.length && dataset.measures?.length) {
1543
+ const measureByName = new Map(dataset.measures.map((m) => [m.name, m]));
1544
+ for (const f of result.fields) {
1545
+ const m = measureByName.get(f.name) ?? measureByName.get(f.name.replace(/__compare$/, ""));
1546
+ if (!m) continue;
1547
+ if (f.label == null && typeof m.label === "string") f.label = m.label;
1548
+ if (f.format == null && m.format) f.format = m.format;
1549
+ }
1550
+ }
1551
+ return result;
1230
1552
  }
1231
1553
  /**
1232
1554
  * Get cube metadata for discovery.
@@ -1350,16 +1672,19 @@ var AnalyticsService = class {
1350
1672
  };
1351
1673
  }
1352
1674
  /**
1353
- * Walk the strategy chain and return the first strategy that can handle the query.
1675
+ * Walk the strategy chain and return the first strategy that can handle the
1676
+ * query. `skip` excludes strategies that already proved incapable at
1677
+ * execution time (see {@link query}'s RAW_SQL_UNSUPPORTED fallback).
1354
1678
  */
1355
- resolveStrategy(query, ctx) {
1679
+ resolveStrategy(query, ctx, skip) {
1356
1680
  for (const strategy of this.strategies) {
1681
+ if (skip?.has(strategy)) continue;
1357
1682
  if (strategy.canHandle(query, ctx)) {
1358
1683
  return strategy;
1359
1684
  }
1360
1685
  }
1361
1686
  throw new Error(
1362
- `[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.`
1687
+ `[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.`
1363
1688
  );
1364
1689
  }
1365
1690
  };
@@ -1481,8 +1806,15 @@ var AnalyticsServicePlugin = class {
1481
1806
  }
1482
1807
  const knexSql = sql.replace(/\$(\d+)/g, "?");
1483
1808
  const result = await engine.execute(knexSql, { args: params });
1809
+ if (result === null || result === void 0) {
1810
+ const err = new Error(
1811
+ `[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.`
1812
+ );
1813
+ err.code = "RAW_SQL_UNSUPPORTED";
1814
+ throw err;
1815
+ }
1484
1816
  if (Array.isArray(result)) return result;
1485
- if (result && typeof result === "object" && "rows" in result) {
1817
+ if (typeof result === "object" && "rows" in result) {
1486
1818
  return result.rows;
1487
1819
  }
1488
1820
  return [];
@@ -1526,6 +1858,56 @@ var AnalyticsServicePlugin = class {
1526
1858
  }
1527
1859
  return engine ? void 0 : relationshipName;
1528
1860
  };
1861
+ const dataEngine = () => {
1862
+ try {
1863
+ const svc = ctx.getService("data");
1864
+ return svc && typeof svc.getObject === "function" ? svc : void 0;
1865
+ } catch {
1866
+ return void 0;
1867
+ }
1868
+ };
1869
+ const labelResolver = {
1870
+ getObjectFields: (objectName) => dataEngine()?.getObject?.(objectName)?.fields,
1871
+ fetchRecordLabels: async (targetObject, ids) => {
1872
+ const map = /* @__PURE__ */ new Map();
1873
+ const displayField = pickDisplayField(dataEngine()?.getObject?.(targetObject)?.fields);
1874
+ if (!displayField || !executeAggregate || ids.length === 0) return map;
1875
+ const rows = await executeAggregate(targetObject, {
1876
+ groupBy: ["id", displayField],
1877
+ aggregations: [{ field: "id", method: "count", alias: "_c" }],
1878
+ filter: { id: { $in: ids } }
1879
+ });
1880
+ for (const r of rows) {
1881
+ if (r.id != null && r[displayField] != null) map.set(r.id, String(r[displayField]));
1882
+ }
1883
+ return map;
1884
+ }
1885
+ };
1886
+ const draftRowsResolver = async (objectName) => {
1887
+ let protocol;
1888
+ try {
1889
+ protocol = ctx.getService("protocol");
1890
+ } catch {
1891
+ return null;
1892
+ }
1893
+ if (!protocol?.getMetaItems || !protocol.getMetaItem) return null;
1894
+ const res = await protocol.getMetaItems({ type: "seed", previewDrafts: true }).catch(() => null);
1895
+ const list = Array.isArray(res) ? res : res && typeof res === "object" && Array.isArray(res.items) ? res.items : [];
1896
+ const rows = [];
1897
+ let pending = false;
1898
+ for (const entry of list) {
1899
+ const body = entry?.item ?? entry;
1900
+ if (!body?.name || body.object !== objectName) continue;
1901
+ const draft = await protocol.getMetaItem({ type: "seed", name: body.name, state: "draft" }).catch(() => null);
1902
+ const draftBody = draft?.item;
1903
+ if (!draftBody) continue;
1904
+ pending = true;
1905
+ for (const r of Array.isArray(draftBody.records) ? draftBody.records : []) {
1906
+ if (r && typeof r === "object") rows.push(r);
1907
+ }
1908
+ }
1909
+ return pending ? rows : null;
1910
+ };
1529
1911
  const config = {
1530
1912
  cubes: this.options.cubes,
1531
1913
  logger: ctx.logger,
@@ -1535,7 +1917,9 @@ var AnalyticsServicePlugin = class {
1535
1917
  fallbackService,
1536
1918
  getReadScope,
1537
1919
  getAllowedRelationships: this.options.getAllowedRelationships,
1538
- relationshipResolver
1920
+ relationshipResolver,
1921
+ labelResolver,
1922
+ draftRowsResolver
1539
1923
  };
1540
1924
  if (autoBridgedReadScope) {
1541
1925
  ctx.logger.info('[Analytics] Auto-bridged getReadScope \u2192 "security" service (getReadFilter)');
@@ -1586,6 +1970,8 @@ export {
1586
1970
  compileScopedFilterToSql,
1587
1971
  evaluateDerivedMeasures,
1588
1972
  mergeByDimensions,
1973
+ pickDisplayField,
1974
+ resolveDimensionLabels,
1589
1975
  shiftRange
1590
1976
  };
1591
1977
  //# sourceMappingURL=index.js.map