@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 +401 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +104 -3
- package/dist/index.d.ts +104 -3
- package/dist/index.js +399 -13
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
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,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
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|