@happyvertical/smrt-reports 0.42.6 → 0.42.7
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/AGENTS.md +33 -0
- package/README.md +120 -0
- package/dist/index.d.ts +483 -4
- package/dist/index.js +908 -5
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/refresh.js +2 -2
- package/dist/scheduler.js +1 -1
- package/dist/smrt-knowledge.json +7 -5
- package/package.json +6 -5
package/dist/index.js
CHANGED
|
@@ -1,13 +1,916 @@
|
|
|
1
|
-
import { bucketExpr, buildAggregate } from "./aggregate.js";
|
|
2
1
|
import { aggregate, avg, count, day, getRuntimeReportOptions, groupBy, hour, max, min, minute, month, quarter, report, sum, week, year } from "./decorators.js";
|
|
3
2
|
import { buildReportDefinition, compileReportDefinition, compileReportSpec, getReportGroupingColumns } from "./compiler.js";
|
|
4
3
|
import { REPORT_LOCKS_TABLE, REPORT_REFRESH_TASKS_TABLE, REPORT_RUNS_TABLE, REPORT_RUNTIME_TABLES, REPORT_SCHEDULER_TABLES, REPORT_SCHEDULES_TABLE, REPORT_WATERMARKS_TABLE, SmrtReportLock, SmrtReportLockCollection, SmrtReportRun, SmrtReportRunCollection, SmrtReportSchedule, SmrtReportScheduleCollection, SmrtReportWatermark, SmrtReportWatermarkCollection, assertReportTablesReady, scopeKeyForTenant } from "./state.js";
|
|
5
4
|
import { refreshReport, reportRowIdentity } from "./refresh.js";
|
|
6
5
|
import { ReportScheduleRunner, SmrtReportRefreshTask, enqueueReportRefresh, ensureReportRefreshSchedules, registerReportRefreshInterceptor } from "./scheduler.js";
|
|
7
|
-
import {
|
|
8
|
-
import { ObjectRegistry, SmrtCollection, SmrtObject } from "@happyvertical/smrt-core";
|
|
6
|
+
import { bucketExpr, buildAggregate } from "./aggregate.js";
|
|
7
|
+
import { DataQueryValidationError, ObjectRegistry, SmrtCollection, SmrtObject, createDataQueryFingerprint, normalizeDataQueryRequest, normalizeDataQueryResult } from "@happyvertical/smrt-core";
|
|
9
8
|
import { toSnakeCase } from "@happyvertical/smrt-core/utils";
|
|
10
|
-
import { getTenantId } from "@happyvertical/smrt-tenancy";
|
|
9
|
+
import { getTenantId, withSystemContext } from "@happyvertical/smrt-tenancy";
|
|
10
|
+
import { tableExists, validateColumnName } from "@happyvertical/sql";
|
|
11
|
+
//#region src/lifecycle.ts
|
|
12
|
+
function registeredReport(reportCtor) {
|
|
13
|
+
return ObjectRegistry.getClassByConstructor(reportCtor) ?? ObjectRegistry.getClass(reportCtor.name);
|
|
14
|
+
}
|
|
15
|
+
function canonicalClassName(reportCtor) {
|
|
16
|
+
const registered = registeredReport(reportCtor);
|
|
17
|
+
return registered?.qualifiedName ?? registered?.name ?? reportCtor.name;
|
|
18
|
+
}
|
|
19
|
+
function reportTableName(reportCtor) {
|
|
20
|
+
const reportClassName = canonicalClassName(reportCtor);
|
|
21
|
+
const tableName = ObjectRegistry.getTableName(reportClassName);
|
|
22
|
+
if (!tableName) throw new Error(`No report table registered for ${reportCtor.name}`);
|
|
23
|
+
return validateColumnName(tableName);
|
|
24
|
+
}
|
|
25
|
+
async function reportColumnName(reportClassName, fieldName) {
|
|
26
|
+
const direct = (await ObjectRegistry.getAllFields(reportClassName)).get(fieldName);
|
|
27
|
+
return validateColumnName(direct?.columnName ?? direct?._meta?.columnName ?? fieldName.replace(/([A-Z])/g, "_$1").toLowerCase());
|
|
28
|
+
}
|
|
29
|
+
function toIso(value) {
|
|
30
|
+
if (!value) return void 0;
|
|
31
|
+
const date = value instanceof Date ? value : new Date(String(value));
|
|
32
|
+
return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
|
|
33
|
+
}
|
|
34
|
+
function latestIso(first, second) {
|
|
35
|
+
if (!first) return second;
|
|
36
|
+
if (!second) return first;
|
|
37
|
+
return new Date(first).getTime() >= new Date(second).getTime() ? first : second;
|
|
38
|
+
}
|
|
39
|
+
function lifecycleTenantId(reportCtor) {
|
|
40
|
+
return registeredReport(reportCtor)?.tenantScopedConfig ? getTenantId() ?? null : null;
|
|
41
|
+
}
|
|
42
|
+
function numberValue(value) {
|
|
43
|
+
const numeric = Number(value ?? 0);
|
|
44
|
+
return Number.isFinite(numeric) ? numeric : 0;
|
|
45
|
+
}
|
|
46
|
+
function rowWhere(reportClassName, scopeKey, tenantId) {
|
|
47
|
+
if (tenantId) return {
|
|
48
|
+
sql: "report_class = ? AND scope_key = ? AND tenant_id = ?",
|
|
49
|
+
values: [
|
|
50
|
+
reportClassName,
|
|
51
|
+
scopeKey,
|
|
52
|
+
tenantId
|
|
53
|
+
]
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
sql: "report_class = ? AND scope_key = ? AND tenant_id IS NULL",
|
|
57
|
+
values: [reportClassName, scopeKey]
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function runFromRow(row) {
|
|
61
|
+
if (!row || typeof row.id !== "string") return void 0;
|
|
62
|
+
const status = row.status;
|
|
63
|
+
if (status !== "running" && status !== "success" && status !== "failed" && status !== "skipped") return;
|
|
64
|
+
const mode = row.mode === "incremental" ? "incremental" : "rebuild";
|
|
65
|
+
const trigger = row.trigger === "schedule" || row.trigger === "change" || row.trigger === "ttl" || row.trigger === "job" ? row.trigger : "manual";
|
|
66
|
+
return {
|
|
67
|
+
id: row.id,
|
|
68
|
+
status,
|
|
69
|
+
mode,
|
|
70
|
+
trigger,
|
|
71
|
+
...toIso(row.started_at) ? { startedAt: toIso(row.started_at) } : {},
|
|
72
|
+
...toIso(row.completed_at) ? { completedAt: toIso(row.completed_at) } : {},
|
|
73
|
+
rowCount: numberValue(row.row_count),
|
|
74
|
+
changedGroupCount: numberValue(row.changed_group_count),
|
|
75
|
+
mayRetry: status === "failed"
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function jobHandle(job) {
|
|
79
|
+
if (typeof job.id !== "string" || job.id.length === 0) throw new Error("Queued report refresh is missing its job id");
|
|
80
|
+
return {
|
|
81
|
+
jobId: job.id,
|
|
82
|
+
status: job.status,
|
|
83
|
+
attempts: job.attempts,
|
|
84
|
+
maxAttempts: job.maxAttempts,
|
|
85
|
+
pollAfterMs: 1e3
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
async function getReportLifecycle(reportCtor, options) {
|
|
89
|
+
await assertReportTablesReady(options.db, [REPORT_RUNS_TABLE, REPORT_LOCKS_TABLE]);
|
|
90
|
+
const definition = await buildReportDefinition(reportCtor);
|
|
91
|
+
const reportClassName = canonicalClassName(reportCtor);
|
|
92
|
+
const tenantId = lifecycleTenantId(reportCtor);
|
|
93
|
+
const where = rowWhere(reportClassName, scopeKeyForTenant(tenantId), tenantId);
|
|
94
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
95
|
+
const [runResult, lockResult] = await Promise.all([options.db.query(`SELECT id, mode, trigger, status, started_at, completed_at, row_count, changed_group_count
|
|
96
|
+
FROM ${REPORT_RUNS_TABLE}
|
|
97
|
+
WHERE ${where.sql}
|
|
98
|
+
ORDER BY started_at DESC, created_at DESC, id DESC
|
|
99
|
+
LIMIT 1`, ...where.values), options.db.query(`SELECT expires_at
|
|
100
|
+
FROM ${REPORT_LOCKS_TABLE}
|
|
101
|
+
WHERE ${where.sql} AND expires_at > ?
|
|
102
|
+
LIMIT 1`, ...where.values, now.toISOString())]);
|
|
103
|
+
const tableName = reportTableName(reportCtor);
|
|
104
|
+
if (!await tableExists(options.db, tableName)) throw new Error(`Report table '${tableName}' does not exist for ${reportClassName}`);
|
|
105
|
+
const refreshedAtColumn = await reportColumnName(reportClassName, "refreshedAt");
|
|
106
|
+
const configuredTenantField = registeredReport(reportCtor)?.tenantScopedConfig?.field;
|
|
107
|
+
const tenantColumn = configuredTenantField ? await reportColumnName(reportClassName, configuredTenantField) : void 0;
|
|
108
|
+
const materialized = tenantColumn ? await options.db.query(`SELECT MAX(${refreshedAtColumn}) AS refreshed_at FROM ${tableName} WHERE ${tenantColumn} ${tenantId ? "= ?" : "IS NULL"}`, ...tenantId ? [tenantId] : []) : await options.db.query(`SELECT MAX(${refreshedAtColumn}) AS refreshed_at FROM ${tableName}`);
|
|
109
|
+
const run = runFromRow(runResult.rows[0]);
|
|
110
|
+
const refreshedAt = toIso(materialized.rows[0]?.refreshed_at);
|
|
111
|
+
const asOf = latestIso(refreshedAt, run?.status === "success" ? run.completedAt : void 0);
|
|
112
|
+
const ttlMs = definition.refresh?.ttl;
|
|
113
|
+
const isStale = !asOf || ttlMs !== void 0 && now.getTime() - new Date(asOf).getTime() > ttlMs;
|
|
114
|
+
const lockExpiresAt = toIso(lockResult.rows[0]?.expires_at);
|
|
115
|
+
const lockHeld = Boolean(lockExpiresAt);
|
|
116
|
+
return {
|
|
117
|
+
version: 1,
|
|
118
|
+
state: run?.status === "skipped" ? "lock-skipped" : lockHeld ? "refreshing" : run?.status === "failed" ? "failed" : run?.status === "running" ? "stale" : isStale ? "stale" : "current",
|
|
119
|
+
...asOf ? { asOf } : {},
|
|
120
|
+
...refreshedAt ? { refreshedAt } : {},
|
|
121
|
+
hasUsableRows: Boolean(refreshedAt),
|
|
122
|
+
mode: run?.mode ?? definition.refresh?.mode ?? "rebuild",
|
|
123
|
+
...run ? { run } : {},
|
|
124
|
+
...run?.status === "failed" ? { failure: {
|
|
125
|
+
code: "refresh_failed",
|
|
126
|
+
retryable: true
|
|
127
|
+
} } : {},
|
|
128
|
+
lock: {
|
|
129
|
+
held: lockHeld,
|
|
130
|
+
...lockExpiresAt ? { expiresAt: lockExpiresAt } : {}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function actionContext(reportCtor, phase, mode, refreshAction) {
|
|
135
|
+
return {
|
|
136
|
+
phase,
|
|
137
|
+
reportClassName: canonicalClassName(reportCtor),
|
|
138
|
+
tenantScope: "ambient",
|
|
139
|
+
mode,
|
|
140
|
+
requiredPermission: refreshAction?.requiredPermission && refreshAction.requiredPermission.trim().length > 0 ? refreshAction.requiredPermission : "reports.refresh"
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
async function actionMode(reportCtor, requested) {
|
|
144
|
+
if (requested) return requested;
|
|
145
|
+
return (await buildReportDefinition(reportCtor)).refresh?.mode ?? "rebuild";
|
|
146
|
+
}
|
|
147
|
+
async function previewReportRefresh(reportCtor, options) {
|
|
148
|
+
const action = actionContext(reportCtor, "preview", await actionMode(reportCtor, options.mode), options.refreshAction);
|
|
149
|
+
await options.host.authorize(action);
|
|
150
|
+
await options.host.audit(action);
|
|
151
|
+
return {
|
|
152
|
+
phase: "preview",
|
|
153
|
+
lifecycle: await getReportLifecycle(reportCtor, options),
|
|
154
|
+
action,
|
|
155
|
+
execution: "background"
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
async function applyReportRefresh(reportCtor, options) {
|
|
159
|
+
const mode = await actionMode(reportCtor, options.mode);
|
|
160
|
+
const action = actionContext(reportCtor, "apply", mode, options.refreshAction);
|
|
161
|
+
await options.host.authorize(action);
|
|
162
|
+
await options.host.audit(action);
|
|
163
|
+
const tenantId = lifecycleTenantId(reportCtor);
|
|
164
|
+
const enqueue = () => enqueueReportRefresh({
|
|
165
|
+
db: options.db,
|
|
166
|
+
reportClass: canonicalClassName(reportCtor),
|
|
167
|
+
mode,
|
|
168
|
+
trigger: "manual",
|
|
169
|
+
tenantId,
|
|
170
|
+
queue: options.queue,
|
|
171
|
+
priority: options.priority,
|
|
172
|
+
timeout: options.timeout,
|
|
173
|
+
maxAttempts: options.maxAttempts,
|
|
174
|
+
tenantJobCap: options.tenantJobCap
|
|
175
|
+
});
|
|
176
|
+
return {
|
|
177
|
+
phase: "apply",
|
|
178
|
+
job: jobHandle(tenantId === null && getTenantId() ? await withSystemContext(enqueue) : await enqueue())
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function reportRefreshOutcome(result) {
|
|
182
|
+
const scopes = result.tenantResults ?? [result];
|
|
183
|
+
const lockSkippedScopes = scopes.filter((scope) => scope.skipped).length;
|
|
184
|
+
const completedScopes = scopes.length - lockSkippedScopes;
|
|
185
|
+
return {
|
|
186
|
+
state: scopes.length > 1 && lockSkippedScopes > 0 ? "partial" : lockSkippedScopes > 0 ? "lock-skipped" : "current",
|
|
187
|
+
rowCount: result.rowCount,
|
|
188
|
+
changedGroupCount: result.changedGroupCount ?? 0,
|
|
189
|
+
completedScopes,
|
|
190
|
+
lockSkippedScopes,
|
|
191
|
+
mode: result.mode,
|
|
192
|
+
refreshedAt: result.refreshedAt.toISOString(),
|
|
193
|
+
...result.runId ? { runId: result.runId } : {}
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
//#endregion
|
|
197
|
+
//#region src/adapter.ts
|
|
198
|
+
var SENSITIVITIES = /* @__PURE__ */ new Set([
|
|
199
|
+
"public",
|
|
200
|
+
"personal",
|
|
201
|
+
"sensitive",
|
|
202
|
+
"secret"
|
|
203
|
+
]);
|
|
204
|
+
function registeredClass(ctor) {
|
|
205
|
+
return ObjectRegistry.getClassByConstructor(ctor) ?? ObjectRegistry.getClass(ctor.name);
|
|
206
|
+
}
|
|
207
|
+
function fieldPolicy(field) {
|
|
208
|
+
const meta = field?._meta ?? {};
|
|
209
|
+
const sensitivity = [field?.sensitivity, meta.sensitivity].find((value) => typeof value === "string");
|
|
210
|
+
return {
|
|
211
|
+
sensitive: field?.sensitive === true || meta.sensitive === true || sensitivity === "sensitive" || sensitivity === "secret",
|
|
212
|
+
readPermission: typeof field?.readPermission === "string" ? field.readPermission : typeof meta.readPermission === "string" ? meta.readPermission : void 0
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function descriptorMetadata(field) {
|
|
216
|
+
const metadata = field?._meta ?? {};
|
|
217
|
+
const format = [field?.format, metadata.format].find((value) => typeof value === "string" && value.length > 0);
|
|
218
|
+
const rawSensitivity = [field?.sensitivity, metadata.sensitivity].find((value) => typeof value === "string" && SENSITIVITIES.has(value));
|
|
219
|
+
return {
|
|
220
|
+
...format ? { format } : {},
|
|
221
|
+
...rawSensitivity ? { sensitivity: rawSensitivity } : {}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function labelFor(fieldName) {
|
|
225
|
+
return fieldName.replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").replace(/^./, (value) => value.toUpperCase());
|
|
226
|
+
}
|
|
227
|
+
function queryType(type) {
|
|
228
|
+
switch (type) {
|
|
229
|
+
case "integer":
|
|
230
|
+
case "decimal": return "number";
|
|
231
|
+
case "boolean": return "boolean";
|
|
232
|
+
case "datetime": return "datetime";
|
|
233
|
+
case "json": return "json";
|
|
234
|
+
default: return "string";
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function configuredTenantField(configured) {
|
|
238
|
+
return configured ? toSnakeCase(configured) : void 0;
|
|
239
|
+
}
|
|
240
|
+
function isColumnBackedField(field) {
|
|
241
|
+
if (!field) return false;
|
|
242
|
+
if (field.transient === true || field._meta?.transient === true) return false;
|
|
243
|
+
return ![
|
|
244
|
+
"meta",
|
|
245
|
+
"oneToMany",
|
|
246
|
+
"manyToMany"
|
|
247
|
+
].includes(field.type ?? "");
|
|
248
|
+
}
|
|
249
|
+
function reportColumn(field, registryField) {
|
|
250
|
+
const policy = fieldPolicy(registryField);
|
|
251
|
+
if (policy.sensitive || policy.readPermission || !isColumnBackedField(registryField)) return;
|
|
252
|
+
const id = field.columnName ?? toSnakeCase(field.fieldName);
|
|
253
|
+
const report = field.report;
|
|
254
|
+
if (!report) return void 0;
|
|
255
|
+
const type = queryType(field.type ?? registryField?.type);
|
|
256
|
+
const filterOperators = filterOperatorsFor(type);
|
|
257
|
+
return {
|
|
258
|
+
id,
|
|
259
|
+
fieldName: field.fieldName,
|
|
260
|
+
label: registryField?.description || labelFor(field.fieldName),
|
|
261
|
+
kind: report.kind,
|
|
262
|
+
filterScope: report.kind === "aggregate" ? "having" : "where",
|
|
263
|
+
type,
|
|
264
|
+
projectable: true,
|
|
265
|
+
sortable: true,
|
|
266
|
+
facetable: report.kind !== "aggregate",
|
|
267
|
+
...filterOperators ? { filterOperators } : {},
|
|
268
|
+
capabilities: [
|
|
269
|
+
"project",
|
|
270
|
+
"read",
|
|
271
|
+
...filterOperators ? ["filter"] : [],
|
|
272
|
+
"sort",
|
|
273
|
+
...report.kind !== "aggregate" ? ["facet"] : [],
|
|
274
|
+
...report.kind === "aggregate" ? ["aggregate"] : report.kind === "group" ? ["group"] : []
|
|
275
|
+
],
|
|
276
|
+
...report.kind === "group" ? { sourceColumn: toSnakeCase(report.sourceColumn ?? field.fieldName) } : {},
|
|
277
|
+
...report.kind === "bucket" ? {
|
|
278
|
+
bucket: report.unit,
|
|
279
|
+
sourceColumn: toSnakeCase(report.sourceColumn)
|
|
280
|
+
} : {},
|
|
281
|
+
...report.kind === "aggregate" ? {
|
|
282
|
+
aggregate: report.fn,
|
|
283
|
+
...report.column ? { sourceColumn: toSnakeCase(report.column) } : {},
|
|
284
|
+
...report.distinct === void 0 ? {} : { distinct: report.distinct }
|
|
285
|
+
} : {},
|
|
286
|
+
...descriptorMetadata(registryField)
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function identityColumn() {
|
|
290
|
+
return {
|
|
291
|
+
id: "id",
|
|
292
|
+
fieldName: "id",
|
|
293
|
+
label: "ID",
|
|
294
|
+
kind: "identity",
|
|
295
|
+
filterScope: "where",
|
|
296
|
+
type: "string",
|
|
297
|
+
projectable: true,
|
|
298
|
+
sortable: true,
|
|
299
|
+
facetable: false,
|
|
300
|
+
filterOperators: filterOperatorsFor("string"),
|
|
301
|
+
capabilities: [
|
|
302
|
+
"project",
|
|
303
|
+
"read",
|
|
304
|
+
"filter",
|
|
305
|
+
"sort"
|
|
306
|
+
]
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function toQueryField(column) {
|
|
310
|
+
return {
|
|
311
|
+
id: column.id,
|
|
312
|
+
type: column.type,
|
|
313
|
+
...column.projectable === void 0 ? {} : { projectable: column.projectable },
|
|
314
|
+
sortable: column.sortable === true,
|
|
315
|
+
facetable: column.facetable === true,
|
|
316
|
+
...column.filterOperators === void 0 ? {} : { filterOperators: [...column.filterOperators] }
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
function filterOperatorsFor(type) {
|
|
320
|
+
switch (type) {
|
|
321
|
+
case "string": return [
|
|
322
|
+
"eq",
|
|
323
|
+
"ne",
|
|
324
|
+
"gt",
|
|
325
|
+
"gte",
|
|
326
|
+
"lt",
|
|
327
|
+
"lte",
|
|
328
|
+
"in",
|
|
329
|
+
"notIn",
|
|
330
|
+
"like"
|
|
331
|
+
];
|
|
332
|
+
case "number":
|
|
333
|
+
case "datetime": return [
|
|
334
|
+
"eq",
|
|
335
|
+
"ne",
|
|
336
|
+
"gt",
|
|
337
|
+
"gte",
|
|
338
|
+
"lt",
|
|
339
|
+
"lte",
|
|
340
|
+
"in",
|
|
341
|
+
"notIn"
|
|
342
|
+
];
|
|
343
|
+
case "boolean": return [
|
|
344
|
+
"eq",
|
|
345
|
+
"ne",
|
|
346
|
+
"in",
|
|
347
|
+
"notIn"
|
|
348
|
+
];
|
|
349
|
+
case "json": return;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function refreshDescriptor(refresh, refreshPermission = "reports.refresh") {
|
|
353
|
+
const config = refresh ?? {};
|
|
354
|
+
const triggers = /* @__PURE__ */ new Set(["manual"]);
|
|
355
|
+
if (!config.manual) {
|
|
356
|
+
const hasSchedule = Boolean(config.schedule || config.fullRebuildSchedule);
|
|
357
|
+
const hasChangeTrigger = Boolean(config.onChange?.length);
|
|
358
|
+
if (hasSchedule) triggers.add("schedule");
|
|
359
|
+
if (hasChangeTrigger) triggers.add("change");
|
|
360
|
+
if (config.ttl !== void 0 && config.ttl > 0) triggers.add("ttl");
|
|
361
|
+
if (hasSchedule || hasChangeTrigger) triggers.add("job");
|
|
362
|
+
}
|
|
363
|
+
return {
|
|
364
|
+
mode: config.mode ?? "rebuild",
|
|
365
|
+
mayRefreshOnRead: config.ttl !== void 0 && config.ttl > 0 && !config.manual,
|
|
366
|
+
...config.ttl === void 0 ? {} : { ttlMs: config.ttl },
|
|
367
|
+
triggers: [...triggers],
|
|
368
|
+
action: {
|
|
369
|
+
id: "refresh",
|
|
370
|
+
label: "Refresh report",
|
|
371
|
+
scope: "surface",
|
|
372
|
+
phases: ["preview", "apply"],
|
|
373
|
+
requiresPermission: true,
|
|
374
|
+
requiredPermission: refreshPermission.trim().length > 0 ? refreshPermission : "reports.refresh",
|
|
375
|
+
auditRequired: true
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
function resourceId(definition, scope) {
|
|
380
|
+
return `${definition.reportClassName}#${scope ?? "current"}`;
|
|
381
|
+
}
|
|
382
|
+
var VALUE_FORMAT_ALIASES = {
|
|
383
|
+
text: "text",
|
|
384
|
+
date: "date",
|
|
385
|
+
datetime: "datetime",
|
|
386
|
+
"date-time": "datetime",
|
|
387
|
+
percentage: "percentage",
|
|
388
|
+
percent: "percentage",
|
|
389
|
+
count: "count",
|
|
390
|
+
money: "money",
|
|
391
|
+
currency: "money",
|
|
392
|
+
number: "number",
|
|
393
|
+
decimal: "number",
|
|
394
|
+
integer: "number"
|
|
395
|
+
};
|
|
396
|
+
var VALUE_FORMATS = new Set(Object.values(VALUE_FORMAT_ALIASES));
|
|
397
|
+
var COLUMN_ROLES = /* @__PURE__ */ new Set([
|
|
398
|
+
"data",
|
|
399
|
+
"status",
|
|
400
|
+
"action"
|
|
401
|
+
]);
|
|
402
|
+
var STRUCTURAL_ROW_KINDS = /* @__PURE__ */ new Set([
|
|
403
|
+
"summary",
|
|
404
|
+
"subtotal",
|
|
405
|
+
"aggregate",
|
|
406
|
+
"footer"
|
|
407
|
+
]);
|
|
408
|
+
function valueFormat(column) {
|
|
409
|
+
const configured = column.format?.trim().toLowerCase();
|
|
410
|
+
if (configured && Object.hasOwn(VALUE_FORMAT_ALIASES, configured)) return VALUE_FORMAT_ALIASES[configured];
|
|
411
|
+
if (column.kind === "bucket") return column.bucket === "minute" || column.bucket === "hour" ? "datetime" : "date";
|
|
412
|
+
if (column.kind === "aggregate" && column.aggregate === "count") return "count";
|
|
413
|
+
if (column.type === "datetime") return "datetime";
|
|
414
|
+
if (column.type === "number") return "number";
|
|
415
|
+
return "text";
|
|
416
|
+
}
|
|
417
|
+
function headerPath(column) {
|
|
418
|
+
if (column.kind === "aggregate") return [{
|
|
419
|
+
id: "measures",
|
|
420
|
+
label: "Measures"
|
|
421
|
+
}, {
|
|
422
|
+
id: `aggregate:${column.aggregate ?? "value"}`,
|
|
423
|
+
label: labelFor(column.aggregate ?? "value")
|
|
424
|
+
}];
|
|
425
|
+
if (column.kind === "bucket") return [{
|
|
426
|
+
id: "dimensions",
|
|
427
|
+
label: "Dimensions"
|
|
428
|
+
}, {
|
|
429
|
+
id: "time",
|
|
430
|
+
label: "Time"
|
|
431
|
+
}];
|
|
432
|
+
if (column.kind === "group") return [{
|
|
433
|
+
id: "dimensions",
|
|
434
|
+
label: "Dimensions"
|
|
435
|
+
}, {
|
|
436
|
+
id: "groups",
|
|
437
|
+
label: "Groups"
|
|
438
|
+
}];
|
|
439
|
+
return [{
|
|
440
|
+
id: "dimensions",
|
|
441
|
+
label: "Dimensions"
|
|
442
|
+
}];
|
|
443
|
+
}
|
|
444
|
+
function responsive(column) {
|
|
445
|
+
if (column.kind === "group" || column.kind === "bucket") return {
|
|
446
|
+
priority: 100,
|
|
447
|
+
keepVisible: true
|
|
448
|
+
};
|
|
449
|
+
if (column.kind === "identity") return { priority: 80 };
|
|
450
|
+
return { priority: 20 };
|
|
451
|
+
}
|
|
452
|
+
function assertHeaderPath(columnId, path) {
|
|
453
|
+
if (!Array.isArray(path)) throw new TypeError(`Report DataTable headerPath for ${columnId} must be an array`);
|
|
454
|
+
return path.map((segment) => {
|
|
455
|
+
if (!segment || typeof segment.id !== "string" || segment.id.trim().length === 0 || typeof segment.label !== "string" || segment.label.trim().length === 0) throw new TypeError(`Report DataTable headerPath entries for ${columnId} require non-empty id and label`);
|
|
456
|
+
return {
|
|
457
|
+
id: segment.id,
|
|
458
|
+
label: segment.label
|
|
459
|
+
};
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
function dataTableColumn(column, override) {
|
|
463
|
+
const format = override?.valueFormat ?? valueFormat(column);
|
|
464
|
+
const path = override?.headerPath ?? headerPath(column);
|
|
465
|
+
if (!VALUE_FORMATS.has(format)) throw new TypeError(`Report DataTable valueFormat for ${column.id} is not supported: ${String(format)}`);
|
|
466
|
+
if (override?.role !== void 0 && !COLUMN_ROLES.has(override.role)) throw new TypeError(`Report DataTable role for ${column.id} is not supported: ${String(override.role)}`);
|
|
467
|
+
if (override?.label !== void 0 && (typeof override.label !== "string" || override.label.trim().length === 0)) throw new TypeError(`Report DataTable label for ${column.id} must not be empty`);
|
|
468
|
+
if (override?.align !== void 0 && override.align !== "left" && override.align !== "right") throw new TypeError(`Report DataTable align for ${column.id} is not supported: ${String(override.align)}`);
|
|
469
|
+
if (override?.responsive?.priority !== void 0 && (!Number.isFinite(override.responsive.priority) || override.responsive.priority < 0)) throw new TypeError(`Report DataTable responsive priority for ${column.id} must be a non-negative finite number`);
|
|
470
|
+
if (override?.responsive?.keepVisible !== void 0 && typeof override.responsive.keepVisible !== "boolean") throw new TypeError(`Report DataTable keepVisible for ${column.id} must be a boolean`);
|
|
471
|
+
return {
|
|
472
|
+
id: column.id,
|
|
473
|
+
label: override?.label ?? column.label,
|
|
474
|
+
accessor: column.id,
|
|
475
|
+
sortable: column.sortable === true,
|
|
476
|
+
searchable: false,
|
|
477
|
+
filterable: column.filterOperators !== void 0,
|
|
478
|
+
headerPath: assertHeaderPath(column.id, path),
|
|
479
|
+
valueFormat: format,
|
|
480
|
+
align: override?.align ?? ([
|
|
481
|
+
"count",
|
|
482
|
+
"money",
|
|
483
|
+
"number",
|
|
484
|
+
"percentage"
|
|
485
|
+
].includes(format) ? "right" : "left"),
|
|
486
|
+
role: override?.role ?? "data",
|
|
487
|
+
responsive: {
|
|
488
|
+
...responsive(column),
|
|
489
|
+
...override?.responsive
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
function structuralRows(columnIds, rows = []) {
|
|
494
|
+
const ids = /* @__PURE__ */ new Set();
|
|
495
|
+
return rows.map((row) => {
|
|
496
|
+
if (!row || typeof row.id !== "string" || row.id.trim().length === 0 || typeof row.label !== "string" || row.label.trim().length === 0) throw new TypeError("Report structural rows require non-empty id and label values");
|
|
497
|
+
if (ids.has(row.id)) throw new TypeError(`Report structural row id must be unique: ${row.id}`);
|
|
498
|
+
if (!STRUCTURAL_ROW_KINDS.has(row.kind)) throw new TypeError(`Report structural row kind is not supported: ${String(row.kind)}`);
|
|
499
|
+
if (row.labelColumnId !== void 0) {
|
|
500
|
+
if (typeof row.labelColumnId !== "string" || row.labelColumnId.trim().length === 0 || !columnIds.has(row.labelColumnId)) throw new TypeError(`Report structural row labelColumnId must name an adapter column: ${String(row.labelColumnId)}`);
|
|
501
|
+
}
|
|
502
|
+
if (row.values !== void 0 && (typeof row.values !== "object" || row.values === null || Array.isArray(row.values))) throw new TypeError("Report structural row values must be an object");
|
|
503
|
+
if (row.values && Object.keys(row.values).some((columnId) => !columnIds.has(columnId))) throw new TypeError("Report structural row values must use adapter column ids");
|
|
504
|
+
ids.add(row.id);
|
|
505
|
+
return {
|
|
506
|
+
id: row.id,
|
|
507
|
+
kind: row.kind,
|
|
508
|
+
label: row.label,
|
|
509
|
+
...row.values !== void 0 ? { values: jsonSafeValue(row.values) } : {},
|
|
510
|
+
...row.labelColumnId !== void 0 ? { labelColumnId: row.labelColumnId } : {},
|
|
511
|
+
selection: "excluded",
|
|
512
|
+
actions: "excluded"
|
|
513
|
+
};
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
function drilldownDescriptor(columns, sourceClassName) {
|
|
517
|
+
return {
|
|
518
|
+
id: "drilldown",
|
|
519
|
+
sourceClassName,
|
|
520
|
+
fields: columns.filter((column) => (column.kind === "group" || column.kind === "bucket") && typeof column.sourceColumn === "string").map((column) => ({
|
|
521
|
+
id: column.id,
|
|
522
|
+
sourceColumn: column.sourceColumn,
|
|
523
|
+
kind: column.kind,
|
|
524
|
+
...column.kind === "bucket" && column.bucket ? { bucket: column.bucket } : {}
|
|
525
|
+
})),
|
|
526
|
+
inherits: [
|
|
527
|
+
"principal",
|
|
528
|
+
"tenant",
|
|
529
|
+
"report-definition",
|
|
530
|
+
"field-policy"
|
|
531
|
+
]
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
async function buildReportAdapterDescriptor(reportCtor, options = {}) {
|
|
535
|
+
const definition = await buildReportDefinition(reportCtor);
|
|
536
|
+
const registered = registeredClass(reportCtor);
|
|
537
|
+
const fields = await ObjectRegistry.getAllFields(definition.reportClassName);
|
|
538
|
+
const tenantField = configuredTenantField(registered?.tenantScopedConfig?.field);
|
|
539
|
+
const columns = [identityColumn(), ...definition.fields.map((field) => reportColumn(field, fields.get(field.fieldName))).filter((field) => Boolean(field))].sort((left, right) => left.id.localeCompare(right.id));
|
|
540
|
+
const schema = {
|
|
541
|
+
version: 1,
|
|
542
|
+
identityField: "id",
|
|
543
|
+
fields: columns.map(toQueryField),
|
|
544
|
+
defaultSort: [{
|
|
545
|
+
field: "id",
|
|
546
|
+
direction: "asc"
|
|
547
|
+
}],
|
|
548
|
+
supports: {
|
|
549
|
+
cursorPagination: false,
|
|
550
|
+
consistency: false,
|
|
551
|
+
facets: true
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
const dataTable = {
|
|
555
|
+
rowKey: "id",
|
|
556
|
+
manualPagination: true,
|
|
557
|
+
manualSorting: true,
|
|
558
|
+
enableFiltering: true,
|
|
559
|
+
enableSearch: false,
|
|
560
|
+
columns: columns.map((column) => dataTableColumn(column, options.dataTable?.columns?.[column.id])),
|
|
561
|
+
structuralRows: structuralRows(new Set(columns.map((column) => column.id)), options.dataTable?.structuralRows)
|
|
562
|
+
};
|
|
563
|
+
return {
|
|
564
|
+
version: 1,
|
|
565
|
+
resourceId: resourceId(definition, options.tenantScope),
|
|
566
|
+
reportClassName: definition.reportClassName,
|
|
567
|
+
sourceClassName: definition.sourceClassName,
|
|
568
|
+
tenantScoped: Boolean(registered?.tenantScopedConfig),
|
|
569
|
+
...tenantField ? { tenantField } : {},
|
|
570
|
+
identityField: "id",
|
|
571
|
+
columns,
|
|
572
|
+
schema,
|
|
573
|
+
queryExecution: {
|
|
574
|
+
modes: [
|
|
575
|
+
"visible",
|
|
576
|
+
"background",
|
|
577
|
+
"silent"
|
|
578
|
+
],
|
|
579
|
+
visible: { delivery: "result" },
|
|
580
|
+
background: {
|
|
581
|
+
delivery: "queued",
|
|
582
|
+
requiresHost: true
|
|
583
|
+
},
|
|
584
|
+
silent: {
|
|
585
|
+
delivery: "result",
|
|
586
|
+
mutatesVisibleSurface: false
|
|
587
|
+
}
|
|
588
|
+
},
|
|
589
|
+
dataTable,
|
|
590
|
+
drilldown: drilldownDescriptor(columns, definition.sourceClassName),
|
|
591
|
+
refresh: refreshDescriptor(definition.refresh, options.refreshPermission)
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
function publicFieldMap(descriptor) {
|
|
595
|
+
return new Map(descriptor.columns.map((column) => [column.id, column.fieldName]));
|
|
596
|
+
}
|
|
597
|
+
async function buildReportDrilldownQuery(reportCtor, row, options = {}) {
|
|
598
|
+
const descriptor = await buildReportAdapterDescriptor(reportCtor, options);
|
|
599
|
+
const constraints = descriptor.drilldown.fields.map((field) => {
|
|
600
|
+
if (!Object.hasOwn(row, field.id)) throw new DataQueryValidationError(`Report drilldown row is missing grouping field: ${field.id}`, "DATA_QUERY_RESULT_INVALID");
|
|
601
|
+
return {
|
|
602
|
+
id: field.id,
|
|
603
|
+
sourceColumn: field.sourceColumn,
|
|
604
|
+
kind: field.kind,
|
|
605
|
+
value: jsonSafeValue(row[field.id]),
|
|
606
|
+
...field.bucket ? { bucket: field.bucket } : {}
|
|
607
|
+
};
|
|
608
|
+
});
|
|
609
|
+
return {
|
|
610
|
+
version: 1,
|
|
611
|
+
resourceId: descriptor.resourceId,
|
|
612
|
+
reportClassName: descriptor.reportClassName,
|
|
613
|
+
sourceClassName: descriptor.drilldown.sourceClassName,
|
|
614
|
+
constraints,
|
|
615
|
+
inherits: [...descriptor.drilldown.inherits]
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
var MAX_REPORT_FILTER_OR_GROUPS = 128;
|
|
619
|
+
function reportFilterError(message) {
|
|
620
|
+
throw new DataQueryValidationError(message, "DATA_QUERY_UNSUPPORTED");
|
|
621
|
+
}
|
|
622
|
+
function crossProductWhere(left, right) {
|
|
623
|
+
if (left.length * right.length > MAX_REPORT_FILTER_OR_GROUPS) return reportFilterError(`Report filter expands beyond ${MAX_REPORT_FILTER_OR_GROUPS} OR groups`);
|
|
624
|
+
return left.flatMap((leftGroup) => right.map((rightGroup) => [...leftGroup, ...rightGroup]));
|
|
625
|
+
}
|
|
626
|
+
function inverseOperator(operator) {
|
|
627
|
+
switch (operator) {
|
|
628
|
+
case "eq": return "ne";
|
|
629
|
+
case "ne": return "eq";
|
|
630
|
+
case "gt": return "lte";
|
|
631
|
+
case "gte": return "lt";
|
|
632
|
+
case "lt": return "gte";
|
|
633
|
+
case "lte": return "gt";
|
|
634
|
+
case "in": return "notIn";
|
|
635
|
+
case "notIn": return "in";
|
|
636
|
+
case "like": return reportFilterError("Report filters do not support negating a LIKE predicate");
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
function collectionCondition(field, operator, value) {
|
|
640
|
+
const keyFor = (suffix2) => suffix2 ? `${field} ${suffix2}` : field;
|
|
641
|
+
const scalar = (key, scalarValue) => [[{ [key]: scalarValue }]];
|
|
642
|
+
if (operator === "in") {
|
|
643
|
+
const values = value;
|
|
644
|
+
const nonNull = values.filter((entry) => entry !== null);
|
|
645
|
+
if (nonNull.length === 0) return scalar(field, null);
|
|
646
|
+
if (nonNull.length === values.length) return scalar(keyFor("in"), nonNull);
|
|
647
|
+
return [[{ [field]: null }], [{ [keyFor("in")]: nonNull }]];
|
|
648
|
+
}
|
|
649
|
+
if (operator === "notIn") return [value.map((entry) => ({ [keyFor("!=")]: entry }))];
|
|
650
|
+
return scalar(keyFor({
|
|
651
|
+
eq: "",
|
|
652
|
+
ne: "!=",
|
|
653
|
+
gt: ">",
|
|
654
|
+
gte: ">=",
|
|
655
|
+
lt: "<",
|
|
656
|
+
lte: "<=",
|
|
657
|
+
like: "like"
|
|
658
|
+
}[operator]), value);
|
|
659
|
+
}
|
|
660
|
+
function materializedWhereForFilter(filter, fields, negate = false) {
|
|
661
|
+
if (filter.kind === "condition") {
|
|
662
|
+
const field = fields.get(filter.field);
|
|
663
|
+
if (!field) return reportFilterError(`Report filter field is not declared: ${filter.field}`);
|
|
664
|
+
return collectionCondition(field, negate ? inverseOperator(filter.operator) : filter.operator, filter.value);
|
|
665
|
+
}
|
|
666
|
+
if (filter.kind === "not") return materializedWhereForFilter(filter.filter, fields, !negate);
|
|
667
|
+
if (filter.kind === "all" && !negate || filter.kind === "any" && negate) return filter.filters.reduce((combined, child) => crossProductWhere(combined, materializedWhereForFilter(child, fields, negate)), [[]]);
|
|
668
|
+
const groups = filter.filters.flatMap((child) => materializedWhereForFilter(child, fields, negate));
|
|
669
|
+
if (groups.length > MAX_REPORT_FILTER_OR_GROUPS) return reportFilterError(`Report filter expands beyond ${MAX_REPORT_FILTER_OR_GROUPS} OR groups`);
|
|
670
|
+
return groups;
|
|
671
|
+
}
|
|
672
|
+
function materializedWhere(filter, fields) {
|
|
673
|
+
return filter ? materializedWhereForFilter(filter, fields) : void 0;
|
|
674
|
+
}
|
|
675
|
+
function filterScopeForColumn(column) {
|
|
676
|
+
return column.filterScope;
|
|
677
|
+
}
|
|
678
|
+
function splitReportFilterScopes(descriptor, filter) {
|
|
679
|
+
if (!filter) return {};
|
|
680
|
+
const columns = new Map(descriptor.columns.map((column) => [column.id, column]));
|
|
681
|
+
const combineAll = (filters) => {
|
|
682
|
+
const flattened = filters.flatMap((candidate) => candidate.kind === "all" ? candidate.filters : [candidate]);
|
|
683
|
+
return flattened.length === 0 ? void 0 : flattened.length === 1 ? flattened[0] : {
|
|
684
|
+
kind: "all",
|
|
685
|
+
filters: flattened
|
|
686
|
+
};
|
|
687
|
+
};
|
|
688
|
+
const split = (candidate) => {
|
|
689
|
+
if (candidate.kind === "condition") {
|
|
690
|
+
const column = columns.get(candidate.field);
|
|
691
|
+
if (!column) reportFilterError(`Report filter field is not declared: ${candidate.field}`);
|
|
692
|
+
return filterScopeForColumn(column) === "where" ? { where: candidate } : { having: candidate };
|
|
693
|
+
}
|
|
694
|
+
if (candidate.kind === "all") {
|
|
695
|
+
const children2 = candidate.filters.map(split);
|
|
696
|
+
const where = combineAll(children2.flatMap((child) => child.where ? [child.where] : []));
|
|
697
|
+
const having = combineAll(children2.flatMap((child) => child.having ? [child.having] : []));
|
|
698
|
+
return {
|
|
699
|
+
...where ? { where } : {},
|
|
700
|
+
...having ? { having } : {}
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
const children = candidate.kind === "not" ? [split(candidate.filter)] : candidate.filters.map(split);
|
|
704
|
+
const scopes = new Set(children.flatMap((child) => [...child.where ? ["where"] : [], ...child.having ? ["having"] : []]));
|
|
705
|
+
if (scopes.size !== 1) reportFilterError("Report WHERE and HAVING filters cannot be mixed inside one OR or NOT expression");
|
|
706
|
+
const scope = scopes.has("where") ? "where" : "having";
|
|
707
|
+
const filters = children.flatMap((child) => {
|
|
708
|
+
const filter2 = scope === "where" ? child.where : child.having;
|
|
709
|
+
return filter2 ? [filter2] : [];
|
|
710
|
+
});
|
|
711
|
+
if (candidate.kind === "not") {
|
|
712
|
+
const [filter2] = filters;
|
|
713
|
+
if (!filter2) reportFilterError("Report NOT expressions must contain one filter");
|
|
714
|
+
return scope === "where" ? { where: {
|
|
715
|
+
kind: "not",
|
|
716
|
+
filter: filter2
|
|
717
|
+
} } : { having: {
|
|
718
|
+
kind: "not",
|
|
719
|
+
filter: filter2
|
|
720
|
+
} };
|
|
721
|
+
}
|
|
722
|
+
return scope === "where" ? { where: {
|
|
723
|
+
kind: "any",
|
|
724
|
+
filters
|
|
725
|
+
} } : { having: {
|
|
726
|
+
kind: "any",
|
|
727
|
+
filters
|
|
728
|
+
} };
|
|
729
|
+
};
|
|
730
|
+
return split(filter);
|
|
731
|
+
}
|
|
732
|
+
function jsonSafeValue(value, ancestors = /* @__PURE__ */ new WeakSet()) {
|
|
733
|
+
if (value instanceof Date) return value.toISOString();
|
|
734
|
+
if (typeof value === "bigint") {
|
|
735
|
+
const numeric = Number(value);
|
|
736
|
+
if (!Number.isSafeInteger(numeric)) throw new RangeError("Materialized bigint values must be safely representable as numbers");
|
|
737
|
+
return numeric;
|
|
738
|
+
}
|
|
739
|
+
if (value && typeof value === "object") {
|
|
740
|
+
if (ancestors.has(value)) throw new TypeError("Values must not contain circular references");
|
|
741
|
+
ancestors.add(value);
|
|
742
|
+
try {
|
|
743
|
+
if (Array.isArray(value)) return value.map((entry) => jsonSafeValue(entry, ancestors));
|
|
744
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, jsonSafeValue(entry, ancestors)]));
|
|
745
|
+
} finally {
|
|
746
|
+
ancestors.delete(value);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return value;
|
|
750
|
+
}
|
|
751
|
+
function mapMaterializedRow(row, projection, fieldMap) {
|
|
752
|
+
const mapped = {};
|
|
753
|
+
for (const id of projection) {
|
|
754
|
+
const sourceField = fieldMap.get(id) ?? id;
|
|
755
|
+
if (sourceField in row) mapped[id] = jsonSafeValue(row[sourceField]);
|
|
756
|
+
}
|
|
757
|
+
return mapped;
|
|
758
|
+
}
|
|
759
|
+
function queryFreshness(lifecycle) {
|
|
760
|
+
switch (lifecycle.state) {
|
|
761
|
+
case "current": return {
|
|
762
|
+
state: "fresh",
|
|
763
|
+
...lifecycle.asOf ? { asOf: lifecycle.asOf } : {}
|
|
764
|
+
};
|
|
765
|
+
case "stale":
|
|
766
|
+
case "lock-skipped":
|
|
767
|
+
case "failed": return {
|
|
768
|
+
state: "stale",
|
|
769
|
+
...lifecycle.asOf ? { asOf: lifecycle.asOf } : {}
|
|
770
|
+
};
|
|
771
|
+
case "refreshing": return lifecycle.hasUsableRows ? {
|
|
772
|
+
state: "stale",
|
|
773
|
+
...lifecycle.asOf ? { asOf: lifecycle.asOf } : {}
|
|
774
|
+
} : { state: "unknown" };
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
async function queryReportMaterializedRows(reportCtor, input, options = {}) {
|
|
778
|
+
const descriptor = await buildReportAdapterDescriptor(reportCtor);
|
|
779
|
+
const request = normalizeDataQueryRequest(input, descriptor.schema);
|
|
780
|
+
splitReportFilterScopes(descriptor, request.filter);
|
|
781
|
+
const queryFingerprint = createDataQueryFingerprint(request, descriptor.schema);
|
|
782
|
+
if (options.execution === "background") {
|
|
783
|
+
const queued = await options.enqueueBackgroundQuery({
|
|
784
|
+
version: 1,
|
|
785
|
+
execution: "background",
|
|
786
|
+
resourceId: descriptor.resourceId,
|
|
787
|
+
reportClassName: descriptor.reportClassName,
|
|
788
|
+
request,
|
|
789
|
+
inherits: [
|
|
790
|
+
"principal",
|
|
791
|
+
"tenant",
|
|
792
|
+
"report-definition",
|
|
793
|
+
"field-policy"
|
|
794
|
+
]
|
|
795
|
+
});
|
|
796
|
+
if (typeof queued.taskId !== "string" || queued.taskId.length === 0) throw new Error("Background report query hosts must return a task id");
|
|
797
|
+
return {
|
|
798
|
+
version: 1,
|
|
799
|
+
execution: "background",
|
|
800
|
+
status: "queued",
|
|
801
|
+
taskId: queued.taskId,
|
|
802
|
+
queryFingerprint
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
const execution = options.execution ?? "visible";
|
|
806
|
+
const lifecycleDb = options.db;
|
|
807
|
+
if (options.lifecycle && !lifecycleDb) throw new Error("Report lifecycle disclosure requires a database handle");
|
|
808
|
+
const lifecycleOptions = options.lifecycle && lifecycleDb ? {
|
|
809
|
+
db: lifecycleDb,
|
|
810
|
+
...options.lifecycle
|
|
811
|
+
} : void 0;
|
|
812
|
+
const lifecycleBefore = lifecycleOptions ? await getReportLifecycle(reportCtor, lifecycleOptions) : void 0;
|
|
813
|
+
const fieldMap = publicFieldMap(descriptor);
|
|
814
|
+
const where = materializedWhere(request.filter, fieldMap);
|
|
815
|
+
const collection = options.collection ?? await ObjectRegistry.getCollection(descriptor.reportClassName, { db: options.db });
|
|
816
|
+
let rows = [];
|
|
817
|
+
let page;
|
|
818
|
+
let total;
|
|
819
|
+
let facets;
|
|
820
|
+
if (request.mode === "rows") {
|
|
821
|
+
const projection = request.projection ?? [descriptor.identityField];
|
|
822
|
+
const select = projection.map((id) => fieldMap.get(id) ?? id);
|
|
823
|
+
const offset = request.page?.kind === "offset" ? request.page.offset : 0;
|
|
824
|
+
const limit = request.page?.limit ?? 50;
|
|
825
|
+
const orderBy = request.sort?.map(({ field, direction }) => `${fieldMap.get(field) ?? field} ${direction.toUpperCase()}`);
|
|
826
|
+
rows = (await collection.list({
|
|
827
|
+
select,
|
|
828
|
+
offset,
|
|
829
|
+
limit,
|
|
830
|
+
orderBy: orderBy?.length === 1 ? orderBy[0] : orderBy,
|
|
831
|
+
...where === void 0 ? {} : { where }
|
|
832
|
+
})).map((row) => mapMaterializedRow(row, projection, fieldMap));
|
|
833
|
+
rows.forEach((row) => {
|
|
834
|
+
if (typeof row.id !== "string" || row.id.length === 0) throw new Error("Materialized report rows require a non-empty string id");
|
|
835
|
+
});
|
|
836
|
+
total = await collection.count(where === void 0 ? void 0 : { where });
|
|
837
|
+
page = {
|
|
838
|
+
kind: "offset",
|
|
839
|
+
offset,
|
|
840
|
+
limit,
|
|
841
|
+
hasMore: offset + rows.length < total
|
|
842
|
+
};
|
|
843
|
+
} else {
|
|
844
|
+
await collection.list({
|
|
845
|
+
select: ["id"],
|
|
846
|
+
offset: 0,
|
|
847
|
+
limit: 1,
|
|
848
|
+
orderBy: "id ASC",
|
|
849
|
+
...where === void 0 ? {} : { where }
|
|
850
|
+
});
|
|
851
|
+
total = await collection.count(where === void 0 ? void 0 : { where });
|
|
852
|
+
if (request.mode === "facets") {
|
|
853
|
+
if (!collection.facets) throw new Error("Report materialized facet reads require a collection facets() implementation");
|
|
854
|
+
const requested = request.facets ?? [];
|
|
855
|
+
const sourceFacets = await collection.facets({
|
|
856
|
+
fields: requested.map((facet) => ({
|
|
857
|
+
field: fieldMap.get(facet.field) ?? facet.field,
|
|
858
|
+
limit: facet.limit
|
|
859
|
+
})),
|
|
860
|
+
...where === void 0 ? {} : { where }
|
|
861
|
+
});
|
|
862
|
+
const byField = new Map(sourceFacets.map((facet) => [facet.field, facet]));
|
|
863
|
+
facets = requested.map((facet) => {
|
|
864
|
+
const sourceField = fieldMap.get(facet.field) ?? facet.field;
|
|
865
|
+
const values = byField.get(sourceField)?.values ?? [];
|
|
866
|
+
return {
|
|
867
|
+
field: facet.field,
|
|
868
|
+
values: values.map((value) => ({
|
|
869
|
+
value: value.value,
|
|
870
|
+
count: value.count
|
|
871
|
+
})),
|
|
872
|
+
truncated: values.length >= facet.limit
|
|
873
|
+
};
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
const normalized = normalizeDataQueryResult({
|
|
878
|
+
version: 1,
|
|
879
|
+
requestId: request.requestId,
|
|
880
|
+
queryFingerprint,
|
|
881
|
+
identityField: descriptor.identityField,
|
|
882
|
+
rows,
|
|
883
|
+
...page ? { page } : {},
|
|
884
|
+
total: {
|
|
885
|
+
kind: "exact",
|
|
886
|
+
value: total
|
|
887
|
+
},
|
|
888
|
+
...facets === void 0 ? {} : { facets },
|
|
889
|
+
freshness: { state: "unknown" },
|
|
890
|
+
warnings: [],
|
|
891
|
+
truncated: false
|
|
892
|
+
}, request, descriptor.schema);
|
|
893
|
+
if (!lifecycleBefore || !lifecycleOptions) return {
|
|
894
|
+
...normalized,
|
|
895
|
+
execution
|
|
896
|
+
};
|
|
897
|
+
const lifecycleAfter = await getReportLifecycle(reportCtor, lifecycleOptions);
|
|
898
|
+
return {
|
|
899
|
+
...normalized,
|
|
900
|
+
execution,
|
|
901
|
+
freshness: queryFreshness(lifecycleAfter),
|
|
902
|
+
reportLifecycle: {
|
|
903
|
+
snapshot: lifecycleAfter,
|
|
904
|
+
read: lifecycleBefore.state !== "current" && lifecycleAfter.state === "current" ? "refresh-triggered" : lifecycleAfter.state === "current" ? "current" : "stale"
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
function reportMaterializedRowKey(row) {
|
|
909
|
+
const id = row.id;
|
|
910
|
+
if (typeof id !== "string" || id.length === 0) throw new Error("Materialized report rows require a non-empty string id");
|
|
911
|
+
return id;
|
|
912
|
+
}
|
|
913
|
+
//#endregion
|
|
11
914
|
//#region src/report.ts
|
|
12
915
|
function registryColumnName(fieldName, field) {
|
|
13
916
|
return validateColumnName(field?.columnName ?? field?._meta?.columnName ?? toSnakeCase(fieldName));
|
|
@@ -85,6 +988,6 @@ var SmrtReportCollection = class extends SmrtCollection {
|
|
|
85
988
|
}
|
|
86
989
|
};
|
|
87
990
|
//#endregion
|
|
88
|
-
export { REPORT_LOCKS_TABLE, REPORT_REFRESH_TASKS_TABLE, REPORT_RUNS_TABLE, REPORT_RUNTIME_TABLES, REPORT_SCHEDULER_TABLES, REPORT_SCHEDULES_TABLE, REPORT_WATERMARKS_TABLE, ReportScheduleRunner, SmrtReport, SmrtReportCollection, SmrtReportLock, SmrtReportLockCollection, SmrtReportRefreshTask, SmrtReportRun, SmrtReportRunCollection, SmrtReportSchedule, SmrtReportScheduleCollection, SmrtReportWatermark, SmrtReportWatermarkCollection, aggregate, assertReportTablesReady, avg, bucketExpr, buildAggregate, buildReportDefinition, compileReportDefinition, compileReportSpec, count, day, enqueueReportRefresh, ensureReportRefreshSchedules, getReportGroupingColumns, getRuntimeReportOptions, groupBy, hour, max, min, minute, month, quarter, refreshReport, registerReportRefreshInterceptor, report, reportRowIdentity, scopeKeyForTenant, sum, week, year };
|
|
991
|
+
export { REPORT_LOCKS_TABLE, REPORT_REFRESH_TASKS_TABLE, REPORT_RUNS_TABLE, REPORT_RUNTIME_TABLES, REPORT_SCHEDULER_TABLES, REPORT_SCHEDULES_TABLE, REPORT_WATERMARKS_TABLE, ReportScheduleRunner, SmrtReport, SmrtReportCollection, SmrtReportLock, SmrtReportLockCollection, SmrtReportRefreshTask, SmrtReportRun, SmrtReportRunCollection, SmrtReportSchedule, SmrtReportScheduleCollection, SmrtReportWatermark, SmrtReportWatermarkCollection, aggregate, applyReportRefresh, assertReportTablesReady, avg, bucketExpr, buildAggregate, buildReportAdapterDescriptor, buildReportDefinition, buildReportDrilldownQuery, compileReportDefinition, compileReportSpec, count, day, enqueueReportRefresh, ensureReportRefreshSchedules, getReportGroupingColumns, getReportLifecycle, getRuntimeReportOptions, groupBy, hour, max, min, minute, month, previewReportRefresh, quarter, queryReportMaterializedRows, refreshReport, registerReportRefreshInterceptor, report, reportMaterializedRowKey, reportRefreshOutcome, reportRowIdentity, scopeKeyForTenant, splitReportFilterScopes, sum, week, year };
|
|
89
992
|
|
|
90
993
|
//# sourceMappingURL=index.js.map
|