@supacloud/compiler 0.44.1 → 0.45.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/README.md +55 -0
- package/dist/cli.js +822 -10664
- package/dist/config.d.ts +2 -0
- package/dist/database-governance-runtime.d.ts +4 -0
- package/dist/database-governance-worker.d.ts +1 -0
- package/dist/database-governance-worker.js +159 -0
- package/dist/database-governance.d.ts +4 -0
- package/dist/delivery-bundle-worker.js +655 -10612
- package/dist/delivery-plan.d.ts +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1020 -10862
- package/dist/types.d.ts +16 -0
- package/package.json +9 -4
package/dist/config.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export interface SupaCloudConfig {
|
|
|
17
17
|
moduleBoundaryPreset?: ModuleBoundaryPresetName;
|
|
18
18
|
moduleBoundaries?: NonNullable<CompileOptions["moduleBoundaries"]>;
|
|
19
19
|
typeSafety?: NonNullable<CompileOptions["typeSafety"]>;
|
|
20
|
+
databaseGovernance?: NonNullable<CompileOptions["databaseGovernance"]>;
|
|
20
21
|
allowRouteCommandBindings?: boolean;
|
|
21
22
|
disallowControllerDirectDb?: boolean;
|
|
22
23
|
detectOrphanModules?: boolean;
|
|
@@ -44,6 +45,7 @@ export declare function resolveSupacloudConfig(config?: SupaCloudConfig, cwd?: s
|
|
|
44
45
|
commandCapabilities?: CommandExecutionCapabilities;
|
|
45
46
|
moduleBoundaries?: NonNullable<CompileOptions["moduleBoundaries"]>;
|
|
46
47
|
typeSafety?: NonNullable<CompileOptions["typeSafety"]>;
|
|
48
|
+
databaseGovernance?: NonNullable<CompileOptions["databaseGovernance"]>;
|
|
47
49
|
allowRouteCommandBindings?: boolean;
|
|
48
50
|
disallowControllerDirectDb?: boolean;
|
|
49
51
|
detectOrphanModules?: boolean;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// src/database-governance-runtime.ts
|
|
2
|
+
import { readFile, realpath } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import {
|
|
6
|
+
lintModule,
|
|
7
|
+
readCatalog,
|
|
8
|
+
reconcileModule
|
|
9
|
+
} from "@supacloud/db";
|
|
10
|
+
import { databaseSources } from "@supacloud/db/source-contracts";
|
|
11
|
+
var record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
12
|
+
var text = (value) => typeof value === "string" && value.trim().length > 0;
|
|
13
|
+
var texts = (value) => Array.isArray(value) && Array.from(value).every(text);
|
|
14
|
+
var declarations = (value, required, check) => Array.isArray(value) && Array.from(value).every((entry) => record(entry) && required.every((key) => text(entry[key])) && check(entry));
|
|
15
|
+
function normalizeModule(value, file) {
|
|
16
|
+
if (!record(value) || !text(value.name) || !texts(value.tables) || !declarations(value.policies, ["name", "table", "source"], (entry) => ["select", "insert", "update", "delete", "all"].includes(String(entry.operation)) && texts(entry.roles) && entry.roles.length > 0 && [entry.usingExpr, entry.checkExpr].every((expr) => expr === undefined || expr === null || typeof expr === "string") && (entry.tests === undefined || texts(entry.tests))) || !declarations(value.functions, ["name", "source"], (entry) => ["invoker", "definer"].includes(String(entry.security)) && (entry.signature === undefined || text(entry.signature)) && (entry.tests === undefined || texts(entry.tests))) || !declarations(value.triggers, ["name", "table", "source"], (entry) => entry.enabled === undefined || typeof entry.enabled === "boolean") || !declarations(value.grants, ["object", "privilege", "role", "source"], () => true)) {
|
|
17
|
+
throw new Error(`Invalid database module: ${file}`);
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
async function loadModules(projectRoot, moduleFile) {
|
|
22
|
+
const file = await withinRoot(projectRoot, moduleFile);
|
|
23
|
+
const imported = await import(pathToFileURL(file).href);
|
|
24
|
+
const exported = imported.default ?? imported.modules;
|
|
25
|
+
if (exported === undefined)
|
|
26
|
+
throw new Error(`Database module file ${moduleFile} must export default or modules`);
|
|
27
|
+
return (Array.isArray(exported) ? exported : [exported]).map((value) => normalizeModule(value, moduleFile));
|
|
28
|
+
}
|
|
29
|
+
function governanceDiagnostic(message, code = "database-governance") {
|
|
30
|
+
return {
|
|
31
|
+
severity: "error",
|
|
32
|
+
code,
|
|
33
|
+
message,
|
|
34
|
+
suggestion: "Fix the database source contract, module manifest or live catalog before compiling."
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
async function enforceDatabaseGovernance(options) {
|
|
38
|
+
const projectRoot = await realpath(options.projectRoot ?? ".");
|
|
39
|
+
const sourceReport = await databaseSources(projectRoot, "check");
|
|
40
|
+
const diagnostics = sourceReport.findings.map((finding) => governanceDiagnostic(`${finding.file}: ${finding.message}`, "database-source-contract"));
|
|
41
|
+
if (!sourceReport.ok && diagnostics.length === 0) {
|
|
42
|
+
diagnostics.push(governanceDiagnostic("Database source contract check failed without findings."));
|
|
43
|
+
}
|
|
44
|
+
if (diagnostics.length > 0)
|
|
45
|
+
return { diagnostics };
|
|
46
|
+
if (options.mode === "check")
|
|
47
|
+
return { diagnostics };
|
|
48
|
+
const modules = await loadModules(projectRoot, options.moduleFile ?? "db/modules.ts");
|
|
49
|
+
if (!modules.length || new Set(modules.map((module) => module.name)).size !== modules.length || modules.some((module) => !module.tables.length && !module.functions.length)) {
|
|
50
|
+
return { diagnostics: [governanceDiagnostic("Enforced governance requires nonempty, uniquely named database modules.")] };
|
|
51
|
+
}
|
|
52
|
+
const readSql = async (path) => readFile(await withinRoot(projectRoot, path), "utf8");
|
|
53
|
+
for (const module of modules) {
|
|
54
|
+
for (const issue of await lintModule(module, readSql)) {
|
|
55
|
+
diagnostics.push(governanceDiagnostic(`${issue.file}${issue.line ? `:${issue.line}` : ""}: ${issue.message}`, "database-sql-contract"));
|
|
56
|
+
}
|
|
57
|
+
for (const entry of [...module.functions, ...module.policies]) {
|
|
58
|
+
for (const path of entry.tests ?? [])
|
|
59
|
+
await readSql(path);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (diagnostics.length)
|
|
63
|
+
return { diagnostics };
|
|
64
|
+
const databaseUrlEnv = options.databaseUrlEnv ?? "DATABASE_URL";
|
|
65
|
+
const databaseUrl = options.databaseUrl ?? process.env[databaseUrlEnv];
|
|
66
|
+
if (!databaseUrl) {
|
|
67
|
+
diagnostics.push(governanceDiagnostic(`Database governance mode "enforce" requires ${databaseUrlEnv} for live Catalog reconciliation.`, "database-catalog-unavailable"));
|
|
68
|
+
return { diagnostics };
|
|
69
|
+
}
|
|
70
|
+
if (typeof Bun === "undefined" || typeof Bun.SQL !== "function") {
|
|
71
|
+
diagnostics.push(governanceDiagnostic("Database governance requires a Bun runtime for Catalog reconciliation.", "database-catalog-unavailable"));
|
|
72
|
+
return { diagnostics };
|
|
73
|
+
}
|
|
74
|
+
const client = new Bun.SQL(databaseUrl);
|
|
75
|
+
const executor = {
|
|
76
|
+
async query(sql, params) {
|
|
77
|
+
return await client.unsafe(sql, params ?? []);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
try {
|
|
81
|
+
const catalog = await readCatalog(executor, options.schemas ?? ["public"]);
|
|
82
|
+
for (const module of modules) {
|
|
83
|
+
for (const issue of reconcileModule(module, catalog).issues) {
|
|
84
|
+
diagnostics.push(governanceDiagnostic(`${module.name}:${issue.object}: ${issue.message}`, `database-catalog-${issue.code}`));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
diagnostics.push(governanceDiagnostic("Live Catalog reconciliation failed. Check the selected read-only database connection.", "database-catalog-unavailable"));
|
|
89
|
+
} finally {
|
|
90
|
+
await client.close({ timeout: 0 }).catch(() => {});
|
|
91
|
+
}
|
|
92
|
+
return { diagnostics };
|
|
93
|
+
}
|
|
94
|
+
async function withinRoot(root, path) {
|
|
95
|
+
const file = await realpath(resolve(root, path));
|
|
96
|
+
const local = relative(root, file);
|
|
97
|
+
if (local === ".." || local.startsWith(`..${sep}`) || isAbsolute(local))
|
|
98
|
+
throw new Error("Database path escapes project root.");
|
|
99
|
+
return file;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/database-governance.ts
|
|
103
|
+
import { execFile } from "node:child_process";
|
|
104
|
+
import { resolve as resolve2 } from "node:path";
|
|
105
|
+
import { fileURLToPath } from "node:url";
|
|
106
|
+
function assertDatabaseGovernance(value) {
|
|
107
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
108
|
+
throw new Error("Invalid databaseGovernance configuration.");
|
|
109
|
+
const entry = value;
|
|
110
|
+
if (entry["mode"] !== "off" && entry["mode"] !== "check" && entry["mode"] !== "enforce" || Object.keys(entry).some((key) => !["mode", "projectRoot", "moduleFile", "databaseUrl", "databaseUrlEnv", "schemas"].includes(key)) || ["projectRoot", "moduleFile", "databaseUrl", "databaseUrlEnv"].some((key) => entry[key] !== undefined && (typeof entry[key] !== "string" || !entry[key].trim())) || entry["databaseUrlEnv"] !== undefined && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(String(entry["databaseUrlEnv"])) || entry["schemas"] !== undefined && (!Array.isArray(entry["schemas"]) || !entry["schemas"].length || !entry["schemas"].every((schema) => typeof schema === "string" && schema.trim()))) {
|
|
111
|
+
throw new Error("Invalid databaseGovernance mode, paths, connection or schemas.");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function checkDatabaseGovernance(rootDir, value) {
|
|
115
|
+
if (value === undefined)
|
|
116
|
+
return [];
|
|
117
|
+
const failure = () => [{
|
|
118
|
+
severity: "error",
|
|
119
|
+
code: "database-governance",
|
|
120
|
+
message: "Database governance could not be verified. Check configuration, installed @supacloud/db, Bun and read-only database access."
|
|
121
|
+
}];
|
|
122
|
+
try {
|
|
123
|
+
assertDatabaseGovernance(value);
|
|
124
|
+
if (value.mode === "off")
|
|
125
|
+
return [];
|
|
126
|
+
const options = { ...value, projectRoot: resolve2(rootDir, value.projectRoot ?? ".") };
|
|
127
|
+
const worker = fileURLToPath(new URL(import.meta.url.endsWith(".ts") ? "./database-governance-worker.ts" : "./database-governance-worker.js", import.meta.url));
|
|
128
|
+
const output = await new Promise((accept, reject) => {
|
|
129
|
+
const child = execFile(process.versions.bun ? process.execPath : "bun", ["--no-env-file", worker], { timeout: 30000, killSignal: "SIGKILL", maxBuffer: 1024 * 1024 }, (error, stdout) => error ? reject(error) : accept(stdout));
|
|
130
|
+
child.stdin?.on("error", reject);
|
|
131
|
+
child.stdin?.end(JSON.stringify(options));
|
|
132
|
+
});
|
|
133
|
+
const report = JSON.parse(output);
|
|
134
|
+
if (!report || typeof report !== "object" || !("diagnostics" in report) || !Array.isArray(report.diagnostics))
|
|
135
|
+
return failure();
|
|
136
|
+
const diagnostics = [];
|
|
137
|
+
for (const item of report.diagnostics) {
|
|
138
|
+
if (!item || typeof item !== "object" || !("code" in item) || typeof item.code !== "string" || !item.code.startsWith("database-") || !("message" in item) || typeof item.message !== "string" || !("severity" in item) || item.severity !== "error")
|
|
139
|
+
return failure();
|
|
140
|
+
diagnostics.push({ severity: "error", code: item.code, message: item.message });
|
|
141
|
+
}
|
|
142
|
+
return diagnostics;
|
|
143
|
+
} catch {
|
|
144
|
+
return failure();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/database-governance-worker.ts
|
|
149
|
+
try {
|
|
150
|
+
const options = JSON.parse(await Bun.stdin.text());
|
|
151
|
+
assertDatabaseGovernance(options);
|
|
152
|
+
process.stdout.write(JSON.stringify(await enforceDatabaseGovernance(options)));
|
|
153
|
+
} catch {
|
|
154
|
+
process.stdout.write(JSON.stringify({ diagnostics: [{
|
|
155
|
+
severity: "error",
|
|
156
|
+
code: "database-governance",
|
|
157
|
+
message: "Database source, module or Catalog verification failed. Check the selected project and database."
|
|
158
|
+
}] }));
|
|
159
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { DatabaseGovernanceOptions, Diagnostic } from "./types";
|
|
2
|
+
export declare function assertDatabaseGovernance(value: unknown): asserts value is DatabaseGovernanceOptions;
|
|
3
|
+
/** A fresh, bounded process avoids stale module imports during incremental compilation. */
|
|
4
|
+
export declare function checkDatabaseGovernance(rootDir: string, value: DatabaseGovernanceOptions | undefined): Promise<Diagnostic[]>;
|