@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.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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,88 @@ 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
|
+
|
|
1125
1235
|
// src/analytics-service.ts
|
|
1126
1236
|
var DEFAULT_CAPABILITIES = {
|
|
1127
1237
|
nativeSql: false,
|
|
@@ -1139,6 +1249,7 @@ var AnalyticsService = class {
|
|
|
1139
1249
|
}
|
|
1140
1250
|
this.readScopeProvider = config.getReadScope;
|
|
1141
1251
|
this.relationshipResolver = config.relationshipResolver;
|
|
1252
|
+
this.labelResolver = config.labelResolver;
|
|
1142
1253
|
if (config.datasets) {
|
|
1143
1254
|
for (const ds of config.datasets) {
|
|
1144
1255
|
try {
|
|
@@ -1263,7 +1374,27 @@ var AnalyticsService = class {
|
|
|
1263
1374
|
async queryDataset(dataset, selection, context) {
|
|
1264
1375
|
const compiled = this.registerDataset(dataset);
|
|
1265
1376
|
this.logger.debug(`[Analytics] queryDataset "${dataset.name}" (object=${dataset.object}, include=${(dataset.include ?? []).join(",") || "\u2014"})`);
|
|
1266
|
-
|
|
1377
|
+
const result = await new DatasetExecutor(this).execute(compiled, selection, context);
|
|
1378
|
+
if (this.labelResolver && selection.dimensions?.length) {
|
|
1379
|
+
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 }));
|
|
1380
|
+
if (dims.length) {
|
|
1381
|
+
try {
|
|
1382
|
+
await resolveDimensionLabels(dataset.object, dims, result.rows, this.labelResolver);
|
|
1383
|
+
} catch (e) {
|
|
1384
|
+
this.logger?.warn?.(`[Analytics] dimension label resolution failed for "${dataset.name}": ${String(e?.message ?? e)}`);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
if (result.fields?.length && dataset.measures?.length) {
|
|
1389
|
+
const measureByName = new Map(dataset.measures.map((m) => [m.name, m]));
|
|
1390
|
+
for (const f of result.fields) {
|
|
1391
|
+
const m = measureByName.get(f.name) ?? measureByName.get(f.name.replace(/__compare$/, ""));
|
|
1392
|
+
if (!m) continue;
|
|
1393
|
+
if (f.label == null && typeof m.label === "string") f.label = m.label;
|
|
1394
|
+
if (f.format == null && m.format) f.format = m.format;
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
return result;
|
|
1267
1398
|
}
|
|
1268
1399
|
/**
|
|
1269
1400
|
* Get cube metadata for discovery.
|
|
@@ -1563,6 +1694,31 @@ var AnalyticsServicePlugin = class {
|
|
|
1563
1694
|
}
|
|
1564
1695
|
return engine ? void 0 : relationshipName;
|
|
1565
1696
|
};
|
|
1697
|
+
const dataEngine = () => {
|
|
1698
|
+
try {
|
|
1699
|
+
const svc = ctx.getService("data");
|
|
1700
|
+
return svc && typeof svc.getObject === "function" ? svc : void 0;
|
|
1701
|
+
} catch {
|
|
1702
|
+
return void 0;
|
|
1703
|
+
}
|
|
1704
|
+
};
|
|
1705
|
+
const labelResolver = {
|
|
1706
|
+
getObjectFields: (objectName) => dataEngine()?.getObject?.(objectName)?.fields,
|
|
1707
|
+
fetchRecordLabels: async (targetObject, ids) => {
|
|
1708
|
+
const map = /* @__PURE__ */ new Map();
|
|
1709
|
+
const displayField = pickDisplayField(dataEngine()?.getObject?.(targetObject)?.fields);
|
|
1710
|
+
if (!displayField || !executeAggregate || ids.length === 0) return map;
|
|
1711
|
+
const rows = await executeAggregate(targetObject, {
|
|
1712
|
+
groupBy: ["id", displayField],
|
|
1713
|
+
aggregations: [{ field: "id", method: "count", alias: "_c" }],
|
|
1714
|
+
filter: { id: { $in: ids } }
|
|
1715
|
+
});
|
|
1716
|
+
for (const r of rows) {
|
|
1717
|
+
if (r.id != null && r[displayField] != null) map.set(r.id, String(r[displayField]));
|
|
1718
|
+
}
|
|
1719
|
+
return map;
|
|
1720
|
+
}
|
|
1721
|
+
};
|
|
1566
1722
|
const config = {
|
|
1567
1723
|
cubes: this.options.cubes,
|
|
1568
1724
|
logger: ctx.logger,
|
|
@@ -1572,7 +1728,8 @@ var AnalyticsServicePlugin = class {
|
|
|
1572
1728
|
fallbackService,
|
|
1573
1729
|
getReadScope,
|
|
1574
1730
|
getAllowedRelationships: this.options.getAllowedRelationships,
|
|
1575
|
-
relationshipResolver
|
|
1731
|
+
relationshipResolver,
|
|
1732
|
+
labelResolver
|
|
1576
1733
|
};
|
|
1577
1734
|
if (autoBridgedReadScope) {
|
|
1578
1735
|
ctx.logger.info('[Analytics] Auto-bridged getReadScope \u2192 "security" service (getReadFilter)');
|
|
@@ -1624,6 +1781,8 @@ var AnalyticsServicePlugin = class {
|
|
|
1624
1781
|
compileScopedFilterToSql,
|
|
1625
1782
|
evaluateDerivedMeasures,
|
|
1626
1783
|
mergeByDimensions,
|
|
1784
|
+
pickDisplayField,
|
|
1785
|
+
resolveDimensionLabels,
|
|
1627
1786
|
shiftRange
|
|
1628
1787
|
});
|
|
1629
1788
|
//# sourceMappingURL=index.cjs.map
|