@objectstack/service-analytics 9.0.0 → 9.1.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 +238 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +26 -2
- package/dist/index.d.ts +26 -2
- package/dist/index.js +238 -9
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.cts
CHANGED
|
@@ -228,6 +228,16 @@ interface AnalyticsServiceConfig {
|
|
|
228
228
|
* by the plugin from the `data` engine; omit to keep raw values.
|
|
229
229
|
*/
|
|
230
230
|
labelResolver?: DimensionLabelDeps;
|
|
231
|
+
/**
|
|
232
|
+
* ADR-0037 Phase 3 — draft data preview. Resolve the PENDING `seed` draft
|
|
233
|
+
* rows for an object (returns null when the object has no pending seed).
|
|
234
|
+
* When provided and `queryDataset` is called with `previewDrafts`, the
|
|
235
|
+
* selection is evaluated over these rows in memory instead of the engine —
|
|
236
|
+
* the Live Canvas charts real numbers from the drafted sample data, and
|
|
237
|
+
* because publish materializes the SAME seed, the numbers are continuous
|
|
238
|
+
* across the publish boundary. Reads only; never touches physical tables.
|
|
239
|
+
*/
|
|
240
|
+
draftRowsResolver?: (objectName: string, context?: ExecutionContext) => Promise<Record<string, unknown>[] | null>;
|
|
231
241
|
}
|
|
232
242
|
/**
|
|
233
243
|
* AnalyticsService — Multi-driver analytics orchestrator.
|
|
@@ -259,6 +269,8 @@ declare class AnalyticsService implements IAnalyticsService {
|
|
|
259
269
|
private readonly relationshipResolver?;
|
|
260
270
|
/** Optional dimension display-label resolver (select options / lookup names). */
|
|
261
271
|
private readonly labelResolver?;
|
|
272
|
+
/** ADR-0037 P3: pending-seed row resolver for draft data preview. */
|
|
273
|
+
private readonly draftRowsResolver?;
|
|
262
274
|
readonly cubeRegistry: CubeRegistry;
|
|
263
275
|
private readonly logger;
|
|
264
276
|
constructor(config?: AnalyticsServiceConfig);
|
|
@@ -284,6 +296,14 @@ declare class AnalyticsService implements IAnalyticsService {
|
|
|
284
296
|
private resolveReadScopes;
|
|
285
297
|
/**
|
|
286
298
|
* Execute an analytical query by delegating to the first capable strategy.
|
|
299
|
+
*
|
|
300
|
+
* A strategy can discover only AT EXECUTION TIME that the underlying driver
|
|
301
|
+
* cannot serve it — the canonical case is NativeSQLStrategy on an in-memory
|
|
302
|
+
* driver, whose `execute()` returns null for raw SQL (the auto-bridge throws
|
|
303
|
+
* `RAW_SQL_UNSUPPORTED`). That is a capability miss, not a query error: fall
|
|
304
|
+
* back to the next capable strategy (e.g. ObjectQLStrategy over the
|
|
305
|
+
* aggregate bridge) instead of failing — or worse, fabricating empty rows.
|
|
306
|
+
* Any other error propagates untouched.
|
|
287
307
|
*/
|
|
288
308
|
query(query: AnalyticsQuery, context?: ExecutionContext): Promise<AnalyticsResult>;
|
|
289
309
|
/**
|
|
@@ -298,7 +318,9 @@ declare class AnalyticsService implements IAnalyticsService {
|
|
|
298
318
|
* runs the selection through the `DatasetExecutor` with the request context so
|
|
299
319
|
* tenant/RLS scoping (D-C) is applied. See {@link IAnalyticsService.queryDataset}.
|
|
300
320
|
*/
|
|
301
|
-
queryDataset(dataset: Dataset, selection: DatasetSelection, context?: ExecutionContext
|
|
321
|
+
queryDataset(dataset: Dataset, selection: DatasetSelection, context?: ExecutionContext, options?: {
|
|
322
|
+
previewDrafts?: boolean;
|
|
323
|
+
}): Promise<AnalyticsResult>;
|
|
302
324
|
/**
|
|
303
325
|
* Get cube metadata for discovery.
|
|
304
326
|
*/
|
|
@@ -326,7 +348,9 @@ declare class AnalyticsService implements IAnalyticsService {
|
|
|
326
348
|
/** Build a minimal Cube from the fields referenced by an AnalyticsQuery. */
|
|
327
349
|
private inferCubeFromQuery;
|
|
328
350
|
/**
|
|
329
|
-
* Walk the strategy chain and return the first strategy that can handle the
|
|
351
|
+
* Walk the strategy chain and return the first strategy that can handle the
|
|
352
|
+
* query. `skip` excludes strategies that already proved incapable at
|
|
353
|
+
* execution time (see {@link query}'s RAW_SQL_UNSUPPORTED fallback).
|
|
330
354
|
*/
|
|
331
355
|
private resolveStrategy;
|
|
332
356
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -228,6 +228,16 @@ interface AnalyticsServiceConfig {
|
|
|
228
228
|
* by the plugin from the `data` engine; omit to keep raw values.
|
|
229
229
|
*/
|
|
230
230
|
labelResolver?: DimensionLabelDeps;
|
|
231
|
+
/**
|
|
232
|
+
* ADR-0037 Phase 3 — draft data preview. Resolve the PENDING `seed` draft
|
|
233
|
+
* rows for an object (returns null when the object has no pending seed).
|
|
234
|
+
* When provided and `queryDataset` is called with `previewDrafts`, the
|
|
235
|
+
* selection is evaluated over these rows in memory instead of the engine —
|
|
236
|
+
* the Live Canvas charts real numbers from the drafted sample data, and
|
|
237
|
+
* because publish materializes the SAME seed, the numbers are continuous
|
|
238
|
+
* across the publish boundary. Reads only; never touches physical tables.
|
|
239
|
+
*/
|
|
240
|
+
draftRowsResolver?: (objectName: string, context?: ExecutionContext) => Promise<Record<string, unknown>[] | null>;
|
|
231
241
|
}
|
|
232
242
|
/**
|
|
233
243
|
* AnalyticsService — Multi-driver analytics orchestrator.
|
|
@@ -259,6 +269,8 @@ declare class AnalyticsService implements IAnalyticsService {
|
|
|
259
269
|
private readonly relationshipResolver?;
|
|
260
270
|
/** Optional dimension display-label resolver (select options / lookup names). */
|
|
261
271
|
private readonly labelResolver?;
|
|
272
|
+
/** ADR-0037 P3: pending-seed row resolver for draft data preview. */
|
|
273
|
+
private readonly draftRowsResolver?;
|
|
262
274
|
readonly cubeRegistry: CubeRegistry;
|
|
263
275
|
private readonly logger;
|
|
264
276
|
constructor(config?: AnalyticsServiceConfig);
|
|
@@ -284,6 +296,14 @@ declare class AnalyticsService implements IAnalyticsService {
|
|
|
284
296
|
private resolveReadScopes;
|
|
285
297
|
/**
|
|
286
298
|
* Execute an analytical query by delegating to the first capable strategy.
|
|
299
|
+
*
|
|
300
|
+
* A strategy can discover only AT EXECUTION TIME that the underlying driver
|
|
301
|
+
* cannot serve it — the canonical case is NativeSQLStrategy on an in-memory
|
|
302
|
+
* driver, whose `execute()` returns null for raw SQL (the auto-bridge throws
|
|
303
|
+
* `RAW_SQL_UNSUPPORTED`). That is a capability miss, not a query error: fall
|
|
304
|
+
* back to the next capable strategy (e.g. ObjectQLStrategy over the
|
|
305
|
+
* aggregate bridge) instead of failing — or worse, fabricating empty rows.
|
|
306
|
+
* Any other error propagates untouched.
|
|
287
307
|
*/
|
|
288
308
|
query(query: AnalyticsQuery, context?: ExecutionContext): Promise<AnalyticsResult>;
|
|
289
309
|
/**
|
|
@@ -298,7 +318,9 @@ declare class AnalyticsService implements IAnalyticsService {
|
|
|
298
318
|
* runs the selection through the `DatasetExecutor` with the request context so
|
|
299
319
|
* tenant/RLS scoping (D-C) is applied. See {@link IAnalyticsService.queryDataset}.
|
|
300
320
|
*/
|
|
301
|
-
queryDataset(dataset: Dataset, selection: DatasetSelection, context?: ExecutionContext
|
|
321
|
+
queryDataset(dataset: Dataset, selection: DatasetSelection, context?: ExecutionContext, options?: {
|
|
322
|
+
previewDrafts?: boolean;
|
|
323
|
+
}): Promise<AnalyticsResult>;
|
|
302
324
|
/**
|
|
303
325
|
* Get cube metadata for discovery.
|
|
304
326
|
*/
|
|
@@ -326,7 +348,9 @@ declare class AnalyticsService implements IAnalyticsService {
|
|
|
326
348
|
/** Build a minimal Cube from the fields referenced by an AnalyticsQuery. */
|
|
327
349
|
private inferCubeFromQuery;
|
|
328
350
|
/**
|
|
329
|
-
* Walk the strategy chain and return the first strategy that can handle the
|
|
351
|
+
* Walk the strategy chain and return the first strategy that can handle the
|
|
352
|
+
* query. `skip` excludes strategies that already proved incapable at
|
|
353
|
+
* execution time (see {@link query}'s RAW_SQL_UNSUPPORTED fallback).
|
|
330
354
|
*/
|
|
331
355
|
private resolveStrategy;
|
|
332
356
|
}
|
package/dist/index.js
CHANGED
|
@@ -1193,6 +1193,160 @@ function pickDisplayField(fields) {
|
|
|
1193
1193
|
return void 0;
|
|
1194
1194
|
}
|
|
1195
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
|
+
|
|
1196
1350
|
// src/analytics-service.ts
|
|
1197
1351
|
var DEFAULT_CAPABILITIES = {
|
|
1198
1352
|
nativeSql: false,
|
|
@@ -1211,6 +1365,7 @@ var AnalyticsService = class {
|
|
|
1211
1365
|
this.readScopeProvider = config.getReadScope;
|
|
1212
1366
|
this.relationshipResolver = config.relationshipResolver;
|
|
1213
1367
|
this.labelResolver = config.labelResolver;
|
|
1368
|
+
this.draftRowsResolver = config.draftRowsResolver;
|
|
1214
1369
|
if (config.datasets) {
|
|
1215
1370
|
for (const ds of config.datasets) {
|
|
1216
1371
|
try {
|
|
@@ -1304,6 +1459,14 @@ var AnalyticsService = class {
|
|
|
1304
1459
|
}
|
|
1305
1460
|
/**
|
|
1306
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.
|
|
1307
1470
|
*/
|
|
1308
1471
|
async query(query, context) {
|
|
1309
1472
|
if (!query.cube) {
|
|
@@ -1311,9 +1474,23 @@ var AnalyticsService = class {
|
|
|
1311
1474
|
}
|
|
1312
1475
|
this.ensureCube(query);
|
|
1313
1476
|
const ctx = await this.callCtx(query, context);
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
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
|
+
}
|
|
1317
1494
|
}
|
|
1318
1495
|
/**
|
|
1319
1496
|
* Compile a `dataset` (ADR-0021) and register its Cube + join allowlist so it
|
|
@@ -1332,9 +1509,25 @@ var AnalyticsService = class {
|
|
|
1332
1509
|
* runs the selection through the `DatasetExecutor` with the request context so
|
|
1333
1510
|
* tenant/RLS scoping (D-C) is applied. See {@link IAnalyticsService.queryDataset}.
|
|
1334
1511
|
*/
|
|
1335
|
-
async queryDataset(dataset, selection, context) {
|
|
1512
|
+
async queryDataset(dataset, selection, context, options) {
|
|
1336
1513
|
const compiled = this.registerDataset(dataset);
|
|
1337
1514
|
this.logger.debug(`[Analytics] queryDataset "${dataset.name}" (object=${dataset.object}, include=${(dataset.include ?? []).join(",") || "\u2014"})`);
|
|
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
|
+
}
|
|
1338
1531
|
const result = await new DatasetExecutor(this).execute(compiled, selection, context);
|
|
1339
1532
|
if (this.labelResolver && selection.dimensions?.length) {
|
|
1340
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 }));
|
|
@@ -1479,16 +1672,19 @@ var AnalyticsService = class {
|
|
|
1479
1672
|
};
|
|
1480
1673
|
}
|
|
1481
1674
|
/**
|
|
1482
|
-
* 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).
|
|
1483
1678
|
*/
|
|
1484
|
-
resolveStrategy(query, ctx) {
|
|
1679
|
+
resolveStrategy(query, ctx, skip) {
|
|
1485
1680
|
for (const strategy of this.strategies) {
|
|
1681
|
+
if (skip?.has(strategy)) continue;
|
|
1486
1682
|
if (strategy.canHandle(query, ctx)) {
|
|
1487
1683
|
return strategy;
|
|
1488
1684
|
}
|
|
1489
1685
|
}
|
|
1490
1686
|
throw new Error(
|
|
1491
|
-
`[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.`
|
|
1492
1688
|
);
|
|
1493
1689
|
}
|
|
1494
1690
|
};
|
|
@@ -1610,8 +1806,15 @@ var AnalyticsServicePlugin = class {
|
|
|
1610
1806
|
}
|
|
1611
1807
|
const knexSql = sql.replace(/\$(\d+)/g, "?");
|
|
1612
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
|
+
}
|
|
1613
1816
|
if (Array.isArray(result)) return result;
|
|
1614
|
-
if (
|
|
1817
|
+
if (typeof result === "object" && "rows" in result) {
|
|
1615
1818
|
return result.rows;
|
|
1616
1819
|
}
|
|
1617
1820
|
return [];
|
|
@@ -1680,6 +1883,31 @@ var AnalyticsServicePlugin = class {
|
|
|
1680
1883
|
return map;
|
|
1681
1884
|
}
|
|
1682
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
|
+
};
|
|
1683
1911
|
const config = {
|
|
1684
1912
|
cubes: this.options.cubes,
|
|
1685
1913
|
logger: ctx.logger,
|
|
@@ -1690,7 +1918,8 @@ var AnalyticsServicePlugin = class {
|
|
|
1690
1918
|
getReadScope,
|
|
1691
1919
|
getAllowedRelationships: this.options.getAllowedRelationships,
|
|
1692
1920
|
relationshipResolver,
|
|
1693
|
-
labelResolver
|
|
1921
|
+
labelResolver,
|
|
1922
|
+
draftRowsResolver
|
|
1694
1923
|
};
|
|
1695
1924
|
if (autoBridgedReadScope) {
|
|
1696
1925
|
ctx.logger.info('[Analytics] Auto-bridged getReadScope \u2192 "security" service (getReadFilter)');
|