@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.
- package/package.json +40 -0
- package/src/actions/action.test.ts +108 -0
- package/src/actions/action.ts +60 -0
- package/src/actions/loader.ts +47 -0
- package/src/api/actions.ts +124 -0
- package/src/api/meta-mutations.ts +922 -0
- package/src/api/meta.ts +222 -0
- package/src/api/reports.ts +98 -0
- package/src/api/server.ts +24 -0
- package/src/api/tables.ts +108 -0
- package/src/api/views.ts +44 -0
- package/src/boot.ts +206 -0
- package/src/cli/ai-commands.ts +220 -0
- package/src/cli/check.ts +169 -0
- package/src/cli/cli-utils.test.ts +313 -0
- package/src/cli/describe.test.ts +151 -0
- package/src/cli/describe.ts +88 -0
- package/src/cli/emit-result.test.ts +160 -0
- package/src/cli/format.ts +150 -0
- package/src/cli/http-client.ts +55 -0
- package/src/cli/index.ts +162 -0
- package/src/cli/init.ts +35 -0
- package/src/cli/project-context.ts +38 -0
- package/src/cli/request.ts +146 -0
- package/src/cli/routes.ts +418 -0
- package/src/cli/rows-insert-master-detail.test.ts +124 -0
- package/src/cli/rows-insert-master-detail.ts +186 -0
- package/src/cli/rows-insert.test.ts +137 -0
- package/src/cli/rows-insert.ts +97 -0
- package/src/cli/serve-single.ts +49 -0
- package/src/create-project.ts +81 -0
- package/src/data/count.ts +62 -0
- package/src/data/crud.test.ts +188 -0
- package/src/data/crud.ts +242 -0
- package/src/data/lookup.test.ts +96 -0
- package/src/data/lookup.ts +104 -0
- package/src/data/query-parser.test.ts +67 -0
- package/src/data/query-parser.ts +106 -0
- package/src/data/sanitize.test.ts +57 -0
- package/src/data/sanitize.ts +25 -0
- package/src/data/save-pipeline.test.ts +115 -0
- package/src/data/save-pipeline.ts +93 -0
- package/src/data/validate.test.ts +110 -0
- package/src/data/validate.ts +98 -0
- package/src/db/errors.ts +20 -0
- package/src/db/logger.ts +63 -0
- package/src/db/sqlite-connection.test.ts +59 -0
- package/src/db/sqlite-connection.ts +79 -0
- package/src/index.ts +111 -0
- package/src/integration/api-actions.test.ts +60 -0
- package/src/integration/api-global.test.ts +21 -0
- package/src/integration/api-meta.test.ts +252 -0
- package/src/integration/api-reports.test.ts +77 -0
- package/src/integration/api-tables.test.ts +238 -0
- package/src/integration/api-views.test.ts +39 -0
- package/src/integration/cli-routes.test.ts +167 -0
- package/src/integration/fixtures/actions/create-account.ts +23 -0
- package/src/integration/fixtures/reports/account-list.ts +25 -0
- package/src/integration/fixtures/schema/accounts.ts +21 -0
- package/src/integration/fixtures/schema/audit-log.ts +19 -0
- package/src/integration/fixtures/schema/journal-entries.ts +20 -0
- package/src/integration/fixtures/views/dashboard.tsx +4 -0
- package/src/integration/fixtures/views/settings.tsx +3 -0
- package/src/integration/setup.ts +72 -0
- package/src/introspect/db-helpers.ts +109 -0
- package/src/introspect/describe-all.test.ts +73 -0
- package/src/introspect/describe-all.ts +80 -0
- package/src/introspect/describe.test.ts +65 -0
- package/src/introspect/describe.ts +184 -0
- package/src/introspect/exec.test.ts +103 -0
- package/src/introspect/exec.ts +57 -0
- package/src/introspect/indexes.test.ts +41 -0
- package/src/introspect/indexes.ts +95 -0
- package/src/introspect/inference.ts +98 -0
- package/src/introspect/list-tables.test.ts +40 -0
- package/src/introspect/list-tables.ts +62 -0
- package/src/introspect/query.test.ts +77 -0
- package/src/introspect/query.ts +47 -0
- package/src/introspect/sample.test.ts +67 -0
- package/src/introspect/sample.ts +50 -0
- package/src/introspect/sql-safety.ts +76 -0
- package/src/introspect/sqlite/db-helpers.test.ts +79 -0
- package/src/introspect/sqlite/db-helpers.ts +56 -0
- package/src/introspect/sqlite/describe-all.ts +21 -0
- package/src/introspect/sqlite/describe.test.ts +160 -0
- package/src/introspect/sqlite/describe.ts +185 -0
- package/src/introspect/sqlite/exec.ts +57 -0
- package/src/introspect/sqlite/indexes.test.ts +60 -0
- package/src/introspect/sqlite/indexes.ts +96 -0
- package/src/introspect/sqlite/list-tables.test.ts +100 -0
- package/src/introspect/sqlite/list-tables.ts +67 -0
- package/src/introspect/sqlite/query.ts +49 -0
- package/src/introspect/sqlite/sample.ts +50 -0
- package/src/introspect/table-rename.test.ts +235 -0
- package/src/introspect/table-rename.ts +115 -0
- package/src/introspect/types.ts +95 -0
- package/src/reports/check.test.ts +499 -0
- package/src/reports/check.ts +208 -0
- package/src/reports/engine.test.ts +1465 -0
- package/src/reports/engine.ts +678 -0
- package/src/reports/loader.ts +55 -0
- package/src/reports/report.ts +308 -0
- package/src/reports/sql-bind.ts +161 -0
- package/src/reports/sqlite-bind.test.ts +98 -0
- package/src/reports/sqlite-bind.ts +58 -0
- package/src/reports/sqlite-sql-client.ts +42 -0
- package/src/runtime.ts +3 -0
- package/src/schema/check.ts +90 -0
- package/src/schema/ddl.test.ts +210 -0
- package/src/schema/ddl.ts +180 -0
- package/src/schema/dynamic-builder.ts +297 -0
- package/src/schema/extract.test.ts +261 -0
- package/src/schema/extract.ts +285 -0
- package/src/schema/loader.test.ts +31 -0
- package/src/schema/loader.ts +60 -0
- package/src/schema/metadata-io.test.ts +261 -0
- package/src/schema/metadata-io.ts +161 -0
- package/src/schema/metadata-tables.test.ts +737 -0
- package/src/schema/metadata-tables.ts +341 -0
- package/src/schema/migrate.ts +195 -0
- package/src/schema/normalize-datatype.test.ts +58 -0
- package/src/schema/normalize-datatype.ts +99 -0
- package/src/schema/registry.test.ts +174 -0
- package/src/schema/registry.ts +139 -0
- package/src/schema/reserved.ts +227 -0
- package/src/schema/table.ts +135 -0
- package/src/test-fixtures/schema/accounts.ts +24 -0
- package/src/test-fixtures/schema/not-a-table.ts +6 -0
- package/src/testing/test-utils.ts +44 -0
- package/src/views/loader.test.ts +70 -0
- package/src/views/loader.ts +38 -0
- package/src/views/view.test.ts +121 -0
- 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
|
+
}
|