@sapporta/server 0.0.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 (133) hide show
  1. package/package.json +40 -0
  2. package/src/actions/action.test.ts +108 -0
  3. package/src/actions/action.ts +60 -0
  4. package/src/actions/loader.ts +47 -0
  5. package/src/api/actions.ts +124 -0
  6. package/src/api/meta-mutations.ts +922 -0
  7. package/src/api/meta.ts +222 -0
  8. package/src/api/reports.ts +98 -0
  9. package/src/api/server.ts +24 -0
  10. package/src/api/tables.ts +108 -0
  11. package/src/api/views.ts +44 -0
  12. package/src/boot.ts +206 -0
  13. package/src/cli/ai-commands.ts +220 -0
  14. package/src/cli/check.ts +169 -0
  15. package/src/cli/cli-utils.test.ts +313 -0
  16. package/src/cli/describe.test.ts +151 -0
  17. package/src/cli/describe.ts +88 -0
  18. package/src/cli/emit-result.test.ts +160 -0
  19. package/src/cli/format.ts +150 -0
  20. package/src/cli/http-client.ts +55 -0
  21. package/src/cli/index.ts +162 -0
  22. package/src/cli/init.ts +35 -0
  23. package/src/cli/project-context.ts +38 -0
  24. package/src/cli/request.ts +146 -0
  25. package/src/cli/routes.ts +418 -0
  26. package/src/cli/rows-insert-master-detail.test.ts +124 -0
  27. package/src/cli/rows-insert-master-detail.ts +186 -0
  28. package/src/cli/rows-insert.test.ts +137 -0
  29. package/src/cli/rows-insert.ts +97 -0
  30. package/src/cli/serve-single.ts +49 -0
  31. package/src/create-project.ts +81 -0
  32. package/src/data/count.ts +62 -0
  33. package/src/data/crud.test.ts +188 -0
  34. package/src/data/crud.ts +242 -0
  35. package/src/data/lookup.test.ts +96 -0
  36. package/src/data/lookup.ts +104 -0
  37. package/src/data/query-parser.test.ts +67 -0
  38. package/src/data/query-parser.ts +106 -0
  39. package/src/data/sanitize.test.ts +57 -0
  40. package/src/data/sanitize.ts +25 -0
  41. package/src/data/save-pipeline.test.ts +115 -0
  42. package/src/data/save-pipeline.ts +93 -0
  43. package/src/data/validate.test.ts +110 -0
  44. package/src/data/validate.ts +98 -0
  45. package/src/db/errors.ts +20 -0
  46. package/src/db/logger.ts +63 -0
  47. package/src/db/sqlite-connection.test.ts +59 -0
  48. package/src/db/sqlite-connection.ts +79 -0
  49. package/src/index.ts +111 -0
  50. package/src/integration/api-actions.test.ts +60 -0
  51. package/src/integration/api-global.test.ts +21 -0
  52. package/src/integration/api-meta.test.ts +252 -0
  53. package/src/integration/api-reports.test.ts +77 -0
  54. package/src/integration/api-tables.test.ts +238 -0
  55. package/src/integration/api-views.test.ts +39 -0
  56. package/src/integration/cli-routes.test.ts +167 -0
  57. package/src/integration/fixtures/actions/create-account.ts +23 -0
  58. package/src/integration/fixtures/reports/account-list.ts +25 -0
  59. package/src/integration/fixtures/schema/accounts.ts +21 -0
  60. package/src/integration/fixtures/schema/audit-log.ts +19 -0
  61. package/src/integration/fixtures/schema/journal-entries.ts +20 -0
  62. package/src/integration/fixtures/views/dashboard.tsx +4 -0
  63. package/src/integration/fixtures/views/settings.tsx +3 -0
  64. package/src/integration/setup.ts +72 -0
  65. package/src/introspect/db-helpers.ts +109 -0
  66. package/src/introspect/describe-all.test.ts +73 -0
  67. package/src/introspect/describe-all.ts +80 -0
  68. package/src/introspect/describe.test.ts +65 -0
  69. package/src/introspect/describe.ts +184 -0
  70. package/src/introspect/exec.test.ts +103 -0
  71. package/src/introspect/exec.ts +57 -0
  72. package/src/introspect/indexes.test.ts +41 -0
  73. package/src/introspect/indexes.ts +95 -0
  74. package/src/introspect/inference.ts +98 -0
  75. package/src/introspect/list-tables.test.ts +40 -0
  76. package/src/introspect/list-tables.ts +62 -0
  77. package/src/introspect/query.test.ts +77 -0
  78. package/src/introspect/query.ts +47 -0
  79. package/src/introspect/sample.test.ts +67 -0
  80. package/src/introspect/sample.ts +50 -0
  81. package/src/introspect/sql-safety.ts +76 -0
  82. package/src/introspect/sqlite/db-helpers.test.ts +79 -0
  83. package/src/introspect/sqlite/db-helpers.ts +56 -0
  84. package/src/introspect/sqlite/describe-all.ts +21 -0
  85. package/src/introspect/sqlite/describe.test.ts +160 -0
  86. package/src/introspect/sqlite/describe.ts +185 -0
  87. package/src/introspect/sqlite/exec.ts +57 -0
  88. package/src/introspect/sqlite/indexes.test.ts +60 -0
  89. package/src/introspect/sqlite/indexes.ts +96 -0
  90. package/src/introspect/sqlite/list-tables.test.ts +100 -0
  91. package/src/introspect/sqlite/list-tables.ts +67 -0
  92. package/src/introspect/sqlite/query.ts +49 -0
  93. package/src/introspect/sqlite/sample.ts +50 -0
  94. package/src/introspect/table-rename.test.ts +235 -0
  95. package/src/introspect/table-rename.ts +115 -0
  96. package/src/introspect/types.ts +95 -0
  97. package/src/reports/check.test.ts +499 -0
  98. package/src/reports/check.ts +208 -0
  99. package/src/reports/engine.test.ts +1465 -0
  100. package/src/reports/engine.ts +678 -0
  101. package/src/reports/loader.ts +55 -0
  102. package/src/reports/report.ts +308 -0
  103. package/src/reports/sql-bind.ts +161 -0
  104. package/src/reports/sqlite-bind.test.ts +98 -0
  105. package/src/reports/sqlite-bind.ts +58 -0
  106. package/src/reports/sqlite-sql-client.ts +42 -0
  107. package/src/runtime.ts +3 -0
  108. package/src/schema/check.ts +90 -0
  109. package/src/schema/ddl.test.ts +210 -0
  110. package/src/schema/ddl.ts +180 -0
  111. package/src/schema/dynamic-builder.ts +297 -0
  112. package/src/schema/extract.test.ts +261 -0
  113. package/src/schema/extract.ts +285 -0
  114. package/src/schema/loader.test.ts +31 -0
  115. package/src/schema/loader.ts +60 -0
  116. package/src/schema/metadata-io.test.ts +261 -0
  117. package/src/schema/metadata-io.ts +161 -0
  118. package/src/schema/metadata-tables.test.ts +737 -0
  119. package/src/schema/metadata-tables.ts +341 -0
  120. package/src/schema/migrate.ts +195 -0
  121. package/src/schema/normalize-datatype.test.ts +58 -0
  122. package/src/schema/normalize-datatype.ts +99 -0
  123. package/src/schema/registry.test.ts +174 -0
  124. package/src/schema/registry.ts +139 -0
  125. package/src/schema/reserved.ts +227 -0
  126. package/src/schema/table.ts +135 -0
  127. package/src/test-fixtures/schema/accounts.ts +24 -0
  128. package/src/test-fixtures/schema/not-a-table.ts +6 -0
  129. package/src/testing/test-utils.ts +44 -0
  130. package/src/views/loader.test.ts +70 -0
  131. package/src/views/loader.ts +38 -0
  132. package/src/views/view.test.ts +121 -0
  133. package/src/views/view.ts +16 -0
@@ -0,0 +1,208 @@
1
+ import type {
2
+ ReportDefinition,
3
+ ReportTreeNode,
4
+ ReportOutputNode,
5
+ } from "./report.js";
6
+ import {
7
+ extractBindVariables,
8
+ buildPositionalQuery,
9
+ } from "./engine.js";
10
+
11
+ export type CheckIssue = {
12
+ /** Dot-separated path through the tree, e.g. "section.footer[\"Grand Total\"]" */
13
+ path: string;
14
+ /** Human-readable description of the issue */
15
+ message: string;
16
+ };
17
+
18
+ /**
19
+ * Statically validate a report definition without executing any SQL.
20
+ *
21
+ * Checks performed (recursively over the tree):
22
+ * 1. Source references — each node's `source` must exist in the report's `sources` map
23
+ * 2. Rollup keys vs columns — call rollup() with empty children to discover output keys,
24
+ * warn if any key isn't in columns[] on that level
25
+ * 3. Footer keys vs columns — call each footer.compute([]) to discover output keys,
26
+ * warn if any key isn't in columns[] on that level
27
+ */
28
+ export function checkReportDefinition(def: ReportDefinition): CheckIssue[] {
29
+ const issues: CheckIssue[] = [];
30
+ walkTree(def.tree, def, issues, def.tree.levelName);
31
+ return issues;
32
+ }
33
+
34
+ function walkTree(
35
+ node: ReportTreeNode,
36
+ def: ReportDefinition,
37
+ issues: CheckIssue[],
38
+ path: string,
39
+ ): void {
40
+ const declaredNames = new Set(node.columns.map((c) => c.name));
41
+
42
+ // 1. Source reference check
43
+ if (!(node.source in def.sources)) {
44
+ issues.push({
45
+ path,
46
+ message: `source "${node.source}" not found in sources`,
47
+ });
48
+ }
49
+
50
+ // 2. Rollup keys vs columns
51
+ if (node.rollup && node.children && node.children.length > 0) {
52
+ try {
53
+ // Build mock children map: { childLevelName: [] } for each child
54
+ const mockChildren: Record<string, ReportOutputNode[]> = {};
55
+ for (const child of node.children) {
56
+ mockChildren[child.levelName] = [];
57
+ }
58
+ const rollupResult = node.rollup(mockChildren);
59
+ if (rollupResult && typeof rollupResult === "object") {
60
+ const undeclared = Object.keys(rollupResult).filter(
61
+ (k) => !declaredNames.has(k),
62
+ );
63
+ for (const key of undeclared) {
64
+ issues.push({
65
+ path: `${path}.rollup`,
66
+ message: `key "${key}" not declared in columns[] on level "${node.levelName}"`,
67
+ });
68
+ }
69
+ }
70
+ } catch {
71
+ // Rollup function threw with empty input — can't check statically
72
+ }
73
+ }
74
+
75
+ // 3. Footer keys vs columns
76
+ if (node.footer) {
77
+ for (const footer of node.footer) {
78
+ try {
79
+ const footerResult = footer.compute([]);
80
+ if (footerResult && typeof footerResult === "object") {
81
+ const undeclared = Object.keys(footerResult).filter(
82
+ (k) => !declaredNames.has(k),
83
+ );
84
+ for (const key of undeclared) {
85
+ issues.push({
86
+ path: `${path}.footer["${footer.label}"]`,
87
+ message: `key "${key}" not declared in columns[] on level "${node.levelName}"`,
88
+ });
89
+ }
90
+ }
91
+ } catch {
92
+ // Footer compute threw with empty input — can't check statically
93
+ }
94
+ }
95
+ }
96
+
97
+ // Recurse into children
98
+ if (node.children) {
99
+ for (const child of node.children) {
100
+ walkTree(child, def, issues, `${path}.${child.levelName}`);
101
+ }
102
+ }
103
+ }
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // DB-aware SQL validation: verify source queries via query planner
107
+ // ---------------------------------------------------------------------------
108
+ //
109
+ // For every source query in the report, we ask PostgreSQL's query planner to
110
+ // validate it — without needing valid parameter values or actual table data.
111
+ // We do this by replacing all $name bind variables with NULL, wrapping in a
112
+ // LIMIT 0 subquery, and executing. PostgreSQL plans (but fetches zero rows),
113
+ // catching syntax errors, missing tables, and parameter type inference failures.
114
+ //
115
+ // NOTE: This check previously also flagged undeclared SQL columns on transform
116
+ // nodes (columns returned by SQL but not in columns[]). That check was removed
117
+ // because the engine now preserves raw SQL rows via __rawRow and exposes them
118
+ // to transforms via context.rawRows and to display functions. Undeclared columns
119
+ // are no longer dropped before transforms/display can access them.
120
+ // ---------------------------------------------------------------------------
121
+
122
+ /**
123
+ * SQL interface expected by checkReportSqlColumns.
124
+ *
125
+ * Must return result arrays with a `.columns` property containing column
126
+ * metadata. The real postgres.js driver attaches this automatically.
127
+ * In tests, PGlite's `result.fields` must be adapted to this shape.
128
+ */
129
+ export interface CheckSql {
130
+ unsafe(
131
+ query: string,
132
+ params?: unknown[],
133
+ ): Promise<any[] & { columns: { name: string }[] }>;
134
+ }
135
+
136
+ /**
137
+ * Discover column names returned by a source query without fetching any data.
138
+ *
139
+ * Technique: replace all $name bind variables with NULL, wrap in a LIMIT 0
140
+ * subquery, and execute. PostgreSQL plans the query and returns column
141
+ * metadata even though zero rows are fetched. This works even when:
142
+ * - The referenced tables are empty
143
+ * - The bind variable values would normally filter out all rows
144
+ * - The query uses complex joins, CTEs, or aggregations
145
+ *
146
+ * It may fail if the SQL has syntax errors or references nonexistent tables,
147
+ * which is caught and reported as an issue by the caller.
148
+ */
149
+ async function discoverSqlColumns(
150
+ sql: CheckSql,
151
+ sourceQuery: string,
152
+ ): Promise<string[]> {
153
+ const bindVars = extractBindVariables(sourceQuery);
154
+
155
+ // Replace every bind variable with NULL. The actual values don't matter
156
+ // because LIMIT 0 means no rows are evaluated — we only need PostgreSQL
157
+ // to parse and plan the query to discover the output column names.
158
+ const nullValues: Record<string, unknown> = {};
159
+ for (const v of bindVars) {
160
+ nullValues[v] = null;
161
+ }
162
+ const { sql: processedSql, values } = buildPositionalQuery(
163
+ sourceQuery,
164
+ bindVars,
165
+ nullValues,
166
+ );
167
+
168
+ const wrapped = `SELECT * FROM (${processedSql}) AS _t LIMIT 0`;
169
+ const result = await sql.unsafe(wrapped, values);
170
+ return result.columns.map((c: { name: string }) => c.name);
171
+ }
172
+
173
+ /**
174
+ * Validate report SQL sources and check column declarations.
175
+ *
176
+ * Performs two phases:
177
+ *
178
+ * 1. **Planning validation** — every source query is sent to PostgreSQL's
179
+ * query planner (via LIMIT 0 + NULL parameters). This catches syntax
180
+ * errors, missing tables, and parameter type inference failures (e.g.
181
+ * `date_trunc($1, ...)` or `$2 IS NULL` without type context).
182
+ *
183
+ * Requires a live database connection. Gracefully handles planning
184
+ * errors by reporting them as issues rather than crashing.
185
+ */
186
+ export async function checkReportSqlColumns(
187
+ sql: CheckSql,
188
+ def: ReportDefinition,
189
+ ): Promise<CheckIssue[]> {
190
+ const issues: CheckIssue[] = [];
191
+
192
+ // Validate ALL source queries can be planned by PostgreSQL.
193
+ // This catches errors that would only surface at runtime, like parameter
194
+ // type inference failures (e.g. `$param IS NULL` combined with casts).
195
+ for (const [sourceName, source] of Object.entries(def.sources)) {
196
+ try {
197
+ await discoverSqlColumns(sql, source.query);
198
+ } catch (err: any) {
199
+ issues.push({
200
+ path: sourceName,
201
+ message:
202
+ `SQL planning failed for source "${sourceName}": ${err.message}`,
203
+ });
204
+ }
205
+ }
206
+
207
+ return issues;
208
+ }