@happyvertical/smrt-reports 0.55.24 → 0.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/AGENTS.md +26 -3
  2. package/README.md +9 -0
  3. package/dist/index.d.ts +3 -1
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +471 -112
  6. package/dist/index.js.map +1 -1
  7. package/dist/manifest.json +174 -5
  8. package/dist/playground.d.ts +2 -0
  9. package/dist/playground.d.ts.map +1 -0
  10. package/dist/playground.js +154 -0
  11. package/dist/playground.js.map +1 -0
  12. package/dist/recipes.d.ts +55 -0
  13. package/dist/recipes.d.ts.map +1 -1
  14. package/dist/runtime-report.d.ts +9 -0
  15. package/dist/runtime-report.d.ts.map +1 -1
  16. package/dist/runtime-server.d.ts +77 -0
  17. package/dist/runtime-server.d.ts.map +1 -0
  18. package/dist/runtime-spec.d.ts +2 -1
  19. package/dist/runtime-spec.d.ts.map +1 -1
  20. package/dist/smrt-knowledge.json +203 -11
  21. package/dist/state.js +1 -1
  22. package/dist/state.js.map +1 -1
  23. package/dist/svelte/ReportConfirmation.svelte +99 -0
  24. package/dist/svelte/ReportConfirmation.svelte.d.ts +15 -0
  25. package/dist/svelte/ReportConfirmation.svelte.d.ts.map +1 -0
  26. package/dist/svelte/ReportDetail.svelte +69 -0
  27. package/dist/svelte/ReportDetail.svelte.d.ts +11 -0
  28. package/dist/svelte/ReportDetail.svelte.d.ts.map +1 -0
  29. package/dist/svelte/ReportPicker.svelte +73 -0
  30. package/dist/svelte/ReportPicker.svelte.d.ts +16 -0
  31. package/dist/svelte/ReportPicker.svelte.d.ts.map +1 -0
  32. package/dist/svelte/ReportResult.svelte +69 -0
  33. package/dist/svelte/ReportResult.svelte.d.ts +10 -0
  34. package/dist/svelte/ReportResult.svelte.d.ts.map +1 -0
  35. package/dist/svelte/RuntimeReportChartWidget.svelte +20 -0
  36. package/dist/svelte/RuntimeReportChartWidget.svelte.d.ts +11 -0
  37. package/dist/svelte/RuntimeReportChartWidget.svelte.d.ts.map +1 -0
  38. package/dist/svelte/RuntimeReportDetailRoute.svelte +67 -0
  39. package/dist/svelte/RuntimeReportDetailRoute.svelte.d.ts +21 -0
  40. package/dist/svelte/RuntimeReportDetailRoute.svelte.d.ts.map +1 -0
  41. package/dist/svelte/RuntimeReportToolResult.svelte +26 -0
  42. package/dist/svelte/RuntimeReportToolResult.svelte.d.ts +8 -0
  43. package/dist/svelte/RuntimeReportToolResult.svelte.d.ts.map +1 -0
  44. package/dist/svelte/RuntimeReportWidgetOptions.svelte +58 -0
  45. package/dist/svelte/RuntimeReportWidgetOptions.svelte.d.ts +14 -0
  46. package/dist/svelte/RuntimeReportWidgetOptions.svelte.d.ts.map +1 -0
  47. package/dist/svelte/RuntimeReportsRoute.svelte +67 -0
  48. package/dist/svelte/RuntimeReportsRoute.svelte.d.ts +19 -0
  49. package/dist/svelte/RuntimeReportsRoute.svelte.d.ts.map +1 -0
  50. package/dist/svelte/SavedReports.svelte +114 -0
  51. package/dist/svelte/SavedReports.svelte.d.ts +11 -0
  52. package/dist/svelte/SavedReports.svelte.d.ts.map +1 -0
  53. package/dist/svelte/browser-client.d.ts +10 -0
  54. package/dist/svelte/browser-client.d.ts.map +1 -0
  55. package/dist/svelte/browser-client.js +16 -0
  56. package/dist/svelte/client.d.ts +15 -0
  57. package/dist/svelte/client.d.ts.map +1 -0
  58. package/dist/svelte/client.js +179 -0
  59. package/dist/svelte/index.d.ts +15 -0
  60. package/dist/svelte/index.d.ts.map +1 -0
  61. package/dist/svelte/index.js +13 -0
  62. package/dist/svelte/messages.d.ts +72 -0
  63. package/dist/svelte/messages.d.ts.map +1 -0
  64. package/dist/svelte/messages.js +72 -0
  65. package/dist/svelte/playground.d.ts +132 -0
  66. package/dist/svelte/playground.d.ts.map +1 -0
  67. package/dist/svelte/playground.js +62 -0
  68. package/dist/svelte/preview-data.d.ts +7 -0
  69. package/dist/svelte/preview-data.d.ts.map +1 -0
  70. package/dist/svelte/preview-data.js +59 -0
  71. package/dist/svelte/result.d.ts +8 -0
  72. package/dist/svelte/result.d.ts.map +1 -0
  73. package/dist/svelte/result.js +99 -0
  74. package/dist/svelte/spec.d.ts +5 -0
  75. package/dist/svelte/spec.d.ts.map +1 -0
  76. package/dist/svelte/spec.js +282 -0
  77. package/dist/svelte/tool-result.d.ts +11 -0
  78. package/dist/svelte/tool-result.d.ts.map +1 -0
  79. package/dist/svelte/tool-result.js +19 -0
  80. package/dist/svelte/types.d.ts +38 -0
  81. package/dist/svelte/types.d.ts.map +1 -0
  82. package/dist/svelte/types.js +1 -0
  83. package/package.json +36 -12
package/dist/index.js CHANGED
@@ -4,11 +4,12 @@ import { REPORT_LOCKS_TABLE, REPORT_REFRESH_TASKS_TABLE, REPORT_RUNS_TABLE, REPO
4
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, SmrtRecipe, createDataQueryFingerprint, field, normalizeDataQueryRequest, normalizeDataQueryResult, smrt } from "@happyvertical/smrt-core";
7
+ import { DataQueryValidationError, ObjectRegistry, SmrtCollection, SmrtError, SmrtObject, SmrtRecipe, createDataQueryFingerprint, field, normalizeDataQueryRequest, normalizeDataQueryResult, runOnce, smrt } from "@happyvertical/smrt-core";
8
8
  import { toSnakeCase } from "@happyvertical/smrt-core/utils";
9
- import { TenantScoped, getTenantId, isTenancyEnabled, isTenantScopedClass, tenantId, withSystemContext } from "@happyvertical/smrt-tenancy";
9
+ import { TenantScoped, getCurrentTenant, getTenantId, isTenancyEnabled, isTenantScopedClass, tenantId, withSystemContext, withTenant } from "@happyvertical/smrt-tenancy";
10
10
  import { tableExists, validateColumnName } from "@happyvertical/sql";
11
11
  import { createHash } from "node:crypto";
12
+ import { getPackageConfig } from "@happyvertical/smrt-config";
12
13
  //#region src/lifecycle.ts
13
14
  function registeredReport(reportCtor) {
14
15
  return ObjectRegistry.getClassByConstructor(reportCtor) ?? ObjectRegistry.getClass(reportCtor.name);
@@ -971,109 +972,6 @@ function reportMaterializedRowKey(row) {
971
972
  return id;
972
973
  }
973
974
  //#endregion
974
- //#region src/recipes.ts
975
- var MaterializedReportsRecipe = class extends SmrtRecipe {
976
- static id = "reports.materialized";
977
- static help = "./materialized.recipe.md";
978
- static label = "Reports";
979
- static summary = "Keep totals and trends ready to read, refreshed on a schedule and open to the assistant.";
980
- static synonyms = [
981
- "reports",
982
- "totals",
983
- "summaries",
984
- "rollups",
985
- "kpis",
986
- "trends",
987
- "dashboards"
988
- ];
989
- static models = [
990
- SmrtReportRun,
991
- SmrtReportWatermark,
992
- SmrtReportLock,
993
- SmrtReportSchedule,
994
- SmrtReportRefreshTask,
995
- SmrtPrincipalReportRefreshTask
996
- ];
997
- static runtime = "server";
998
- };
999
- //#endregion
1000
- //#region src/report.ts
1001
- function registryColumnName(fieldName, field) {
1002
- return validateColumnName(field?.columnName ?? field?._meta?.columnName ?? toSnakeCase(fieldName));
1003
- }
1004
- function findFieldColumn(fields, fieldName) {
1005
- const direct = fields.get(fieldName);
1006
- if (direct) return registryColumnName(fieldName, direct);
1007
- const requestedColumn = toSnakeCase(fieldName);
1008
- for (const [name, field] of fields.entries()) if (toSnakeCase(name) === requestedColumn) return registryColumnName(name, field);
1009
- return validateColumnName(requestedColumn);
1010
- }
1011
- function findTenantColumn(fields, configuredField) {
1012
- if (configuredField) return registryColumnName(configuredField, fields.get(configuredField));
1013
- for (const [fieldName, field] of fields.entries()) if (fieldName === "tenantId" || field?._meta?.__tenancy?.isTenantIdField) return registryColumnName(fieldName, field);
1014
- return null;
1015
- }
1016
- var SmrtReport = class extends SmrtObject {
1017
- static _isReportBase = true;
1018
- refreshedAt = null;
1019
- constructor(options = {}) {
1020
- super(options);
1021
- if (options.refreshedAt !== void 0) this.refreshedAt = options.refreshedAt instanceof Date ? options.refreshedAt : options.refreshedAt ? new Date(options.refreshedAt) : null;
1022
- }
1023
- isStale(ttlMs) {
1024
- if (!this.refreshedAt) return true;
1025
- if (ttlMs === void 0) return false;
1026
- return Date.now() - this.refreshedAt.getTime() > ttlMs;
1027
- }
1028
- async refresh(options = {}) {
1029
- const result = await refreshReport(this.constructor, {
1030
- ...options,
1031
- db: this.db
1032
- });
1033
- this.refreshedAt = result.refreshedAt;
1034
- return result;
1035
- }
1036
- };
1037
- var SmrtReportCollection = class extends SmrtCollection {
1038
- async refreshIfStale() {
1039
- const reportCtor = this.getItemClass();
1040
- const refresh = (await buildReportDefinition(reportCtor)).refresh;
1041
- if (!refresh?.ttl || refresh.manual) return;
1042
- const registered = ObjectRegistry.getClassByConstructor(reportCtor) ?? ObjectRegistry.getClass(reportCtor.name);
1043
- const reportClass = registered?.qualifiedName ?? registered?.name ?? reportCtor.name;
1044
- const tableName = ObjectRegistry.getTableName(reportClass);
1045
- if (!tableName) return;
1046
- const fields = await ObjectRegistry.getAllFields(reportClass);
1047
- const safeTableName = validateColumnName(tableName);
1048
- const refreshedAtColumn = findFieldColumn(fields, "refreshedAt");
1049
- const tenantColumn = findTenantColumn(fields, registered?.tenantScopedConfig?.field);
1050
- const tenantId = getTenantId() ?? null;
1051
- const result = tenantColumn ? await this.db.query(`SELECT MAX(${refreshedAtColumn}) AS refreshed_at FROM ${safeTableName} WHERE ${tenantColumn} ${tenantId ? "= ?" : "IS NULL"}`, ...tenantId ? [tenantId] : []) : await this.db.query(`SELECT MAX(${refreshedAtColumn}) AS refreshed_at FROM ${safeTableName}`);
1052
- const refreshedAt = result.rows[0]?.refreshed_at ? new Date(result.rows[0].refreshed_at) : null;
1053
- if (!(!refreshedAt || Date.now() - refreshedAt.getTime() > refresh.ttl)) return;
1054
- await refreshReport(reportCtor, {
1055
- db: this.db,
1056
- mode: refresh.mode ?? "rebuild",
1057
- trigger: "ttl",
1058
- tenantId
1059
- });
1060
- }
1061
- async refresh(options = {}) {
1062
- return refreshReport(this.getItemClass(), {
1063
- ...options,
1064
- db: this.db
1065
- });
1066
- }
1067
- async list(options = {}) {
1068
- await this.refreshIfStale();
1069
- return super.list(options);
1070
- }
1071
- async get(filter, options = {}) {
1072
- await this.refreshIfStale();
1073
- return super.get(filter, options);
1074
- }
1075
- };
1076
- //#endregion
1077
975
  //#region src/runtime-spec.ts
1078
976
  var RUNTIME_REPORT_SPEC_VERSION = 1;
1079
977
  var RUNTIME_REPORT_LIMITS = Object.freeze({
@@ -1137,7 +1035,7 @@ var RUNTIME_REPORT_CHART_TYPES = [
1137
1035
  "pie",
1138
1036
  "stat"
1139
1037
  ];
1140
- var RuntimeReportError = class extends Error {
1038
+ var RuntimeReportError = class extends SmrtError {
1141
1039
  code;
1142
1040
  path;
1143
1041
  /**
@@ -1147,7 +1045,7 @@ var RuntimeReportError = class extends Error {
1147
1045
  */
1148
1046
  status;
1149
1047
  constructor(code, path, message) {
1150
- super(path ? `${path}: ${message}` : message);
1048
+ super(path ? `${path}: ${message}` : message, code, "validation");
1151
1049
  this.name = "RuntimeReportError";
1152
1050
  this.code = code;
1153
1051
  this.path = path;
@@ -1909,6 +1807,248 @@ async function runRuntimeReport(compiled, options) {
1909
1807
  };
1910
1808
  }
1911
1809
  //#endregion
1810
+ //#region src/runtime-server.ts
1811
+ var RuntimeReportHttpError = class extends Error {
1812
+ constructor(status, code, message) {
1813
+ super(message);
1814
+ this.status = status;
1815
+ this.code = code;
1816
+ this.name = "RuntimeReportHttpError";
1817
+ }
1818
+ status;
1819
+ code;
1820
+ };
1821
+ function isRuntimeReportAccessError(error) {
1822
+ return error.code === "unknown_field" || error.code === "unknown_source";
1823
+ }
1824
+ function assertRuntimeReportPrincipal(context, permission) {
1825
+ if (!context?.userId) throw new RuntimeReportHttpError(401, "unauthenticated", "Sign in required");
1826
+ const active = getCurrentTenant();
1827
+ if (!context.tenantId || active && active.tenantId !== context.tenantId || active?.userId && active.userId !== context.userId || !context.permissions.includes(permission)) throw new RuntimeReportHttpError(403, "forbidden", "Report access denied");
1828
+ }
1829
+ function runtimeReportPrincipalCompileContext(options, principal) {
1830
+ return {
1831
+ sources: options.sources.filter((source) => principal.permissions.includes(`${source.collection}.read`)),
1832
+ permissions: principal.permissions,
1833
+ tenantId: principal.tenantId
1834
+ };
1835
+ }
1836
+ function createRuntimeReportConfirmHandler(options) {
1837
+ return async (request, context) => {
1838
+ try {
1839
+ if (request.method !== "POST") return Response.json({ code: "method_not_allowed" }, {
1840
+ status: 405,
1841
+ headers: { Allow: "POST" }
1842
+ });
1843
+ const origin = request.headers.get("origin");
1844
+ const site = request.headers.get("sec-fetch-site");
1845
+ if (origin !== null && origin !== new URL(request.url).origin || site !== null && site !== "same-origin" || origin === null && site !== "same-origin") throw new RuntimeReportHttpError(403, "cross_origin", "Same-origin request required");
1846
+ assertRuntimeReportPrincipal(context, "runtimereports.create");
1847
+ let body;
1848
+ try {
1849
+ body = await request.json();
1850
+ } catch {
1851
+ throw new RuntimeReportHttpError(422, "invalid_spec", "Invalid JSON body");
1852
+ }
1853
+ if (!body || typeof body !== "object" || Array.isArray(body) || Object.keys(body).some((key) => key !== "spec" && key !== "specHash")) throw new RuntimeReportHttpError(422, "invalid_spec", "Expected spec and specHash");
1854
+ const input = body;
1855
+ const spec = parseRuntimeReportSpec(input.spec);
1856
+ const hash = runtimeReportSpecHash(spec);
1857
+ if (input.specHash !== hash) throw new RuntimeReportHttpError(422, "spec_hash_mismatch", "The approved spec hash does not match");
1858
+ const principal = context;
1859
+ return await withTenant({
1860
+ tenantId: principal.tenantId,
1861
+ userId: principal.userId,
1862
+ permissions: new Set(principal.permissions)
1863
+ }, async () => {
1864
+ const serverOptions = options ?? getRuntimeReportServerOptions();
1865
+ const transaction = principal.db.transaction?.bind(principal.db);
1866
+ if (!transaction) throw new RuntimeReportHttpError(500, "transaction_required", "Runtime report confirmation requires transaction support");
1867
+ const { existing, archived } = await transaction(async (tx) => {
1868
+ const lookup = {
1869
+ db: tx,
1870
+ tenantId: principal.tenantId,
1871
+ createdByUserId: principal.userId,
1872
+ specHash: hash,
1873
+ limit: 1
1874
+ };
1875
+ const [existing2] = await listRuntimeReports(lookup);
1876
+ const [archived2] = existing2 ? [] : await listRuntimeReports({
1877
+ ...lookup,
1878
+ status: "archived"
1879
+ });
1880
+ return {
1881
+ existing: existing2,
1882
+ archived: archived2
1883
+ };
1884
+ });
1885
+ if (archived) throw new RuntimeReportHttpError(409, "report_archived", "Restore the archived report before confirming it again");
1886
+ let compiled;
1887
+ try {
1888
+ compiled = await compileRuntimeReportSpec(spec, runtimeReportPrincipalCompileContext(serverOptions, principal));
1889
+ } catch (error) {
1890
+ if (existing && error instanceof RuntimeReportError && isRuntimeReportAccessError(error)) throw new RuntimeReportHttpError(403, "forbidden", "Report access denied");
1891
+ throw error;
1892
+ }
1893
+ let created = false;
1894
+ let report = existing ? {
1895
+ id: existing.id,
1896
+ specHash: hash,
1897
+ title: existing.title
1898
+ } : void 0;
1899
+ if (!report) report = await runOnce({
1900
+ db: principal.db,
1901
+ tenantId: principal.tenantId,
1902
+ actor: principal.userId,
1903
+ token: "runtime-report-confirm",
1904
+ content: spec
1905
+ }, async (tx) => {
1906
+ const [current] = await listRuntimeReports({
1907
+ db: tx,
1908
+ tenantId: principal.tenantId,
1909
+ createdByUserId: principal.userId,
1910
+ specHash: hash,
1911
+ limit: 1
1912
+ });
1913
+ const saved = current ?? await saveRuntimeReport({
1914
+ db: tx,
1915
+ tenantId: principal.tenantId,
1916
+ createdByUserId: principal.userId,
1917
+ compiled,
1918
+ confirmedSpecHash: hash
1919
+ });
1920
+ created = !current;
1921
+ if (!saved.id) throw new Error("Saved report has no id");
1922
+ return {
1923
+ id: saved.id,
1924
+ specHash: hash,
1925
+ title: saved.title
1926
+ };
1927
+ });
1928
+ if (!report.id) throw new Error("Saved report has no id");
1929
+ await serverOptions.audit?.({
1930
+ action: "confirm",
1931
+ userId: principal.userId,
1932
+ tenantId: principal.tenantId,
1933
+ specHash: hash,
1934
+ reportId: report.id,
1935
+ reused: !created
1936
+ });
1937
+ return Response.json({
1938
+ id: report.id,
1939
+ specHash: hash,
1940
+ title: report.title
1941
+ }, {
1942
+ status: created ? 201 : 200,
1943
+ headers: { "Cache-Control": "no-store" }
1944
+ });
1945
+ });
1946
+ } catch (error) {
1947
+ if (error instanceof RuntimeReportHttpError || error instanceof RuntimeReportError) return Response.json({
1948
+ code: error.code,
1949
+ message: error.message
1950
+ }, {
1951
+ status: error instanceof RuntimeReportError ? 422 : error.status,
1952
+ headers: { "Cache-Control": "no-store" }
1953
+ });
1954
+ return Response.json({
1955
+ code: "internal_error",
1956
+ message: "Unable to confirm report"
1957
+ }, {
1958
+ status: 500,
1959
+ headers: { "Cache-Control": "no-store" }
1960
+ });
1961
+ }
1962
+ };
1963
+ }
1964
+ function getRuntimeReportServerOptions() {
1965
+ return getPackageConfig("reports").runtime ?? { sources: [] };
1966
+ }
1967
+ function currentRuntimeReportPrincipal(db) {
1968
+ const context = getCurrentTenant();
1969
+ return {
1970
+ db,
1971
+ userId: context?.userId ?? "",
1972
+ tenantId: context?.tenantId ?? "",
1973
+ permissions: [...context?.permissions ?? []]
1974
+ };
1975
+ }
1976
+ async function runRuntimeReportForPrincipal(ref, principal, options = getRuntimeReportServerOptions()) {
1977
+ assertRuntimeReportPrincipal(principal, "runtimereports.read");
1978
+ return withTenant({
1979
+ tenantId: principal.tenantId,
1980
+ userId: principal.userId,
1981
+ permissions: new Set(principal.permissions)
1982
+ }, async () => {
1983
+ const report = await getRuntimeReport({
1984
+ db: principal.db,
1985
+ ref,
1986
+ tenantId: principal.tenantId
1987
+ });
1988
+ if (!report) throw new RuntimeReportHttpError(404, "not_found", "Report not found");
1989
+ if (report.status === "archived") throw new RuntimeReportHttpError(409, "report_archived", "Restore the archived report before running it");
1990
+ try {
1991
+ return await runStoredRuntimeReport({
1992
+ db: principal.db,
1993
+ report,
1994
+ context: runtimeReportPrincipalCompileContext(options, principal)
1995
+ });
1996
+ } catch (error) {
1997
+ if (error instanceof RuntimeReportError) {
1998
+ if (isRuntimeReportAccessError(error)) throw new RuntimeReportHttpError(403, "forbidden", "Report access denied");
1999
+ throw new RuntimeReportHttpError(422, error.code, error.message);
2000
+ }
2001
+ throw error;
2002
+ }
2003
+ });
2004
+ }
2005
+ async function archiveRuntimeReportForPrincipal(ref, archived, principal) {
2006
+ assertRuntimeReportPrincipal(principal, "runtimereports.create");
2007
+ if (typeof archived !== "boolean") throw new RuntimeReportHttpError(422, "invalid_value", "archived must be a boolean");
2008
+ return withTenant({
2009
+ tenantId: principal.tenantId,
2010
+ userId: principal.userId,
2011
+ permissions: new Set(principal.permissions)
2012
+ }, async () => {
2013
+ const report = await getRuntimeReport({
2014
+ db: principal.db,
2015
+ ref,
2016
+ tenantId: principal.tenantId
2017
+ });
2018
+ if (!report?.id) throw new RuntimeReportHttpError(404, "not_found", "Report not found");
2019
+ const status = archived ? "archived" : "active";
2020
+ if (report.status !== status) {
2021
+ report.status = status;
2022
+ await report.save();
2023
+ }
2024
+ return {
2025
+ id: report.id,
2026
+ status
2027
+ };
2028
+ });
2029
+ }
2030
+ async function loadRuntimeReportChart(options, context) {
2031
+ assertRuntimeReportPrincipal(context.reportPrincipal, "runtimereports.read");
2032
+ if (typeof options.reportId !== "string" || typeof options.measure !== "string") throw new RuntimeReportHttpError(422, "invalid_value", "Select a report and measure");
2033
+ const result = await runRuntimeReportForPrincipal(options.reportId, context.reportPrincipal);
2034
+ const measure = result.columns.find((column) => column.id === options.measure && column.role === "measure");
2035
+ const dimension = options.dimension === void 0 ? result.columns.find((column) => column.role === "dimension") : result.columns.find((column) => column.id === options.dimension && column.role === "dimension");
2036
+ if (!measure || options.dimension !== void 0 && !dimension) throw new RuntimeReportHttpError(422, "invalid_value", "Invalid report measure or dimension");
2037
+ return {
2038
+ points: result.rows.map((row) => {
2039
+ const value = row[measure.id];
2040
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new RuntimeReportHttpError(422, "invalid_result", "Chart requires numeric measures");
2041
+ return {
2042
+ label: dimension ? String(row[dimension.id] ?? "") : result.title,
2043
+ value
2044
+ };
2045
+ }),
2046
+ format: measure.format === "money" ? "money" : measure.type === "integer" ? "integer" : "decimal",
2047
+ ...context.currency ? { currency: context.currency } : {},
2048
+ href: `/reports/${encodeURIComponent(options.reportId)}`
2049
+ };
2050
+ }
2051
+ //#endregion
1912
2052
  //#region src/runtime-report.ts
1913
2053
  var __defProp = Object.defineProperty;
1914
2054
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -1941,6 +2081,14 @@ var RuntimeReport = class extends SmrtObject {
1941
2081
  const proof = options[CONFIRMED_SAVE];
1942
2082
  if (typeof proof === "string") confirmedSaves.set(this, proof);
1943
2083
  }
2084
+ /** Run fresh totals using the authenticated viewer's current permissions. */
2085
+ async run() {
2086
+ return runRuntimeReportForPrincipal(this.id ?? "", currentRuntimeReportPrincipal(this.db));
2087
+ }
2088
+ /** Archive or restore this definition; no report content can be changed. */
2089
+ async archive(options) {
2090
+ return archiveRuntimeReportForPrincipal(this.id ?? "", options?.archived, currentRuntimeReportPrincipal(this.db));
2091
+ }
1944
2092
  /** Re-parse the stored JSON; never trust the column without validation. */
1945
2093
  getSpec() {
1946
2094
  const parsed = parseRuntimeReportSpec(this.spec);
@@ -1999,13 +2147,60 @@ __decorateClass([field({
1999
2147
  __decorateClass([field({ type: "text" })], RuntimeReport.prototype, "createdByUserId", 2);
2000
2148
  RuntimeReport = __decorateClass([TenantScoped({ mode: "required" }), smrt({
2001
2149
  tableName: "runtime_reports",
2002
- api: { include: [
2150
+ indexes: [{
2151
+ name: "idx_runtime_reports_confirm",
2152
+ columns: [
2153
+ "tenantId",
2154
+ "createdByUserId",
2155
+ "specHash",
2156
+ "status"
2157
+ ]
2158
+ }],
2159
+ operationPermissions: {
2160
+ run: { action: "read" },
2161
+ archive: {
2162
+ action: "create",
2163
+ databaseActions: ["SELECT", "UPDATE"]
2164
+ }
2165
+ },
2166
+ api: {
2167
+ include: [
2168
+ "list",
2169
+ "get",
2170
+ "create",
2171
+ "run",
2172
+ "archive"
2173
+ ],
2174
+ principalContext: true,
2175
+ routes: {
2176
+ run: {
2177
+ method: "POST",
2178
+ scope: "item",
2179
+ effect: "read",
2180
+ idempotent: true,
2181
+ openWorld: false
2182
+ },
2183
+ archive: {
2184
+ method: "POST",
2185
+ scope: "item",
2186
+ effect: "write",
2187
+ idempotent: true,
2188
+ openWorld: false
2189
+ }
2190
+ }
2191
+ },
2192
+ mcp: { include: [
2003
2193
  "list",
2004
2194
  "get",
2005
- "create"
2195
+ "run",
2196
+ "archive"
2006
2197
  ] },
2007
- mcp: { include: ["list", "get"] },
2008
- cli: { include: ["list", "get"] }
2198
+ cli: { include: [
2199
+ "list",
2200
+ "get",
2201
+ "run",
2202
+ "archive"
2203
+ ] }
2009
2204
  })], RuntimeReport);
2010
2205
  var RuntimeReportCollection = class extends SmrtCollection {
2011
2206
  static _itemClass = RuntimeReport;
@@ -2064,6 +2259,170 @@ async function runStoredRuntimeReport(options) {
2064
2259
  });
2065
2260
  }
2066
2261
  //#endregion
2262
+ //#region src/recipes.ts
2263
+ var MaterializedReportsRecipe = class extends SmrtRecipe {
2264
+ static id = "reports.materialized";
2265
+ static help = "./materialized.recipe.md";
2266
+ static label = "Reports";
2267
+ static summary = "Keep totals and trends ready to read, refreshed on a schedule and open to the assistant.";
2268
+ static synonyms = [
2269
+ "reports",
2270
+ "totals",
2271
+ "summaries",
2272
+ "rollups",
2273
+ "kpis",
2274
+ "trends",
2275
+ "dashboards"
2276
+ ];
2277
+ static models = [
2278
+ SmrtReportRun,
2279
+ SmrtReportWatermark,
2280
+ SmrtReportLock,
2281
+ SmrtReportSchedule,
2282
+ SmrtReportRefreshTask,
2283
+ SmrtPrincipalReportRefreshTask
2284
+ ];
2285
+ static runtime = "server";
2286
+ };
2287
+ var RuntimeReportsRecipe = class extends SmrtRecipe {
2288
+ static id = "reports.runtime";
2289
+ static label = "Reports";
2290
+ static summary = "Ask for a report, approve it, and share its live totals with your team.";
2291
+ static help = "./runtime.recipe.md";
2292
+ static models = [RuntimeReport];
2293
+ static nav = [{
2294
+ label: "Reports",
2295
+ model: RuntimeReport,
2296
+ icon: "fileText"
2297
+ }];
2298
+ static surfaces = [
2299
+ {
2300
+ kind: "route",
2301
+ path: "/reports",
2302
+ export: "@happyvertical/smrt-reports/svelte#RuntimeReportsRoute",
2303
+ label: "Reports"
2304
+ },
2305
+ {
2306
+ kind: "route",
2307
+ path: "/reports/:id",
2308
+ export: "@happyvertical/smrt-reports/svelte#RuntimeReportDetailRoute",
2309
+ label: "Report detail"
2310
+ },
2311
+ {
2312
+ kind: "widget",
2313
+ type: "runtime-report-chart",
2314
+ export: "@happyvertical/smrt-reports/svelte#RuntimeReportChartWidget",
2315
+ label: "Report chart",
2316
+ description: "Chart a measure from a saved report.",
2317
+ icon: "chart",
2318
+ optionsEditor: "@happyvertical/smrt-reports/svelte#RuntimeReportWidgetOptions",
2319
+ options: [
2320
+ {
2321
+ key: "reportId",
2322
+ type: "text",
2323
+ label: "Report",
2324
+ required: true
2325
+ },
2326
+ {
2327
+ key: "measure",
2328
+ type: "identifier",
2329
+ label: "Measure",
2330
+ required: true
2331
+ },
2332
+ {
2333
+ key: "dimension",
2334
+ type: "identifier",
2335
+ label: "Dimension"
2336
+ }
2337
+ ],
2338
+ data: {
2339
+ load: "@happyvertical/smrt-reports#loadRuntimeReportChart",
2340
+ models: ["@happyvertical/smrt-reports:RuntimeReport"]
2341
+ },
2342
+ defaultSpan: 2,
2343
+ maxSpan: 4
2344
+ }
2345
+ ];
2346
+ static runtime = "server";
2347
+ };
2348
+ //#endregion
2349
+ //#region src/report.ts
2350
+ function registryColumnName(fieldName, field) {
2351
+ return validateColumnName(field?.columnName ?? field?._meta?.columnName ?? toSnakeCase(fieldName));
2352
+ }
2353
+ function findFieldColumn(fields, fieldName) {
2354
+ const direct = fields.get(fieldName);
2355
+ if (direct) return registryColumnName(fieldName, direct);
2356
+ const requestedColumn = toSnakeCase(fieldName);
2357
+ for (const [name, field] of fields.entries()) if (toSnakeCase(name) === requestedColumn) return registryColumnName(name, field);
2358
+ return validateColumnName(requestedColumn);
2359
+ }
2360
+ function findTenantColumn(fields, configuredField) {
2361
+ if (configuredField) return registryColumnName(configuredField, fields.get(configuredField));
2362
+ for (const [fieldName, field] of fields.entries()) if (fieldName === "tenantId" || field?._meta?.__tenancy?.isTenantIdField) return registryColumnName(fieldName, field);
2363
+ return null;
2364
+ }
2365
+ var SmrtReport = class extends SmrtObject {
2366
+ static _isReportBase = true;
2367
+ refreshedAt = null;
2368
+ constructor(options = {}) {
2369
+ super(options);
2370
+ if (options.refreshedAt !== void 0) this.refreshedAt = options.refreshedAt instanceof Date ? options.refreshedAt : options.refreshedAt ? new Date(options.refreshedAt) : null;
2371
+ }
2372
+ isStale(ttlMs) {
2373
+ if (!this.refreshedAt) return true;
2374
+ if (ttlMs === void 0) return false;
2375
+ return Date.now() - this.refreshedAt.getTime() > ttlMs;
2376
+ }
2377
+ async refresh(options = {}) {
2378
+ const result = await refreshReport(this.constructor, {
2379
+ ...options,
2380
+ db: this.db
2381
+ });
2382
+ this.refreshedAt = result.refreshedAt;
2383
+ return result;
2384
+ }
2385
+ };
2386
+ var SmrtReportCollection = class extends SmrtCollection {
2387
+ async refreshIfStale() {
2388
+ const reportCtor = this.getItemClass();
2389
+ const refresh = (await buildReportDefinition(reportCtor)).refresh;
2390
+ if (!refresh?.ttl || refresh.manual) return;
2391
+ const registered = ObjectRegistry.getClassByConstructor(reportCtor) ?? ObjectRegistry.getClass(reportCtor.name);
2392
+ const reportClass = registered?.qualifiedName ?? registered?.name ?? reportCtor.name;
2393
+ const tableName = ObjectRegistry.getTableName(reportClass);
2394
+ if (!tableName) return;
2395
+ const fields = await ObjectRegistry.getAllFields(reportClass);
2396
+ const safeTableName = validateColumnName(tableName);
2397
+ const refreshedAtColumn = findFieldColumn(fields, "refreshedAt");
2398
+ const tenantColumn = findTenantColumn(fields, registered?.tenantScopedConfig?.field);
2399
+ const tenantId = getTenantId() ?? null;
2400
+ const result = tenantColumn ? await this.db.query(`SELECT MAX(${refreshedAtColumn}) AS refreshed_at FROM ${safeTableName} WHERE ${tenantColumn} ${tenantId ? "= ?" : "IS NULL"}`, ...tenantId ? [tenantId] : []) : await this.db.query(`SELECT MAX(${refreshedAtColumn}) AS refreshed_at FROM ${safeTableName}`);
2401
+ const refreshedAt = result.rows[0]?.refreshed_at ? new Date(result.rows[0].refreshed_at) : null;
2402
+ if (!(!refreshedAt || Date.now() - refreshedAt.getTime() > refresh.ttl)) return;
2403
+ await refreshReport(reportCtor, {
2404
+ db: this.db,
2405
+ mode: refresh.mode ?? "rebuild",
2406
+ trigger: "ttl",
2407
+ tenantId
2408
+ });
2409
+ }
2410
+ async refresh(options = {}) {
2411
+ return refreshReport(this.getItemClass(), {
2412
+ ...options,
2413
+ db: this.db
2414
+ });
2415
+ }
2416
+ async list(options = {}) {
2417
+ await this.refreshIfStale();
2418
+ return super.list(options);
2419
+ }
2420
+ async get(filter, options = {}) {
2421
+ await this.refreshIfStale();
2422
+ return super.get(filter, options);
2423
+ }
2424
+ };
2425
+ //#endregion
2067
2426
  //#region src/views.ts
2068
2427
  var INHERITED_REPORT_SCOPE = [
2069
2428
  "principal",
@@ -2584,6 +2943,6 @@ function validateReportExportArtifact(descriptor, artifact, now = /* @__PURE__ *
2584
2943
  };
2585
2944
  }
2586
2945
  //#endregion
2587
- 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 };
2946
+ 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, RuntimeReportHttpError, RuntimeReportsRecipe, SmrtPrincipalReportRefreshTask, SmrtReport, SmrtReportCollection, SmrtReportLock, SmrtReportLockCollection, SmrtReportRefreshTask, SmrtReportRun, SmrtReportRunCollection, SmrtReportSchedule, SmrtReportScheduleCollection, SmrtReportWatermark, SmrtReportWatermarkCollection, aggregate, applyReportExport, applyReportRefresh, archiveRuntimeReport, archiveRuntimeReportForPrincipal, assertReportTablesReady, avg, bucketExpr, buildAggregate, buildReportAdapterDescriptor, buildReportDefinition, buildReportDrilldownQuery, compileReportDefinition, compileReportSpec, compileRuntimeReportSpec, count, createReportExportPageRequest, createReportExportRequest, createReportExportSnapshot, createRuntimeReportConfirmHandler, day, describeRuntimeReportSource, enqueueReportRefresh, ensureReportRefreshSchedules, getReportGroupingColumns, getReportLifecycle, getRuntimeReport, getRuntimeReportOptions, getRuntimeReportServerOptions, groupBy, hour, listRuntimeReports, loadRuntimeReportChart, max, migrateReportSavedView, min, minute, month, normalizeReportSavedView, parseRuntimeReportSpec, previewReportExport, previewReportRefresh, quarter, queryReportMaterializedRows, refreshReport, registerReportRefreshExecutionAuthorityHost, registerReportRefreshInterceptor, registerReportRefreshJobIntegritySigner, report, reportDefinitionFingerprint, reportMaterializedRowKey, reportRefreshOutcome, reportRowIdentity, restoreReportSavedView, runRuntimeReport, runRuntimeReportForPrincipal, runStoredRuntimeReport, runtimeReportSpecHash, saveRuntimeReport, scopeKeyForTenant, serializeRuntimeReportSpec, splitReportFilterScopes, sum, validateReportExportArtifact, validateReportExportExecution, validateReportExportRequest, verifyReportExportSnapshot, week, year };
2588
2947
 
2589
2948
  //# sourceMappingURL=index.js.map