@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.cjs +164 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +78 -1
- package/dist/index.d.ts +78 -1
- package/dist/index.js +162 -5
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|