@objectstack/service-analytics 8.0.1 → 9.0.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
@@ -83,6 +83,73 @@ interface CompiledDataset {
83
83
  type RelationshipResolver = (baseObject: string, relationshipName: string) => string | undefined;
84
84
  declare function compileDataset(dataset: Dataset, resolver?: RelationshipResolver): CompiledDataset;
85
85
 
86
+ /**
87
+ * Dimension display-label resolution (ADR-0021).
88
+ *
89
+ * Analytics groups by the raw stored value of a dimension field. For two field
90
+ * kinds that value is NOT human-readable:
91
+ *
92
+ * - **select** — grouped by the stored option `value` (e.g. `backlog`), but the
93
+ * user-facing text is the option `label` (e.g. `Backlog`).
94
+ * - **lookup / master_detail** — grouped by the foreign-key `id` (e.g.
95
+ * `8eqtuKI4G9IhUsPS`), but the user-facing text is the related record's
96
+ * display field (its name/title).
97
+ *
98
+ * `resolveDimensionLabels` post-processes the result rows IN PLACE, replacing the
99
+ * raw value at `row[dimension.name]` with its display label when one is found.
100
+ * Unresolved values are left untouched so an orphaned id still renders as itself
101
+ * rather than blanking out. Date / number / plain-string dimensions are no-ops.
102
+ *
103
+ * The resolution LOGIC lives here (and is unit-tested); the low-level capabilities
104
+ * — reading an object's field map and fetching id→label pairs — are injected via
105
+ * {@link DimensionLabelDeps} so this module stays free of any engine dependency.
106
+ */
107
+ /** The minimal field shape this resolver needs. */
108
+ interface FieldMetaLite {
109
+ type?: string;
110
+ /** Lookup / master_detail target object name. */
111
+ reference?: string;
112
+ /** Select options — the value→label source. */
113
+ options?: Array<{
114
+ value: unknown;
115
+ label?: string;
116
+ }>;
117
+ }
118
+ /** Capabilities the resolver needs from the runtime (injected by the plugin). */
119
+ interface DimensionLabelDeps {
120
+ /** Return the field map for an object, or `undefined` if unknown. */
121
+ getObjectFields(objectName: string): Record<string, FieldMetaLite> | undefined;
122
+ /**
123
+ * Fetch a map of `id → display label` for the given ids of a target object.
124
+ * The implementation chooses the target's display field. Returning an empty
125
+ * map (e.g. no display field, no data access) leaves the ids unresolved.
126
+ */
127
+ fetchRecordLabels(targetObject: string, ids: unknown[]): Promise<Map<unknown, string>>;
128
+ }
129
+ /** Date-dimension granularity (mirrors the dataset `dateGranularity` enum). */
130
+ type DateGranularity = 'day' | 'week' | 'month' | 'quarter' | 'year';
131
+ /**
132
+ * Replace raw dimension values with display labels, in place.
133
+ *
134
+ * @param baseObject - the dataset's base object (where the dimension fields live)
135
+ * @param dims - selected dimensions as `{ name, field, type?, dateGranularity? }`
136
+ * (row key = `name`)
137
+ * @param rows - result rows, mutated in place
138
+ * @param deps - injected runtime capabilities
139
+ */
140
+ declare function resolveDimensionLabels(baseObject: string, dims: Array<{
141
+ name: string;
142
+ field: string;
143
+ type?: string;
144
+ dateGranularity?: DateGranularity | string;
145
+ }>, rows: Record<string, unknown>[], deps: DimensionLabelDeps): Promise<void>;
146
+ /**
147
+ * Pick the display field for an object from its field map, by convention:
148
+ * an explicit `name`/`title`/`label` field, else the first text-like field.
149
+ * Returns `undefined` when nothing suitable exists.
150
+ */
151
+ declare function pickDisplayField(fields: Record<string, FieldMetaLite> | undefined): string | undefined;
152
+
86
153
  /**
87
154
  * Configuration for AnalyticsService.
88
155
  */
@@ -153,6 +220,14 @@ interface AnalyticsServiceConfig {
153
220
  relationshipResolver?: RelationshipResolver;
154
221
  /** Pre-defined datasets to compile + register at construction (ADR-0021). */
155
222
  datasets?: Dataset[];
223
+ /**
224
+ * ADR-0021 — resolve raw dimension values to human display labels. When
225
+ * provided, `queryDataset` post-processes result rows so a `select` dimension
226
+ * shows its option label (not the stored value) and a `lookup`/`master_detail`
227
+ * dimension shows the related record's display name (not the FK id). Injected
228
+ * by the plugin from the `data` engine; omit to keep raw values.
229
+ */
230
+ labelResolver?: DimensionLabelDeps;
156
231
  }
157
232
  /**
158
233
  * AnalyticsService — Multi-driver analytics orchestrator.
@@ -182,6 +257,8 @@ declare class AnalyticsService implements IAnalyticsService {
182
257
  private readonly datasetRegistry;
183
258
  /** Optional object-graph resolver used when compiling datasets. */
184
259
  private readonly relationshipResolver?;
260
+ /** Optional dimension display-label resolver (select options / lookup names). */
261
+ private readonly labelResolver?;
185
262
  readonly cubeRegistry: CubeRegistry;
186
263
  private readonly logger;
187
264
  constructor(config?: AnalyticsServiceConfig);
@@ -495,4 +572,4 @@ declare class ObjectQLStrategy implements AnalyticsStrategy {
495
572
  private buildFieldMeta;
496
573
  }
497
574
 
498
- export { AnalyticsService, type AnalyticsServiceConfig, AnalyticsServicePlugin, type AnalyticsServicePluginOptions, type CompareTo, type CompiledDataset, CubeRegistry, DatasetExecutor, type DerivedMeasureSpec, NativeSQLStrategy, ObjectQLStrategy, type RelationshipResolver, combineFilters, compileDataset, compileScopedFilterToSql, evaluateDerivedMeasures, mergeByDimensions, shiftRange };
575
+ export { AnalyticsService, type AnalyticsServiceConfig, AnalyticsServicePlugin, type AnalyticsServicePluginOptions, type CompareTo, type CompiledDataset, CubeRegistry, DatasetExecutor, type DerivedMeasureSpec, type DimensionLabelDeps, type FieldMetaLite, NativeSQLStrategy, ObjectQLStrategy, type RelationshipResolver, combineFilters, compileDataset, compileScopedFilterToSql, evaluateDerivedMeasures, mergeByDimensions, pickDisplayField, resolveDimensionLabels, shiftRange };
package/dist/index.d.ts CHANGED
@@ -83,6 +83,73 @@ interface CompiledDataset {
83
83
  type RelationshipResolver = (baseObject: string, relationshipName: string) => string | undefined;
84
84
  declare function compileDataset(dataset: Dataset, resolver?: RelationshipResolver): CompiledDataset;
85
85
 
86
+ /**
87
+ * Dimension display-label resolution (ADR-0021).
88
+ *
89
+ * Analytics groups by the raw stored value of a dimension field. For two field
90
+ * kinds that value is NOT human-readable:
91
+ *
92
+ * - **select** — grouped by the stored option `value` (e.g. `backlog`), but the
93
+ * user-facing text is the option `label` (e.g. `Backlog`).
94
+ * - **lookup / master_detail** — grouped by the foreign-key `id` (e.g.
95
+ * `8eqtuKI4G9IhUsPS`), but the user-facing text is the related record's
96
+ * display field (its name/title).
97
+ *
98
+ * `resolveDimensionLabels` post-processes the result rows IN PLACE, replacing the
99
+ * raw value at `row[dimension.name]` with its display label when one is found.
100
+ * Unresolved values are left untouched so an orphaned id still renders as itself
101
+ * rather than blanking out. Date / number / plain-string dimensions are no-ops.
102
+ *
103
+ * The resolution LOGIC lives here (and is unit-tested); the low-level capabilities
104
+ * — reading an object's field map and fetching id→label pairs — are injected via
105
+ * {@link DimensionLabelDeps} so this module stays free of any engine dependency.
106
+ */
107
+ /** The minimal field shape this resolver needs. */
108
+ interface FieldMetaLite {
109
+ type?: string;
110
+ /** Lookup / master_detail target object name. */
111
+ reference?: string;
112
+ /** Select options — the value→label source. */
113
+ options?: Array<{
114
+ value: unknown;
115
+ label?: string;
116
+ }>;
117
+ }
118
+ /** Capabilities the resolver needs from the runtime (injected by the plugin). */
119
+ interface DimensionLabelDeps {
120
+ /** Return the field map for an object, or `undefined` if unknown. */
121
+ getObjectFields(objectName: string): Record<string, FieldMetaLite> | undefined;
122
+ /**
123
+ * Fetch a map of `id → display label` for the given ids of a target object.
124
+ * The implementation chooses the target's display field. Returning an empty
125
+ * map (e.g. no display field, no data access) leaves the ids unresolved.
126
+ */
127
+ fetchRecordLabels(targetObject: string, ids: unknown[]): Promise<Map<unknown, string>>;
128
+ }
129
+ /** Date-dimension granularity (mirrors the dataset `dateGranularity` enum). */
130
+ type DateGranularity = 'day' | 'week' | 'month' | 'quarter' | 'year';
131
+ /**
132
+ * Replace raw dimension values with display labels, in place.
133
+ *
134
+ * @param baseObject - the dataset's base object (where the dimension fields live)
135
+ * @param dims - selected dimensions as `{ name, field, type?, dateGranularity? }`
136
+ * (row key = `name`)
137
+ * @param rows - result rows, mutated in place
138
+ * @param deps - injected runtime capabilities
139
+ */
140
+ declare function resolveDimensionLabels(baseObject: string, dims: Array<{
141
+ name: string;
142
+ field: string;
143
+ type?: string;
144
+ dateGranularity?: DateGranularity | string;
145
+ }>, rows: Record<string, unknown>[], deps: DimensionLabelDeps): Promise<void>;
146
+ /**
147
+ * Pick the display field for an object from its field map, by convention:
148
+ * an explicit `name`/`title`/`label` field, else the first text-like field.
149
+ * Returns `undefined` when nothing suitable exists.
150
+ */
151
+ declare function pickDisplayField(fields: Record<string, FieldMetaLite> | undefined): string | undefined;
152
+
86
153
  /**
87
154
  * Configuration for AnalyticsService.
88
155
  */
@@ -153,6 +220,14 @@ interface AnalyticsServiceConfig {
153
220
  relationshipResolver?: RelationshipResolver;
154
221
  /** Pre-defined datasets to compile + register at construction (ADR-0021). */
155
222
  datasets?: Dataset[];
223
+ /**
224
+ * ADR-0021 — resolve raw dimension values to human display labels. When
225
+ * provided, `queryDataset` post-processes result rows so a `select` dimension
226
+ * shows its option label (not the stored value) and a `lookup`/`master_detail`
227
+ * dimension shows the related record's display name (not the FK id). Injected
228
+ * by the plugin from the `data` engine; omit to keep raw values.
229
+ */
230
+ labelResolver?: DimensionLabelDeps;
156
231
  }
157
232
  /**
158
233
  * AnalyticsService — Multi-driver analytics orchestrator.
@@ -182,6 +257,8 @@ declare class AnalyticsService implements IAnalyticsService {
182
257
  private readonly datasetRegistry;
183
258
  /** Optional object-graph resolver used when compiling datasets. */
184
259
  private readonly relationshipResolver?;
260
+ /** Optional dimension display-label resolver (select options / lookup names). */
261
+ private readonly labelResolver?;
185
262
  readonly cubeRegistry: CubeRegistry;
186
263
  private readonly logger;
187
264
  constructor(config?: AnalyticsServiceConfig);
@@ -495,4 +572,4 @@ declare class ObjectQLStrategy implements AnalyticsStrategy {
495
572
  private buildFieldMeta;
496
573
  }
497
574
 
498
- export { AnalyticsService, type AnalyticsServiceConfig, AnalyticsServicePlugin, type AnalyticsServicePluginOptions, type CompareTo, type CompiledDataset, CubeRegistry, DatasetExecutor, type DerivedMeasureSpec, NativeSQLStrategy, ObjectQLStrategy, type RelationshipResolver, combineFilters, compileDataset, compileScopedFilterToSql, evaluateDerivedMeasures, mergeByDimensions, shiftRange };
575
+ export { AnalyticsService, type AnalyticsServiceConfig, AnalyticsServicePlugin, type AnalyticsServicePluginOptions, type CompareTo, type CompiledDataset, CubeRegistry, DatasetExecutor, type DerivedMeasureSpec, type DimensionLabelDeps, type FieldMetaLite, NativeSQLStrategy, ObjectQLStrategy, type RelationshipResolver, combineFilters, compileDataset, compileScopedFilterToSql, evaluateDerivedMeasures, mergeByDimensions, pickDisplayField, resolveDimensionLabels, shiftRange };
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,88 @@ 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
+
1088
1196
  // src/analytics-service.ts
1089
1197
  var DEFAULT_CAPABILITIES = {
1090
1198
  nativeSql: false,
@@ -1102,6 +1210,7 @@ var AnalyticsService = class {
1102
1210
  }
1103
1211
  this.readScopeProvider = config.getReadScope;
1104
1212
  this.relationshipResolver = config.relationshipResolver;
1213
+ this.labelResolver = config.labelResolver;
1105
1214
  if (config.datasets) {
1106
1215
  for (const ds of config.datasets) {
1107
1216
  try {
@@ -1226,7 +1335,27 @@ var AnalyticsService = class {
1226
1335
  async queryDataset(dataset, selection, context) {
1227
1336
  const compiled = this.registerDataset(dataset);
1228
1337
  this.logger.debug(`[Analytics] queryDataset "${dataset.name}" (object=${dataset.object}, include=${(dataset.include ?? []).join(",") || "\u2014"})`);
1229
- return new DatasetExecutor(this).execute(compiled, selection, context);
1338
+ const result = await new DatasetExecutor(this).execute(compiled, selection, context);
1339
+ if (this.labelResolver && selection.dimensions?.length) {
1340
+ 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 }));
1341
+ if (dims.length) {
1342
+ try {
1343
+ await resolveDimensionLabels(dataset.object, dims, result.rows, this.labelResolver);
1344
+ } catch (e) {
1345
+ this.logger?.warn?.(`[Analytics] dimension label resolution failed for "${dataset.name}": ${String(e?.message ?? e)}`);
1346
+ }
1347
+ }
1348
+ }
1349
+ if (result.fields?.length && dataset.measures?.length) {
1350
+ const measureByName = new Map(dataset.measures.map((m) => [m.name, m]));
1351
+ for (const f of result.fields) {
1352
+ const m = measureByName.get(f.name) ?? measureByName.get(f.name.replace(/__compare$/, ""));
1353
+ if (!m) continue;
1354
+ if (f.label == null && typeof m.label === "string") f.label = m.label;
1355
+ if (f.format == null && m.format) f.format = m.format;
1356
+ }
1357
+ }
1358
+ return result;
1230
1359
  }
1231
1360
  /**
1232
1361
  * Get cube metadata for discovery.
@@ -1526,6 +1655,31 @@ var AnalyticsServicePlugin = class {
1526
1655
  }
1527
1656
  return engine ? void 0 : relationshipName;
1528
1657
  };
1658
+ const dataEngine = () => {
1659
+ try {
1660
+ const svc = ctx.getService("data");
1661
+ return svc && typeof svc.getObject === "function" ? svc : void 0;
1662
+ } catch {
1663
+ return void 0;
1664
+ }
1665
+ };
1666
+ const labelResolver = {
1667
+ getObjectFields: (objectName) => dataEngine()?.getObject?.(objectName)?.fields,
1668
+ fetchRecordLabels: async (targetObject, ids) => {
1669
+ const map = /* @__PURE__ */ new Map();
1670
+ const displayField = pickDisplayField(dataEngine()?.getObject?.(targetObject)?.fields);
1671
+ if (!displayField || !executeAggregate || ids.length === 0) return map;
1672
+ const rows = await executeAggregate(targetObject, {
1673
+ groupBy: ["id", displayField],
1674
+ aggregations: [{ field: "id", method: "count", alias: "_c" }],
1675
+ filter: { id: { $in: ids } }
1676
+ });
1677
+ for (const r of rows) {
1678
+ if (r.id != null && r[displayField] != null) map.set(r.id, String(r[displayField]));
1679
+ }
1680
+ return map;
1681
+ }
1682
+ };
1529
1683
  const config = {
1530
1684
  cubes: this.options.cubes,
1531
1685
  logger: ctx.logger,
@@ -1535,7 +1689,8 @@ var AnalyticsServicePlugin = class {
1535
1689
  fallbackService,
1536
1690
  getReadScope,
1537
1691
  getAllowedRelationships: this.options.getAllowedRelationships,
1538
- relationshipResolver
1692
+ relationshipResolver,
1693
+ labelResolver
1539
1694
  };
1540
1695
  if (autoBridgedReadScope) {
1541
1696
  ctx.logger.info('[Analytics] Auto-bridged getReadScope \u2192 "security" service (getReadFilter)');
@@ -1586,6 +1741,8 @@ export {
1586
1741
  compileScopedFilterToSql,
1587
1742
  evaluateDerivedMeasures,
1588
1743
  mergeByDimensions,
1744
+ pickDisplayField,
1745
+ resolveDimensionLabels,
1589
1746
  shiftRange
1590
1747
  };
1591
1748
  //# sourceMappingURL=index.js.map