@supacloud/compiler 0.44.0 → 0.45.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.
- package/README.md +55 -0
- package/dist/cli.js +269 -150
- 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 +6 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +252 -133
- package/dist/migration-app-version.d.ts +1 -1
- package/dist/migration-elysia-version.d.ts +1 -1
- package/dist/types.d.ts +16 -0
- package/package.json +12 -7
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[]>;
|
|
@@ -8293,7 +8293,7 @@ function FromObject10(context, type, value) {
|
|
|
8293
8293
|
return value;
|
|
8294
8294
|
const additionalProperties = GetAdditionalProperties(type);
|
|
8295
8295
|
for (const key of Keys(value)) {
|
|
8296
|
-
if (HasPropertyKey(type.properties, key)) {
|
|
8296
|
+
if (HasPropertyKey(type.properties, key) && IsSchema2(type.properties[key])) {
|
|
8297
8297
|
value[key] = FromType19(context, type.properties[key], value[key]);
|
|
8298
8298
|
continue;
|
|
8299
8299
|
}
|
|
@@ -8425,6 +8425,9 @@ function FromProperties4(properties) {
|
|
|
8425
8425
|
result[key] = FromType20(properties[key]);
|
|
8426
8426
|
return result;
|
|
8427
8427
|
}
|
|
8428
|
+
function FromRecordKey(type) {
|
|
8429
|
+
return String2({ pattern: RecordPattern(type) });
|
|
8430
|
+
}
|
|
8428
8431
|
function FromPriorityTypes(types) {
|
|
8429
8432
|
return FromTypes6(Priority(types));
|
|
8430
8433
|
}
|
|
@@ -8432,7 +8435,7 @@ function FromTypes6(types) {
|
|
|
8432
8435
|
return types.map((type) => FromType20(type));
|
|
8433
8436
|
}
|
|
8434
8437
|
function FromType20(type) {
|
|
8435
|
-
const next = IsArray2(type) ? _Array_(FromType20(type.items), ArrayOptions(type)) : IsIntersect(type) ? Intersect(FromTypes6(type.allOf)) : IsUnion(type) ? Union(FromPriorityTypes(type.anyOf)) : IsObject2(type) ? _Object_(FromProperties4(type.properties)) : IsRecord(type) ? Record(
|
|
8438
|
+
const next = IsArray2(type) ? _Array_(FromType20(type.items), ArrayOptions(type)) : IsIntersect(type) ? Intersect(FromTypes6(type.allOf)) : IsUnion(type) ? Union(FromPriorityTypes(type.anyOf)) : IsObject2(type) ? _Object_(FromProperties4(type.properties)) : IsRecord(type) ? Record(FromRecordKey(type), FromType20(RecordValue(type))) : IsTuple(type) ? Tuple(FromTypes6(type.items)) : type;
|
|
8436
8439
|
return Modifiers(type, next);
|
|
8437
8440
|
}
|
|
8438
8441
|
function UnionPrioritySort(type) {
|
|
@@ -8448,6 +8451,7 @@ var init_union_priority_sort = __esm(() => {
|
|
|
8448
8451
|
init_type3();
|
|
8449
8452
|
init_type3();
|
|
8450
8453
|
init_type3();
|
|
8454
|
+
init_type3();
|
|
8451
8455
|
});
|
|
8452
8456
|
|
|
8453
8457
|
// node_modules/typebox/build/value/clean/clean.mjs
|
package/dist/index.d.ts
CHANGED
|
@@ -52,7 +52,7 @@ export { DEFAULT_SUPACLOUD_CONFIG, compileOptionsFromConfig, defineSupacloudConf
|
|
|
52
52
|
export type { SupaCloudConfig } from "./config";
|
|
53
53
|
export { camelName } from "./util";
|
|
54
54
|
export { ANGULAR_ENTERPRISE_RULES, CLEAN_ARCHITECTURE_RULES, MODULAR_MONOLITH_RULES, MODULE_BOUNDARY_PROFILES, getModuleBoundaryPreset, getModuleBoundaryProfile, resolveModuleBoundaries, } from "./profiles";
|
|
55
|
-
export type { ApplicationGraph, AspectRefNode, CachedModuleEntry, CheckProjectResult, CommandExecutionCapabilities, CommandNode, CompileOptions, CompileResult, CompileStats, ControllerNode, DependencyGraphCache, DependencyGraphIndex, Diagnostic, DiagnosticFix, EffectCompilerOptions, EffectErrorMappingNode, EffectRetryPolicy, EffectRouteNode, ModuleBoundaryPresetName, ModuleBoundaryProfile, ModuleBoundaryRule, ModuleNode, JobNode, ProviderKind, ProviderNode, QueryNode, RouteNode, Scope, TokenKind, TypeSafetyOptions, ValidateOptions, WatchEvent, WatchHandle, WatchOptions, FeatureSpecNode, FeatureTransitionNode, GraphqlOptions, GraphqlContractSummary, OpenApiOptions, OpenApiSecurityScheme, OpenApiServer, } from "./types";
|
|
55
|
+
export type { ApplicationGraph, AspectRefNode, CachedModuleEntry, CheckProjectResult, CommandExecutionCapabilities, CommandNode, CompileOptions, DatabaseGovernanceOptions, CompileResult, CompileStats, ControllerNode, DependencyGraphCache, DependencyGraphIndex, Diagnostic, DiagnosticFix, EffectCompilerOptions, EffectErrorMappingNode, EffectRetryPolicy, EffectRouteNode, ModuleBoundaryPresetName, ModuleBoundaryProfile, ModuleBoundaryRule, ModuleNode, JobNode, ProviderKind, ProviderNode, QueryNode, RouteNode, Scope, TokenKind, TypeSafetyOptions, ValidateOptions, WatchEvent, WatchHandle, WatchOptions, FeatureSpecNode, FeatureTransitionNode, GraphqlOptions, GraphqlContractSummary, OpenApiOptions, OpenApiSecurityScheme, OpenApiServer, } from "./types";
|
|
56
56
|
export { inspectRouteContracts, validateRouteContracts } from "./route-contracts";
|
|
57
57
|
export { validateEffectPolicies } from "./effect-governance";
|
|
58
58
|
export { pullGraphqlSchema } from "./graphql-schema";
|