@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/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,4 @@
1
+ import type { DatabaseGovernanceOptions, Diagnostic } from "./types";
2
+ export declare function enforceDatabaseGovernance(options: DatabaseGovernanceOptions): Promise<{
3
+ diagnostics: Diagnostic[];
4
+ }>;
@@ -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(RecordKey(type), FromType20(RecordValue(type))) : IsTuple(type) ? Tuple(FromTypes6(type.items)) : type;
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";