@happyvertical/smrt-reports 0.55.24 → 0.56.1

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