@happyvertical/smrt-reports 0.55.6 → 0.55.8

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.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import { aggregate, avg, count, day, getRuntimeReportOptions, groupBy, hour, max, min, minute, month, quarter, report, sum, week, year } from "./decorators.js";
2
2
  import { buildReportDefinition, compileReportDefinition, compileReportSpec, getReportGroupingColumns } from "./compiler.js";
3
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";
4
- import { refreshReport, reportRowIdentity } from "./refresh.js";
4
+ import { adapterTypeFromDb, refreshReport, reportRowIdentity, tenantColumn } from "./refresh.js";
5
5
  import { ReportScheduleRunner, SmrtPrincipalReportRefreshTask, SmrtReportRefreshTask, enqueueReportRefresh, ensureReportRefreshSchedules, registerReportRefreshExecutionAuthorityHost, registerReportRefreshInterceptor, registerReportRefreshJobIntegritySigner } from "./scheduler.js";
6
6
  import { bucketExpr, buildAggregate } from "./aggregate.js";
7
- import { DataQueryValidationError, ObjectRegistry, SmrtCollection, SmrtObject, createDataQueryFingerprint, normalizeDataQueryRequest, normalizeDataQueryResult } from "@happyvertical/smrt-core";
7
+ import { DataQueryValidationError, ObjectRegistry, SmrtCollection, SmrtObject, SmrtRecipe, createDataQueryFingerprint, field, normalizeDataQueryRequest, normalizeDataQueryResult, smrt } from "@happyvertical/smrt-core";
8
8
  import { toSnakeCase } from "@happyvertical/smrt-core/utils";
9
- import { getTenantId, withSystemContext } from "@happyvertical/smrt-tenancy";
9
+ import { TenantScoped, getTenantId, isTenancyEnabled, isTenantScopedClass, tenantId, withSystemContext } from "@happyvertical/smrt-tenancy";
10
10
  import { tableExists, validateColumnName } from "@happyvertical/sql";
11
11
  import { createHash } from "node:crypto";
12
12
  //#region src/lifecycle.ts
@@ -918,6 +918,32 @@ function reportMaterializedRowKey(row) {
918
918
  return id;
919
919
  }
920
920
  //#endregion
921
+ //#region src/recipes.ts
922
+ var MaterializedReportsRecipe = class extends SmrtRecipe {
923
+ static id = "reports.materialized";
924
+ static help = "./materialized.recipe.md";
925
+ static label = "Reports";
926
+ static summary = "Keep totals and trends ready to read, refreshed on a schedule and open to the assistant.";
927
+ static synonyms = [
928
+ "reports",
929
+ "totals",
930
+ "summaries",
931
+ "rollups",
932
+ "kpis",
933
+ "trends",
934
+ "dashboards"
935
+ ];
936
+ static models = [
937
+ SmrtReportRun,
938
+ SmrtReportWatermark,
939
+ SmrtReportLock,
940
+ SmrtReportSchedule,
941
+ SmrtReportRefreshTask,
942
+ SmrtPrincipalReportRefreshTask
943
+ ];
944
+ static runtime = "server";
945
+ };
946
+ //#endregion
921
947
  //#region src/report.ts
922
948
  function registryColumnName(fieldName, field) {
923
949
  return validateColumnName(field?.columnName ?? field?._meta?.columnName ?? toSnakeCase(fieldName));
@@ -995,6 +1021,971 @@ var SmrtReportCollection = class extends SmrtCollection {
995
1021
  }
996
1022
  };
997
1023
  //#endregion
1024
+ //#region src/runtime-spec.ts
1025
+ var RUNTIME_REPORT_SPEC_VERSION = 1;
1026
+ var RUNTIME_REPORT_LIMITS = Object.freeze({
1027
+ maxTitleLength: 120,
1028
+ maxDescriptionLength: 500,
1029
+ maxDimensions: 4,
1030
+ maxMeasures: 6,
1031
+ maxFilters: 12,
1032
+ maxHaving: 4,
1033
+ maxSort: 3,
1034
+ maxInValues: 100,
1035
+ maxStringValueLength: 256,
1036
+ defaultRowLimit: 100,
1037
+ maxRowLimit: 1e3,
1038
+ maxAliasLength: 40,
1039
+ maxFieldNameLength: 64,
1040
+ maxSourceIdLength: 120
1041
+ });
1042
+ var RUNTIME_REPORT_BUCKETS = [
1043
+ "minute",
1044
+ "hour",
1045
+ "day",
1046
+ "week",
1047
+ "month",
1048
+ "quarter",
1049
+ "year"
1050
+ ];
1051
+ var RUNTIME_REPORT_MEASURE_FNS = [
1052
+ "count",
1053
+ "countDistinct",
1054
+ "sum",
1055
+ "avg",
1056
+ "min",
1057
+ "max"
1058
+ ];
1059
+ var RUNTIME_REPORT_FILTER_OPS = [
1060
+ "eq",
1061
+ "ne",
1062
+ "gt",
1063
+ "gte",
1064
+ "lt",
1065
+ "lte",
1066
+ "in",
1067
+ "notIn",
1068
+ "contains",
1069
+ "isNull",
1070
+ "isNotNull"
1071
+ ];
1072
+ var RUNTIME_REPORT_HAVING_OPS = [
1073
+ "eq",
1074
+ "ne",
1075
+ "gt",
1076
+ "gte",
1077
+ "lt",
1078
+ "lte"
1079
+ ];
1080
+ var RUNTIME_REPORT_CHART_TYPES = [
1081
+ "table",
1082
+ "bar",
1083
+ "line",
1084
+ "pie",
1085
+ "stat"
1086
+ ];
1087
+ var RuntimeReportError = class extends Error {
1088
+ code;
1089
+ path;
1090
+ /**
1091
+ * HTTP-style status so tool loops classify it: 4xx input problems are
1092
+ * repairable by the caller, `tenant_required` and `confirmation_required`
1093
+ * are denials, results that cannot be represented are server errors.
1094
+ */
1095
+ status;
1096
+ constructor(code, path, message) {
1097
+ super(path ? `${path}: ${message}` : message);
1098
+ this.name = "RuntimeReportError";
1099
+ this.code = code;
1100
+ this.path = path;
1101
+ this.status = code === "tenant_required" || code === "confirmation_required" ? 403 : code === "invalid_result" ? 500 : 422;
1102
+ }
1103
+ };
1104
+ function fail$1(path, message) {
1105
+ throw new RuntimeReportError("invalid_spec", path, message);
1106
+ }
1107
+ var RESERVED_ALIASES = /* @__PURE__ */ new Set([
1108
+ "all",
1109
+ "and",
1110
+ "as",
1111
+ "asc",
1112
+ "between",
1113
+ "by",
1114
+ "case",
1115
+ "cast",
1116
+ "check",
1117
+ "column",
1118
+ "constraint",
1119
+ "create",
1120
+ "cross",
1121
+ "default",
1122
+ "delete",
1123
+ "desc",
1124
+ "distinct",
1125
+ "drop",
1126
+ "else",
1127
+ "end",
1128
+ "except",
1129
+ "exists",
1130
+ "false",
1131
+ "for",
1132
+ "foreign",
1133
+ "from",
1134
+ "full",
1135
+ "group",
1136
+ "having",
1137
+ "in",
1138
+ "inner",
1139
+ "insert",
1140
+ "intersect",
1141
+ "into",
1142
+ "is",
1143
+ "join",
1144
+ "left",
1145
+ "like",
1146
+ "limit",
1147
+ "natural",
1148
+ "not",
1149
+ "null",
1150
+ "offset",
1151
+ "on",
1152
+ "or",
1153
+ "order",
1154
+ "outer",
1155
+ "primary",
1156
+ "references",
1157
+ "right",
1158
+ "select",
1159
+ "set",
1160
+ "table",
1161
+ "then",
1162
+ "to",
1163
+ "true",
1164
+ "union",
1165
+ "unique",
1166
+ "update",
1167
+ "user",
1168
+ "using",
1169
+ "values",
1170
+ "when",
1171
+ "where",
1172
+ "with"
1173
+ ]);
1174
+ var ALIAS_PATTERN = /^[a-z][a-z0-9_]*$/;
1175
+ var FIELD_PATTERN = /^[A-Za-z][A-Za-z0-9_]*$/;
1176
+ var SOURCE_PATTERN = /^[A-Za-z@][A-Za-z0-9_@./:-]*$/;
1177
+ function isRecord(value) {
1178
+ return typeof value === "object" && value !== null && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
1179
+ }
1180
+ function strictObject(value, path, allowed) {
1181
+ if (!isRecord(value)) fail$1(path, "must be an object");
1182
+ for (const key of Object.keys(value)) if (!allowed.includes(key)) fail$1(`${path}.${key}`, "is not a known key");
1183
+ return value;
1184
+ }
1185
+ function strictArray(value, path, max, min = 0) {
1186
+ if (value === void 0) return [];
1187
+ if (!Array.isArray(value)) fail$1(path, "must be an array");
1188
+ if (value.length > max) fail$1(path, `must have at most ${max} entries`);
1189
+ if (value.length < min) fail$1(path, `must have at least ${min} entries`);
1190
+ return value;
1191
+ }
1192
+ function text(value, path, max, options = {}) {
1193
+ if (typeof value !== "string") {
1194
+ if (options.optional && value === void 0) return "";
1195
+ fail$1(path, "must be a string");
1196
+ }
1197
+ const trimmed = value.trim();
1198
+ if (trimmed.length === 0 && !options.optional) fail$1(path, "must not be empty");
1199
+ if (trimmed.length > max) fail$1(path, `must be at most ${max} characters`);
1200
+ if (/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(trimmed)) fail$1(path, "must not contain control characters");
1201
+ return trimmed;
1202
+ }
1203
+ function oneOf(value, path, allowed) {
1204
+ if (typeof value !== "string" || !allowed.includes(value)) fail$1(path, `must be one of: ${allowed.join(", ")}`);
1205
+ return value;
1206
+ }
1207
+ function fieldName(value, path) {
1208
+ if (typeof value !== "string") fail$1(path, "must be a string");
1209
+ if (value.length === 0 || value.length > RUNTIME_REPORT_LIMITS.maxFieldNameLength || !FIELD_PATTERN.test(value)) fail$1(path, "must be a plain field name (letters, digits, underscore)");
1210
+ return value;
1211
+ }
1212
+ function alias(value, path) {
1213
+ if (typeof value !== "string") fail$1(path, "must be a string");
1214
+ if (value.length === 0 || value.length > RUNTIME_REPORT_LIMITS.maxAliasLength || !ALIAS_PATTERN.test(value)) fail$1(path, `must be a lowercase identifier of at most ${RUNTIME_REPORT_LIMITS.maxAliasLength} characters (a-z, 0-9, underscore)`);
1215
+ if (RESERVED_ALIASES.has(value)) fail$1(path, `'${value}' is a reserved word`);
1216
+ return value;
1217
+ }
1218
+ function scalar(value, path) {
1219
+ if (typeof value === "string") {
1220
+ if (value.length > RUNTIME_REPORT_LIMITS.maxStringValueLength) fail$1(path, `must be at most ${RUNTIME_REPORT_LIMITS.maxStringValueLength} characters`);
1221
+ if (value.includes("\0")) fail$1(path, "must not contain NUL");
1222
+ return value;
1223
+ }
1224
+ if (typeof value === "number") {
1225
+ if (!Number.isFinite(value)) fail$1(path, "must be a finite number");
1226
+ return value;
1227
+ }
1228
+ if (typeof value === "boolean") return value;
1229
+ return fail$1(path, "must be a string, number or boolean");
1230
+ }
1231
+ function snake(name) {
1232
+ return name.replace(/([a-z\d])([A-Z])/g, "$1_$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").toLowerCase();
1233
+ }
1234
+ function parseDimension(value, path) {
1235
+ const raw = strictObject(value, path, [
1236
+ "field",
1237
+ "as",
1238
+ "bucket"
1239
+ ]);
1240
+ const field = fieldName(raw.field, `${path}.field`);
1241
+ const bucket = raw.bucket === void 0 ? void 0 : oneOf(raw.bucket, `${path}.bucket`, RUNTIME_REPORT_BUCKETS);
1242
+ return {
1243
+ field,
1244
+ as: raw.as === void 0 ? alias(bucket ? `${snake(field)}_${bucket}` : snake(field), `${path}.as`) : alias(raw.as, `${path}.as`),
1245
+ ...bucket ? { bucket } : {}
1246
+ };
1247
+ }
1248
+ function parseMeasure(value, path) {
1249
+ const raw = strictObject(value, path, [
1250
+ "fn",
1251
+ "field",
1252
+ "as"
1253
+ ]);
1254
+ const fn = oneOf(raw.fn, `${path}.fn`, RUNTIME_REPORT_MEASURE_FNS);
1255
+ const field = raw.field === void 0 ? void 0 : fieldName(raw.field, `${path}.field`);
1256
+ if (!field && fn !== "count") fail$1(`${path}.field`, `is required for ${fn}`);
1257
+ const as = raw.as === void 0 ? alias(field ? `${snake(fn)}_${snake(field)}` : "row_count", `${path}.as`) : alias(raw.as, `${path}.as`);
1258
+ return {
1259
+ fn,
1260
+ ...field ? { field } : {},
1261
+ as
1262
+ };
1263
+ }
1264
+ function parseFilter(value, path) {
1265
+ const raw = strictObject(value, path, [
1266
+ "field",
1267
+ "op",
1268
+ "value"
1269
+ ]);
1270
+ const field = fieldName(raw.field, `${path}.field`);
1271
+ const op = oneOf(raw.op, `${path}.op`, RUNTIME_REPORT_FILTER_OPS);
1272
+ if (op === "isNull" || op === "isNotNull") {
1273
+ if (raw.value !== void 0) fail$1(`${path}.value`, `must be omitted for ${op}`);
1274
+ return {
1275
+ field,
1276
+ op
1277
+ };
1278
+ }
1279
+ if (raw.value === void 0) fail$1(`${path}.value`, `is required for ${op}`);
1280
+ if (op === "in" || op === "notIn") return {
1281
+ field,
1282
+ op,
1283
+ value: strictArray(raw.value, `${path}.value`, RUNTIME_REPORT_LIMITS.maxInValues, 1).map((entry, index) => scalar(entry, `${path}.value[${index}]`))
1284
+ };
1285
+ if (Array.isArray(raw.value)) fail$1(`${path}.value`, `must be a scalar for ${op}`);
1286
+ const parsed = scalar(raw.value, `${path}.value`);
1287
+ if (op === "contains" && typeof parsed !== "string") fail$1(`${path}.value`, "must be a string for contains");
1288
+ return {
1289
+ field,
1290
+ op,
1291
+ value: parsed
1292
+ };
1293
+ }
1294
+ function parseHaving(value, path) {
1295
+ const raw = strictObject(value, path, [
1296
+ "measure",
1297
+ "op",
1298
+ "value"
1299
+ ]);
1300
+ const measure = alias(raw.measure, `${path}.measure`);
1301
+ const op = oneOf(raw.op, `${path}.op`, RUNTIME_REPORT_HAVING_OPS);
1302
+ if (typeof raw.value !== "number" || !Number.isFinite(raw.value)) fail$1(`${path}.value`, "must be a finite number");
1303
+ return {
1304
+ measure,
1305
+ op,
1306
+ value: raw.value
1307
+ };
1308
+ }
1309
+ function parseSort(value, path) {
1310
+ const raw = strictObject(value, path, ["by", "direction"]);
1311
+ return {
1312
+ by: alias(raw.by, `${path}.by`),
1313
+ direction: raw.direction === void 0 ? "asc" : oneOf(raw.direction, `${path}.direction`, ["asc", "desc"])
1314
+ };
1315
+ }
1316
+ function parseRuntimeReportSpec(input) {
1317
+ let candidate = input;
1318
+ if (typeof input === "string") {
1319
+ if (input.length > 64 * 1024) fail$1("", "spec JSON is too large");
1320
+ try {
1321
+ candidate = JSON.parse(input);
1322
+ } catch {
1323
+ fail$1("", "spec is not valid JSON");
1324
+ }
1325
+ }
1326
+ const raw = strictObject(candidate, "spec", [
1327
+ "version",
1328
+ "title",
1329
+ "description",
1330
+ "source",
1331
+ "dimensions",
1332
+ "measures",
1333
+ "filters",
1334
+ "having",
1335
+ "sort",
1336
+ "limit",
1337
+ "chart"
1338
+ ]);
1339
+ if (raw.version !== void 0 && raw.version !== 1) fail$1("spec.version", `unsupported version (expected 1)`);
1340
+ const title = text(raw.title, "spec.title", RUNTIME_REPORT_LIMITS.maxTitleLength);
1341
+ const description = text(raw.description, "spec.description", RUNTIME_REPORT_LIMITS.maxDescriptionLength, { optional: true });
1342
+ if (typeof raw.source !== "string" || raw.source.length === 0 || raw.source.length > RUNTIME_REPORT_LIMITS.maxSourceIdLength || !SOURCE_PATTERN.test(raw.source)) fail$1("spec.source", "must be a source id");
1343
+ const dimensions = strictArray(raw.dimensions, "spec.dimensions", RUNTIME_REPORT_LIMITS.maxDimensions).map((entry, index) => parseDimension(entry, `spec.dimensions[${index}]`));
1344
+ const measures = strictArray(raw.measures, "spec.measures", RUNTIME_REPORT_LIMITS.maxMeasures, 1).map((entry, index) => parseMeasure(entry, `spec.measures[${index}]`));
1345
+ const filters = strictArray(raw.filters, "spec.filters", RUNTIME_REPORT_LIMITS.maxFilters).map((entry, index) => parseFilter(entry, `spec.filters[${index}]`));
1346
+ const having = strictArray(raw.having, "spec.having", RUNTIME_REPORT_LIMITS.maxHaving).map((entry, index) => parseHaving(entry, `spec.having[${index}]`));
1347
+ const sort = strictArray(raw.sort, "spec.sort", RUNTIME_REPORT_LIMITS.maxSort).map((entry, index) => parseSort(entry, `spec.sort[${index}]`));
1348
+ const dimensionAliases = /* @__PURE__ */ new Set();
1349
+ const measureAliases = /* @__PURE__ */ new Set();
1350
+ const seen = /* @__PURE__ */ new Set();
1351
+ for (const [index, dimension] of dimensions.entries()) {
1352
+ if (seen.has(dimension.as)) fail$1(`spec.dimensions[${index}].as`, `duplicate alias '${dimension.as}'`);
1353
+ seen.add(dimension.as);
1354
+ dimensionAliases.add(dimension.as);
1355
+ }
1356
+ for (const [index, measure] of measures.entries()) {
1357
+ if (seen.has(measure.as)) fail$1(`spec.measures[${index}].as`, `duplicate alias '${measure.as}'`);
1358
+ seen.add(measure.as);
1359
+ measureAliases.add(measure.as);
1360
+ }
1361
+ for (const [index, entry] of having.entries()) if (!measureAliases.has(entry.measure)) fail$1(`spec.having[${index}].measure`, "must be a declared measure alias");
1362
+ for (const [index, entry] of sort.entries()) if (!seen.has(entry.by)) fail$1(`spec.sort[${index}].by`, "must be a declared dimension or measure alias");
1363
+ let limit = RUNTIME_REPORT_LIMITS.defaultRowLimit;
1364
+ if (raw.limit !== void 0) {
1365
+ if (typeof raw.limit !== "number" || !Number.isSafeInteger(raw.limit) || raw.limit < 1 || raw.limit > RUNTIME_REPORT_LIMITS.maxRowLimit) fail$1("spec.limit", `must be an integer between 1 and ${RUNTIME_REPORT_LIMITS.maxRowLimit}`);
1366
+ limit = raw.limit;
1367
+ }
1368
+ let chart = { type: "table" };
1369
+ if (raw.chart !== void 0) {
1370
+ const rawChart = strictObject(raw.chart, "spec.chart", [
1371
+ "type",
1372
+ "x",
1373
+ "y"
1374
+ ]);
1375
+ const type = oneOf(rawChart.type, "spec.chart.type", RUNTIME_REPORT_CHART_TYPES);
1376
+ const x = rawChart.x === void 0 ? void 0 : alias(rawChart.x, "spec.chart.x");
1377
+ if (x !== void 0 && !dimensionAliases.has(x)) fail$1("spec.chart.x", "must be a declared dimension alias");
1378
+ const y = strictArray(rawChart.y, "spec.chart.y", RUNTIME_REPORT_LIMITS.maxMeasures).map((entry, index) => alias(entry, `spec.chart.y[${index}]`));
1379
+ for (const [index, name] of y.entries()) if (!measureAliases.has(name)) fail$1(`spec.chart.y[${index}]`, "must be a declared measure alias");
1380
+ chart = {
1381
+ type,
1382
+ ...x ? { x } : {},
1383
+ ...y.length > 0 ? { y } : {}
1384
+ };
1385
+ }
1386
+ return {
1387
+ version: 1,
1388
+ title,
1389
+ ...description ? { description } : {},
1390
+ source: raw.source,
1391
+ dimensions,
1392
+ measures,
1393
+ filters,
1394
+ having,
1395
+ sort,
1396
+ limit,
1397
+ chart
1398
+ };
1399
+ }
1400
+ function canonicalize(value) {
1401
+ if (Array.isArray(value)) return value.map(canonicalize);
1402
+ if (isRecord(value)) return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalize(value[key])]));
1403
+ return value;
1404
+ }
1405
+ function runtimeReportSpecHash(spec) {
1406
+ return createHash("sha256").update(JSON.stringify(canonicalize(spec))).digest("hex");
1407
+ }
1408
+ function serializeRuntimeReportSpec(spec) {
1409
+ return JSON.stringify(canonicalize(spec));
1410
+ }
1411
+ //#endregion
1412
+ //#region src/runtime-compiler.ts
1413
+ var ALIAS_PREFIX = "r_";
1414
+ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1415
+ function humanize(name) {
1416
+ return name.replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").replace(/^./, (value) => value.toUpperCase());
1417
+ }
1418
+ function columnTypeFor(type) {
1419
+ switch (type) {
1420
+ case "text": return "string";
1421
+ case "integer": return "integer";
1422
+ case "decimal": return "decimal";
1423
+ case "boolean": return "boolean";
1424
+ case "datetime": return "datetime";
1425
+ case "foreignKey":
1426
+ case "crossPackageRef": return "id";
1427
+ default: return null;
1428
+ }
1429
+ }
1430
+ function explicitColumn(field) {
1431
+ const metaColumn = field._meta?.columnName;
1432
+ if (typeof field.columnName === "string") return field.columnName;
1433
+ return typeof metaColumn === "string" ? metaColumn : void 0;
1434
+ }
1435
+ function strictFormat(field) {
1436
+ return [field.format, field._meta?.format].find((value) => typeof value === "string" && value.length > 0);
1437
+ }
1438
+ var BASE_FIELDS = {
1439
+ id: "id",
1440
+ slug: "string",
1441
+ createdAt: "datetime",
1442
+ updatedAt: "datetime"
1443
+ };
1444
+ function isExcluded(name, field, tenantField, permissions) {
1445
+ if (name.startsWith("_")) return true;
1446
+ const meta = field._meta ?? {};
1447
+ if (field.transient === true || meta.transient === true) return true;
1448
+ const sensitivity = [field.sensitivity, meta.sensitivity].find((value) => typeof value === "string");
1449
+ if (field.sensitive === true || meta.sensitive === true || sensitivity === "sensitive" || sensitivity === "secret") return true;
1450
+ const readPermission = typeof field.readPermission === "string" ? field.readPermission : typeof meta.readPermission === "string" ? meta.readPermission : void 0;
1451
+ if (readPermission && !permissions.has(readPermission)) return true;
1452
+ if (meta.__tenancy?.isTenantIdField === true) return true;
1453
+ if (name === "tenantId" || name === "tenant_id") return true;
1454
+ if (tenantField && toSnakeCase(name) === tenantField) return true;
1455
+ return false;
1456
+ }
1457
+ async function loadSourceFields(qualifiedName, tenantField, permissions) {
1458
+ const registered = await ObjectRegistry.getAllFields(qualifiedName);
1459
+ const fields = /* @__PURE__ */ new Map();
1460
+ for (const [name, field] of registered) {
1461
+ if (isExcluded(name, field, tenantField, permissions)) continue;
1462
+ const type = columnTypeFor(field.type);
1463
+ if (!type) continue;
1464
+ const format = strictFormat(field);
1465
+ fields.set(name, {
1466
+ name,
1467
+ column: toSnakeCase(explicitColumn(field) ?? name),
1468
+ type,
1469
+ ...format ? { format } : {},
1470
+ label: typeof field.description === "string" && field.description.length > 0 ? field.description : humanize(name)
1471
+ });
1472
+ }
1473
+ for (const [name, type] of Object.entries(BASE_FIELDS)) {
1474
+ if (fields.has(name) || registered.has(name)) continue;
1475
+ fields.set(name, {
1476
+ name,
1477
+ column: toSnakeCase(name),
1478
+ type,
1479
+ label: humanize(name)
1480
+ });
1481
+ }
1482
+ return fields;
1483
+ }
1484
+ function unknownField(path, name) {
1485
+ throw new RuntimeReportError("unknown_field", path, `'${name}' is not a field available on this source`);
1486
+ }
1487
+ function badOperation(path, message) {
1488
+ throw new RuntimeReportError("invalid_operation", path, message);
1489
+ }
1490
+ function badValue(path, message) {
1491
+ throw new RuntimeReportError("invalid_value", path, message);
1492
+ }
1493
+ var OPERATORS_BY_TYPE = {
1494
+ string: [
1495
+ "eq",
1496
+ "ne",
1497
+ "gt",
1498
+ "gte",
1499
+ "lt",
1500
+ "lte",
1501
+ "in",
1502
+ "notIn",
1503
+ "contains",
1504
+ "isNull",
1505
+ "isNotNull"
1506
+ ],
1507
+ integer: [
1508
+ "eq",
1509
+ "ne",
1510
+ "gt",
1511
+ "gte",
1512
+ "lt",
1513
+ "lte",
1514
+ "in",
1515
+ "notIn",
1516
+ "isNull",
1517
+ "isNotNull"
1518
+ ],
1519
+ decimal: [
1520
+ "eq",
1521
+ "ne",
1522
+ "gt",
1523
+ "gte",
1524
+ "lt",
1525
+ "lte",
1526
+ "in",
1527
+ "notIn",
1528
+ "isNull",
1529
+ "isNotNull"
1530
+ ],
1531
+ datetime: [
1532
+ "eq",
1533
+ "ne",
1534
+ "gt",
1535
+ "gte",
1536
+ "lt",
1537
+ "lte",
1538
+ "in",
1539
+ "notIn",
1540
+ "isNull",
1541
+ "isNotNull"
1542
+ ],
1543
+ boolean: [
1544
+ "eq",
1545
+ "ne",
1546
+ "isNull",
1547
+ "isNotNull"
1548
+ ],
1549
+ id: [
1550
+ "eq",
1551
+ "ne",
1552
+ "in",
1553
+ "notIn",
1554
+ "isNull",
1555
+ "isNotNull"
1556
+ ]
1557
+ };
1558
+ function coerceValue(type, value, path) {
1559
+ switch (type) {
1560
+ case "string":
1561
+ if (typeof value !== "string") badValue(path, "must be a string");
1562
+ return value;
1563
+ case "integer":
1564
+ if (typeof value !== "number" || !Number.isSafeInteger(value)) badValue(path, "must be a safe integer");
1565
+ return value;
1566
+ case "decimal":
1567
+ if (typeof value !== "number" || !Number.isFinite(value)) badValue(path, "must be a finite number");
1568
+ return value;
1569
+ case "boolean":
1570
+ if (typeof value !== "boolean") badValue(path, "must be true or false");
1571
+ return value;
1572
+ case "datetime": {
1573
+ if (typeof value !== "string") badValue(path, "must be an ISO-8601 string");
1574
+ const parsed = new Date(value);
1575
+ if (Number.isNaN(parsed.getTime()) || !/^\d{4}-\d{2}-\d{2}/.test(value)) badValue(path, "must be an ISO-8601 date or timestamp");
1576
+ return parsed;
1577
+ }
1578
+ case "id":
1579
+ if (typeof value !== "string" || !UUID_PATTERN.test(value)) badValue(path, "must be a UUID");
1580
+ return value.toLowerCase();
1581
+ }
1582
+ }
1583
+ function filterCondition(filter, field, path) {
1584
+ if (!OPERATORS_BY_TYPE[field.type].includes(filter.op)) badOperation(`${path}.op`, `'${filter.op}' is not valid for ${field.type} fields`);
1585
+ const column = field.column;
1586
+ switch (filter.op) {
1587
+ case "isNull": return { [column]: null };
1588
+ case "isNotNull": return { [`${column} !=`]: null };
1589
+ case "in":
1590
+ case "notIn": {
1591
+ const values = filter.value.map((entry, index) => coerceValue(field.type, entry, `${path}.value[${index}]`));
1592
+ return { [`${column} ${filter.op === "in" ? "in" : "not in"}`]: values };
1593
+ }
1594
+ case "contains": return { [`${column} contains`]: coerceValue(field.type, filter.value, `${path}.value`) };
1595
+ default: return { [`${column}${{
1596
+ eq: "",
1597
+ ne: " !=",
1598
+ gt: " >",
1599
+ gte: " >=",
1600
+ lt: " <",
1601
+ lte: " <="
1602
+ }[filter.op]}`]: coerceValue(field.type, filter.value, `${path}.value`) };
1603
+ }
1604
+ }
1605
+ function measureColumnType(fn, field) {
1606
+ if (fn === "count" || fn === "countDistinct") return "integer";
1607
+ if (fn === "avg") return "decimal";
1608
+ return field?.type ?? "decimal";
1609
+ }
1610
+ function resolveSource(context, id) {
1611
+ const source = context.sources.find((candidate) => candidate.id === id);
1612
+ if (!source) throw new RuntimeReportError("unknown_source", "spec.source", `'${id}' is not an available report source`);
1613
+ return source;
1614
+ }
1615
+ async function prepareSource(context, id) {
1616
+ const source = resolveSource(context, id);
1617
+ await context.authorizeSource?.(source);
1618
+ const registered = ObjectRegistry.getClass(source.className);
1619
+ if (!registered) throw new RuntimeReportError("unknown_source", "spec.source", `'${id}' is not an available report source`);
1620
+ const qualifiedName = registered.qualifiedName ?? registered.name;
1621
+ const table = ObjectRegistry.getTableName(qualifiedName);
1622
+ if (!table) throw new RuntimeReportError("unsupported_source", "spec.source", "this source has no registered table");
1623
+ const tenantCol = await tenantColumn(qualifiedName);
1624
+ if (isTenantScopedClass(qualifiedName) && !tenantCol) throw new RuntimeReportError("unsupported_source", "spec.source", "this tenant-scoped source does not expose a tenant column");
1625
+ const fields = await loadSourceFields(qualifiedName, tenantCol, new Set(context.permissions ?? []));
1626
+ return {
1627
+ source,
1628
+ registeredName: registered.name,
1629
+ qualifiedName,
1630
+ table,
1631
+ tenantCol,
1632
+ fields
1633
+ };
1634
+ }
1635
+ async function describeRuntimeReportSource(context, sourceId) {
1636
+ const prepared = await prepareSource(context, sourceId);
1637
+ return {
1638
+ id: prepared.source.id,
1639
+ label: prepared.source.label ?? humanize(prepared.registeredName),
1640
+ ...prepared.source.description ? { description: prepared.source.description } : {},
1641
+ fields: [...prepared.fields.values()].map((field) => ({
1642
+ name: field.name,
1643
+ label: field.label,
1644
+ type: field.type,
1645
+ ...field.format ? { format: field.format } : {},
1646
+ filterOperators: OPERATORS_BY_TYPE[field.type],
1647
+ aggregates: aggregatesFor(field.type)
1648
+ }))
1649
+ };
1650
+ }
1651
+ function aggregatesFor(type) {
1652
+ if (type === "integer" || type === "decimal") return [
1653
+ "count",
1654
+ "countDistinct",
1655
+ "sum",
1656
+ "avg",
1657
+ "min",
1658
+ "max"
1659
+ ];
1660
+ if (type === "datetime") return [
1661
+ "count",
1662
+ "countDistinct",
1663
+ "min",
1664
+ "max"
1665
+ ];
1666
+ return ["count", "countDistinct"];
1667
+ }
1668
+ async function compileRuntimeReportSpec(spec, context) {
1669
+ const { source, registeredName, qualifiedName, table, tenantCol, fields } = await prepareSource(context, spec.source);
1670
+ const lookup = (name, path) => {
1671
+ const field = fields.get(name);
1672
+ if (!field) unknownField(path, name);
1673
+ return field;
1674
+ };
1675
+ const columns = [];
1676
+ const select = [];
1677
+ const groupBy = [];
1678
+ for (const [index, dimension] of spec.dimensions.entries()) {
1679
+ const path = `spec.dimensions[${index}]`;
1680
+ const field = lookup(dimension.field, `${path}.field`);
1681
+ if (dimension.bucket && field.type !== "datetime") badOperation(`${path}.bucket`, "buckets are only valid on datetime fields");
1682
+ const sqlAlias = `${ALIAS_PREFIX}${dimension.as}`;
1683
+ if (dimension.bucket) select.push({
1684
+ bucket: dimension.bucket,
1685
+ column: field.column,
1686
+ as: sqlAlias
1687
+ });
1688
+ else select.push({
1689
+ column: field.column,
1690
+ as: sqlAlias
1691
+ });
1692
+ groupBy.push(sqlAlias);
1693
+ columns.push({
1694
+ id: dimension.as,
1695
+ label: humanize(dimension.as),
1696
+ role: "dimension",
1697
+ type: dimension.bucket ? "datetime" : field.type,
1698
+ field: field.name,
1699
+ ...dimension.bucket ? { bucket: dimension.bucket } : {},
1700
+ ...field.format && !dimension.bucket ? { format: field.format } : {}
1701
+ });
1702
+ }
1703
+ for (const [index, measure] of spec.measures.entries()) {
1704
+ const path = `spec.measures[${index}]`;
1705
+ const field = measure.field ? lookup(measure.field, `${path}.field`) : void 0;
1706
+ if ((measure.fn === "sum" || measure.fn === "avg") && field?.type !== "integer" && field?.type !== "decimal") badOperation(`${path}.fn`, `${measure.fn} requires a numeric field`);
1707
+ if ((measure.fn === "min" || measure.fn === "max") && field?.type !== "integer" && field?.type !== "decimal" && field?.type !== "datetime") badOperation(`${path}.fn`, `${measure.fn} requires a numeric or datetime field`);
1708
+ const sqlAlias = `${ALIAS_PREFIX}${measure.as}`;
1709
+ select.push({
1710
+ fn: measure.fn === "countDistinct" ? "count" : measure.fn,
1711
+ ...field ? { column: field.column } : {},
1712
+ as: sqlAlias,
1713
+ ...measure.fn === "countDistinct" ? { distinct: true } : {}
1714
+ });
1715
+ const keepsFormat = field?.format && (measure.fn === "sum" || measure.fn === "min" || measure.fn === "max" || measure.fn === "avg");
1716
+ columns.push({
1717
+ id: measure.as,
1718
+ label: humanize(measure.as),
1719
+ role: "measure",
1720
+ type: measureColumnType(measure.fn, field),
1721
+ ...field ? { field: field.name } : {},
1722
+ fn: measure.fn,
1723
+ ...keepsFormat ? { format: field?.format } : {}
1724
+ });
1725
+ }
1726
+ const conditions = [];
1727
+ const tenantId = context.tenantId !== void 0 ? context.tenantId : getTenantId() ?? null;
1728
+ if (tenantCol) {
1729
+ if (tenantId) conditions.push({ [tenantCol]: tenantId });
1730
+ else if (isTenancyEnabled() || isTenantScopedClass(qualifiedName)) conditions.push({ [tenantCol]: null });
1731
+ }
1732
+ if ((await ObjectRegistry.getAllFields(qualifiedName)).has("deletedAt")) conditions.push({ deleted_at: null });
1733
+ if (ObjectRegistry.getTableStrategy(qualifiedName) === "sti") {
1734
+ const base = ObjectRegistry.getSTIBase(qualifiedName);
1735
+ if (base !== null && base !== qualifiedName) conditions.push({ _meta_type: qualifiedName });
1736
+ }
1737
+ for (const [index, filter] of spec.filters.entries()) {
1738
+ const path = `spec.filters[${index}]`;
1739
+ const field = lookup(filter.field, `${path}.field`);
1740
+ conditions.push(filterCondition(filter, field, path));
1741
+ }
1742
+ const having = spec.having.map((entry) => {
1743
+ const suffix = {
1744
+ eq: "",
1745
+ ne: " !=",
1746
+ gt: " >",
1747
+ gte: " >=",
1748
+ lt: " <",
1749
+ lte: " <="
1750
+ }[entry.op];
1751
+ return { [`${ALIAS_PREFIX}${entry.measure}${suffix}`]: entry.value };
1752
+ });
1753
+ const orderBy = spec.sort.map((entry) => `${ALIAS_PREFIX}${entry.by} ${entry.direction === "desc" ? "DESC" : "ASC"}`);
1754
+ if (orderBy.length === 0 && spec.dimensions.length > 0) orderBy.push(`${ALIAS_PREFIX}${spec.dimensions[0].as} ASC`);
1755
+ const aggregate = {
1756
+ from: table,
1757
+ select,
1758
+ groupBy,
1759
+ ...conditions.length > 0 ? { where: [conditions] } : {},
1760
+ ...having.length > 0 ? { having: [having] } : {},
1761
+ ...orderBy.length > 0 ? { orderBy } : {},
1762
+ limit: Math.min(spec.limit, RUNTIME_REPORT_LIMITS.maxRowLimit) + 1
1763
+ };
1764
+ return {
1765
+ version: 1,
1766
+ specHash: runtimeReportSpecHash(spec),
1767
+ spec,
1768
+ source: {
1769
+ id: source.id,
1770
+ className: qualifiedName,
1771
+ label: source.label ?? humanize(registeredName),
1772
+ table
1773
+ },
1774
+ columns,
1775
+ aggregate,
1776
+ limit: spec.limit,
1777
+ tenant: {
1778
+ scoped: Boolean(tenantCol),
1779
+ tenantId: tenantCol ? tenantId : null
1780
+ }
1781
+ };
1782
+ }
1783
+ function adaptWhere(where, adapterType) {
1784
+ if (!where || !Array.isArray(where)) return where;
1785
+ const adapt = (value) => adapterType === "sqlite" && typeof value === "boolean" ? value ? 1 : 0 : value;
1786
+ return where.map((group) => group.map((condition) => Object.fromEntries(Object.entries(condition).map(([key, value]) => [key, Array.isArray(value) ? value.map(adapt) : adapt(value)]))));
1787
+ }
1788
+ function normalizeCell(column, value) {
1789
+ if (value === null || value === void 0) return null;
1790
+ if (value instanceof Date) return value.toISOString();
1791
+ if (typeof value === "bigint") {
1792
+ const asNumber = Number(value);
1793
+ if (!Number.isSafeInteger(asNumber)) throw new RuntimeReportError("invalid_result", column.id, "integer result exceeds the JavaScript safe range");
1794
+ return asNumber;
1795
+ }
1796
+ if (column.type === "boolean") {
1797
+ if (value === 1 || value === "1" || value === "true" || value === true) return true;
1798
+ if (value === 0 || value === "0" || value === "false" || value === false) return false;
1799
+ }
1800
+ if (column.type === "integer" || column.type === "decimal") {
1801
+ const numeric = typeof value === "number" ? value : Number(value);
1802
+ if (!Number.isFinite(numeric)) throw new RuntimeReportError("invalid_result", column.id, "numeric result is not finite");
1803
+ if (column.type === "integer" && !Number.isSafeInteger(numeric)) throw new RuntimeReportError("invalid_result", column.id, "integer result exceeds the JavaScript safe range");
1804
+ return numeric;
1805
+ }
1806
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
1807
+ return String(value);
1808
+ }
1809
+ async function runRuntimeReport(compiled, options) {
1810
+ const adapterType = adapterTypeFromDb(options.db, options.adapterType);
1811
+ const aggregate = buildAggregate({
1812
+ ...compiled.aggregate,
1813
+ where: adaptWhere(compiled.aggregate.where, adapterType)
1814
+ }, 1, adapterType);
1815
+ const rawRows = (await options.db.query(aggregate.sql, ...aggregate.values)).rows ?? [];
1816
+ const limit = Math.min(compiled.limit, options.maxRows ?? RUNTIME_REPORT_LIMITS.maxRowLimit);
1817
+ const truncated = rawRows.length > limit;
1818
+ const rows = rawRows.slice(0, limit).map((row) => {
1819
+ const out = {};
1820
+ for (const column of compiled.columns) out[column.id] = normalizeCell(column, row[`${ALIAS_PREFIX}${column.id}`]);
1821
+ return out;
1822
+ });
1823
+ return {
1824
+ version: 1,
1825
+ specHash: compiled.specHash,
1826
+ title: compiled.spec.title,
1827
+ columns: compiled.columns,
1828
+ rows,
1829
+ rowCount: rows.length,
1830
+ truncated,
1831
+ ranAt: (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString(),
1832
+ chart: compiled.spec.chart
1833
+ };
1834
+ }
1835
+ //#endregion
1836
+ //#region src/runtime-report.ts
1837
+ var __defProp = Object.defineProperty;
1838
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
1839
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, {
1840
+ enumerable: true,
1841
+ configurable: true,
1842
+ writable: true,
1843
+ value
1844
+ }) : obj[key] = value;
1845
+ var __decorateClass = (decorators, target, key, kind) => {
1846
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
1847
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
1848
+ if (kind && result) __defProp(target, key, result);
1849
+ return result;
1850
+ };
1851
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
1852
+ var CONFIRMED_SAVE = /* @__PURE__ */ Symbol("smrt-reports.runtimeReport.confirmedSave");
1853
+ var confirmedSaves = /* @__PURE__ */ new WeakMap();
1854
+ var RuntimeReport = class extends SmrtObject {
1855
+ tenantId = "";
1856
+ title = "";
1857
+ description = "";
1858
+ sourceId = "";
1859
+ spec = "";
1860
+ specHash = "";
1861
+ status = "active";
1862
+ createdByUserId = "";
1863
+ constructor(options = {}) {
1864
+ super(options);
1865
+ const proof = options[CONFIRMED_SAVE];
1866
+ if (typeof proof === "string") confirmedSaves.set(this, proof);
1867
+ }
1868
+ /** Re-parse the stored JSON; never trust the column without validation. */
1869
+ getSpec() {
1870
+ const parsed = parseRuntimeReportSpec(this.spec);
1871
+ if (runtimeReportSpecHash(parsed) !== this.specHash) throw new RuntimeReportError("invalid_spec", "spec", "stored spec does not match its recorded hash");
1872
+ return parsed;
1873
+ }
1874
+ setSpec(spec) {
1875
+ const normalized = parseRuntimeReportSpec(spec);
1876
+ this.spec = serializeRuntimeReportSpec(normalized);
1877
+ this.specHash = runtimeReportSpecHash(normalized);
1878
+ this.title = normalized.title;
1879
+ this.description = normalized.description ?? "";
1880
+ this.sourceId = normalized.source;
1881
+ }
1882
+ async validateBeforeSave() {
1883
+ await super.validateBeforeSave();
1884
+ const parsed = parseRuntimeReportSpec(this.spec);
1885
+ await this.assertConfirmedWrite(parsed);
1886
+ this.setSpec(parsed);
1887
+ if (this.status !== "active" && this.status !== "archived") throw new RuntimeReportError("invalid_spec", "status", "is invalid");
1888
+ }
1889
+ /**
1890
+ * Invariant: a RuntimeReport row is inserted, or has its spec/title/source/
1891
+ * description/author changed, only by the confirmed-save path. Anything else
1892
+ * (REST `create` body, manifest tool args, `new RuntimeReport().save()`,
1893
+ * `collection.update()` of the spec) is refused here. A save that leaves those
1894
+ * columns as stored (an archive/unarchive status change) needs no proof.
1895
+ */
1896
+ async assertConfirmedWrite(parsed) {
1897
+ const hash = runtimeReportSpecHash(parsed);
1898
+ const proof = confirmedSaves.get(this);
1899
+ if (proof !== void 0) {
1900
+ confirmedSaves.delete(this);
1901
+ if (proof === hash) return;
1902
+ } else if (this.isPersisted && this.id) {
1903
+ const stored = await this.db.get(this.tableName, { id: this.id });
1904
+ if (stored && stored.spec_hash === hash && (stored.title ?? "") === parsed.title && (stored.description ?? "") === (parsed.description ?? "") && (stored.source_id ?? "") === parsed.source && (stored.created_by_user_id ?? "") === (this.createdByUserId ?? "")) return;
1905
+ }
1906
+ throw new RuntimeReportError("confirmation_required", "spec", "a runtime report can only be created or changed through the confirmed save of reports.runtime.define; the write was refused");
1907
+ }
1908
+ };
1909
+ __publicField(RuntimeReport, "__smrtPackage__", "@happyvertical/smrt-reports");
1910
+ __decorateClass([tenantId()], RuntimeReport.prototype, "tenantId", 2);
1911
+ __decorateClass([field({ type: "text" })], RuntimeReport.prototype, "title", 2);
1912
+ __decorateClass([field({ type: "text" })], RuntimeReport.prototype, "description", 2);
1913
+ __decorateClass([field({ type: "text" })], RuntimeReport.prototype, "sourceId", 2);
1914
+ __decorateClass([field({
1915
+ type: "text",
1916
+ required: true
1917
+ })], RuntimeReport.prototype, "spec", 2);
1918
+ __decorateClass([field({ type: "text" })], RuntimeReport.prototype, "specHash", 2);
1919
+ __decorateClass([field({
1920
+ type: "text",
1921
+ required: true
1922
+ })], RuntimeReport.prototype, "status", 2);
1923
+ __decorateClass([field({ type: "text" })], RuntimeReport.prototype, "createdByUserId", 2);
1924
+ RuntimeReport = __decorateClass([TenantScoped({ mode: "required" }), smrt({
1925
+ tableName: "runtime_reports",
1926
+ api: { include: [
1927
+ "list",
1928
+ "get",
1929
+ "create"
1930
+ ] },
1931
+ mcp: { include: ["list", "get"] },
1932
+ cli: { include: ["list", "get"] }
1933
+ })], RuntimeReport);
1934
+ var RuntimeReportCollection = class extends SmrtCollection {
1935
+ static _itemClass = RuntimeReport;
1936
+ };
1937
+ async function collectionFor(db) {
1938
+ return RuntimeReportCollection.create({ db });
1939
+ }
1940
+ async function saveRuntimeReport(options) {
1941
+ const tenant = options.tenantId ?? options.compiled.tenant.tenantId ?? getTenantId() ?? "";
1942
+ if (!tenant) throw new RuntimeReportError("tenant_required", "", "a tenant is required to save a runtime report");
1943
+ const spec = parseRuntimeReportSpec(options.compiled.spec);
1944
+ const specHash = runtimeReportSpecHash(spec);
1945
+ if (specHash !== options.compiled.specHash) throw new RuntimeReportError("invalid_spec", "spec", "compiled plan does not match its spec");
1946
+ if (options.confirmedSpecHash !== specHash) throw new RuntimeReportError("confirmation_required", "specHash", "the confirmed spec hash does not match the spec; the report was not saved");
1947
+ return (await collectionFor(options.db)).create({
1948
+ tenantId: tenant,
1949
+ title: spec.title,
1950
+ description: spec.description ?? "",
1951
+ sourceId: spec.source,
1952
+ spec: serializeRuntimeReportSpec(spec),
1953
+ specHash,
1954
+ status: "active",
1955
+ createdByUserId: options.createdByUserId,
1956
+ [CONFIRMED_SAVE]: specHash
1957
+ });
1958
+ }
1959
+ async function getRuntimeReport(options) {
1960
+ const report = await (await collectionFor(options.db)).get(options.ref);
1961
+ if (report && options.tenantId && report.tenantId !== options.tenantId) return null;
1962
+ return report;
1963
+ }
1964
+ async function listRuntimeReports(options) {
1965
+ return (await collectionFor(options.db)).list({
1966
+ where: {
1967
+ status: options.status ?? "active",
1968
+ ...options.tenantId ? { tenantId: options.tenantId } : {}
1969
+ },
1970
+ orderBy: "created_at DESC",
1971
+ limit: Math.min(Math.max(options.limit ?? 50, 1), 200)
1972
+ });
1973
+ }
1974
+ async function archiveRuntimeReport(options) {
1975
+ const report = await getRuntimeReport(options);
1976
+ if (!report) return null;
1977
+ report.status = "archived";
1978
+ await report.save();
1979
+ return report;
1980
+ }
1981
+ async function runStoredRuntimeReport(options) {
1982
+ if (options.report.status !== "active") throw new RuntimeReportError("invalid_spec", "status", "report is archived");
1983
+ return runRuntimeReport(await compileRuntimeReportSpec(options.report.getSpec(), options.context), {
1984
+ db: options.db,
1985
+ maxRows: options.maxRows
1986
+ });
1987
+ }
1988
+ //#endregion
998
1989
  //#region src/views.ts
999
1990
  var INHERITED_REPORT_SCOPE = [
1000
1991
  "principal",
@@ -1515,6 +2506,6 @@ function validateReportExportArtifact(descriptor, artifact, now = /* @__PURE__ *
1515
2506
  };
1516
2507
  }
1517
2508
  //#endregion
1518
- export { REPORT_LOCKS_TABLE, REPORT_REFRESH_TASKS_TABLE, REPORT_RUNS_TABLE, REPORT_RUNTIME_TABLES, REPORT_SCHEDULER_TABLES, REPORT_SCHEDULES_TABLE, REPORT_WATERMARKS_TABLE, ReportScheduleRunner, ReportSurfaceValidationError, SmrtPrincipalReportRefreshTask, SmrtReport, SmrtReportCollection, SmrtReportLock, SmrtReportLockCollection, SmrtReportRefreshTask, SmrtReportRun, SmrtReportRunCollection, SmrtReportSchedule, SmrtReportScheduleCollection, SmrtReportWatermark, SmrtReportWatermarkCollection, aggregate, applyReportExport, applyReportRefresh, assertReportTablesReady, avg, bucketExpr, buildAggregate, buildReportAdapterDescriptor, buildReportDefinition, buildReportDrilldownQuery, compileReportDefinition, compileReportSpec, count, createReportExportPageRequest, createReportExportRequest, createReportExportSnapshot, day, enqueueReportRefresh, ensureReportRefreshSchedules, getReportGroupingColumns, getReportLifecycle, getRuntimeReportOptions, groupBy, hour, max, migrateReportSavedView, min, minute, month, normalizeReportSavedView, previewReportExport, previewReportRefresh, quarter, queryReportMaterializedRows, refreshReport, registerReportRefreshExecutionAuthorityHost, registerReportRefreshInterceptor, registerReportRefreshJobIntegritySigner, report, reportDefinitionFingerprint, reportMaterializedRowKey, reportRefreshOutcome, reportRowIdentity, restoreReportSavedView, scopeKeyForTenant, splitReportFilterScopes, sum, validateReportExportArtifact, validateReportExportExecution, validateReportExportRequest, verifyReportExportSnapshot, week, year };
2509
+ export { MaterializedReportsRecipe, REPORT_LOCKS_TABLE, REPORT_REFRESH_TASKS_TABLE, REPORT_RUNS_TABLE, REPORT_RUNTIME_TABLES, REPORT_SCHEDULER_TABLES, REPORT_SCHEDULES_TABLE, REPORT_WATERMARKS_TABLE, RUNTIME_REPORT_BUCKETS, RUNTIME_REPORT_CHART_TYPES, RUNTIME_REPORT_FILTER_OPS, RUNTIME_REPORT_HAVING_OPS, RUNTIME_REPORT_LIMITS, RUNTIME_REPORT_MEASURE_FNS, RUNTIME_REPORT_SPEC_VERSION, ReportScheduleRunner, ReportSurfaceValidationError, RuntimeReport, RuntimeReportCollection, RuntimeReportError, SmrtPrincipalReportRefreshTask, SmrtReport, SmrtReportCollection, SmrtReportLock, SmrtReportLockCollection, SmrtReportRefreshTask, SmrtReportRun, SmrtReportRunCollection, SmrtReportSchedule, SmrtReportScheduleCollection, SmrtReportWatermark, SmrtReportWatermarkCollection, aggregate, applyReportExport, applyReportRefresh, archiveRuntimeReport, assertReportTablesReady, avg, bucketExpr, buildAggregate, buildReportAdapterDescriptor, buildReportDefinition, buildReportDrilldownQuery, compileReportDefinition, compileReportSpec, compileRuntimeReportSpec, count, createReportExportPageRequest, createReportExportRequest, createReportExportSnapshot, day, describeRuntimeReportSource, enqueueReportRefresh, ensureReportRefreshSchedules, getReportGroupingColumns, getReportLifecycle, getRuntimeReport, getRuntimeReportOptions, groupBy, hour, listRuntimeReports, max, migrateReportSavedView, min, minute, month, normalizeReportSavedView, parseRuntimeReportSpec, previewReportExport, previewReportRefresh, quarter, queryReportMaterializedRows, refreshReport, registerReportRefreshExecutionAuthorityHost, registerReportRefreshInterceptor, registerReportRefreshJobIntegritySigner, report, reportDefinitionFingerprint, reportMaterializedRowKey, reportRefreshOutcome, reportRowIdentity, restoreReportSavedView, runRuntimeReport, runStoredRuntimeReport, runtimeReportSpecHash, saveRuntimeReport, scopeKeyForTenant, serializeRuntimeReportSpec, splitReportFilterScopes, sum, validateReportExportArtifact, validateReportExportExecution, validateReportExportRequest, verifyReportExportSnapshot, week, year };
1519
2510
 
1520
2511
  //# sourceMappingURL=index.js.map