@supacloud/compiler 0.54.0 → 0.55.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 CHANGED
@@ -85,6 +85,11 @@ attempts. The default policy requires `requireRouteEffects`,
85
85
 
86
86
  ## Unified Database Contracts
87
87
 
88
+ Database contracts are protocol-aware: configure only the database sources the
89
+ application uses (`postgrestTypes`, `drizzleSchema`, or `graphql`). The generated
90
+ `database.manifest.json` is a TypeBox-validated, fingerprinted build boundary;
91
+ consumers must reject it when its schema or migration fingerprint is stale.
92
+
88
93
  `supacloud-compiler database-contracts database-contracts.json` generates a shared
89
94
  type entry point while preserving PostgREST, Drizzle and GraphQL native types:
90
95
 
package/dist/cli.js CHANGED
@@ -2123,6 +2123,9 @@ class ModuleGenerator {
2123
2123
  const idempotency = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency;
2124
2124
  const responseEntries = normalizedResponseEntries(route.responses);
2125
2125
  const hasResponseMap = responseEntries.length > 0;
2126
+ const handlerParams = route.handlerParams ?? [];
2127
+ const needsContext = handlerParams.some((parameter) => parameter.kind === "context");
2128
+ const direct = factoryOfScope(controller.scope) === "services" && route.command === undefined && route.effect === undefined && (this.module.aspects?.length ?? 0) === 0 && !handlerParams.some((parameter) => parameter.kind === "cookie") && (route.aspects?.length ?? 0) === 0 && !hasResponseMap && route.parse !== "none" && !needsContext && (route.guards?.length ?? 0) === 0 && (route.canMatch?.length ?? 0) === 0 && (route.canDeactivate?.length ?? 0) === 0 && (route.resolvers === undefined || Object.keys(route.resolvers).length === 0) && (route.paramTransforms === undefined || Object.keys(route.paramTransforms).length === 0) && (route.queryTransforms === undefined || Object.keys(route.queryTransforms).length === 0) && (route.paramDefaults === undefined || Object.keys(route.paramDefaults).length === 0) && (route.queryDefaults === undefined || Object.keys(route.queryDefaults).length === 0);
2126
2129
  const fields = [
2127
2130
  `method: ${JSON.stringify(route.method)}`,
2128
2131
  `path: ${JSON.stringify(route.path)}`,
@@ -2192,7 +2195,7 @@ class ModuleGenerator {
2192
2195
  fields.push(`aspects: ${this.renderAspects(route.aspects)}`);
2193
2196
  fields.push(`aspectPipeline: ${this.renderAspectPipeline(route.aspects)}`);
2194
2197
  }
2195
- const invokerArgs = (route.handlerParams ?? []).map((hp) => {
2198
+ const invokerArgs = handlerParams.map((hp) => {
2196
2199
  if (hp.kind === "param") {
2197
2200
  const accessor = `req.params?.[${JSON.stringify(hp.bindingName ?? hp.name)}]`;
2198
2201
  const fallback = hp.default !== undefined ? JSON.stringify(hp.default) : "undefined";
@@ -2232,7 +2235,17 @@ class ModuleGenerator {
2232
2235
  return "undefined";
2233
2236
  });
2234
2237
  const callArgs = invokerArgs.length > 0 ? invokerArgs.join(", ") : "req";
2235
- fields.push(`invoker: async (ctrl: unknown, req: { params?: Record<string, unknown>; query?: Record<string, unknown>; body?: unknown; headers?: Record<string, unknown>; cookie?: Record<string, unknown>; context?: unknown }) => { ` + `if (!isRecord(ctrl)) throw new TypeError("Route controller is not an object"); ` + `const handler = ctrl[${JSON.stringify(route.handler)}]; ` + `if (!isFunction(handler)) throw new TypeError("Route handler ${route.handler} is not callable"); ` + `return await Reflect.apply(handler, ctrl, [${callArgs}]); }`);
2238
+ fields.push(`invoker: (ctrl: unknown, req: { params?: Record<string, unknown>; query?: Record<string, unknown>; body?: unknown; headers?: Record<string, unknown>; cookie?: Record<string, unknown>; context?: unknown }) => { ` + `if (!isRecord(ctrl)) throw new TypeError("Route controller is not an object"); ` + `const handler = ctrl[${JSON.stringify(route.handler)}]; ` + `if (!isFunction(handler)) throw new TypeError("Route handler ${route.handler} is not callable"); ` + `return handler.call(ctrl, ${callArgs}); }`);
2239
+ if (direct) {
2240
+ const directArgs = invokerArgs.map((argument) => argument.replaceAll("req.", "context.").replaceAll("req)", "context)"));
2241
+ const directCallArgs = directArgs.length > 0 ? directArgs.join(", ") : "";
2242
+ const directContext = directArgs.length > 0 ? "const context = ctx as { params?: Record<string, unknown>; query?: Record<string, unknown>; body?: unknown; headers?: Record<string, unknown>; cookie?: Record<string, unknown>; context?: unknown }; " : "";
2243
+ fields.push(`directInvoker: (ctrl: unknown, ctx: unknown) => { ` + `if (!isRecord(ctrl)) throw new TypeError("Route controller is not an object"); ` + `const handler = ctrl[${JSON.stringify(route.handler)}]; ` + `if (!isFunction(handler)) throw new TypeError("Route handler ${route.handler} is not callable"); ` + `${directContext}return handler.call(ctrl, ${directCallArgs}); }`);
2244
+ fields.push(`directInvokerFast: (ctrl: Record<string, unknown>, handler: (...args: unknown[]) => unknown, ctx: unknown) => { ` + `${directContext}return handler.call(ctrl, ${directCallArgs}); }`);
2245
+ fields.push('executionMode: "direct"');
2246
+ } else {
2247
+ fields.push('executionMode: "pipeline"');
2248
+ }
2236
2249
  if (route.contract !== undefined) {
2237
2250
  fields.push(`contract: ${JSON.stringify(route.contract)}`);
2238
2251
  }
@@ -4068,7 +4081,7 @@ var HEADER = "// GENERATED BY @supacloud/compiler — do not edit", INTERFACES =
4068
4081
  data?: Record<string, unknown>;
4069
4082
  aspects?: CompiledAspect[];
4070
4083
  aspectPipeline?: CompiledAspectPipeline;
4071
- invoker?: (
4084
+ invoker: (
4072
4085
  controller: unknown,
4073
4086
  request: {
4074
4087
  params?: Record<string, unknown>;
@@ -4079,6 +4092,9 @@ var HEADER = "// GENERATED BY @supacloud/compiler — do not edit", INTERFACES =
4079
4092
  context?: unknown;
4080
4093
  },
4081
4094
  ) => Promise<unknown> | unknown;
4095
+ directInvoker?: (controller: unknown, context: unknown) => Promise<unknown> | unknown;
4096
+ directInvokerFast?: (controller: Record<string, unknown>, handler: (...args: unknown[]) => unknown, context: unknown) => Promise<unknown> | unknown;
4097
+ executionMode?: "direct" | "pipeline";
4082
4098
  }
4083
4099
 
4084
4100
  export interface CompiledCommand {
@@ -18187,6 +18203,27 @@ var init_release_evidence_cli = __esm(() => {
18187
18203
  init_release_evidence();
18188
18204
  });
18189
18205
 
18206
+ // src/database-contract-manifest.ts
18207
+ function parseDatabaseContractManifest(value) {
18208
+ if (!Check2(databaseContractManifestSchema, value)) {
18209
+ throw new TypeError("Invalid database contract manifest");
18210
+ }
18211
+ return value;
18212
+ }
18213
+ var databaseContractManifestSchema;
18214
+ var init_database_contract_manifest = __esm(() => {
18215
+ init_build();
18216
+ init_value2();
18217
+ databaseContractManifestSchema = _Object_({
18218
+ version: Literal(1),
18219
+ role: String2({ minLength: 1 }),
18220
+ fingerprint: String2({ pattern: "^[a-f0-9]{64}$" }),
18221
+ inputs: Record(String2({ minLength: 1 }), String2({ pattern: "^[a-f0-9]{64}$" })),
18222
+ migrationOrder: _Array_(String2({ minLength: 1 })),
18223
+ outputs: Record(String2({ minLength: 1 }), String2({ pattern: "^[a-f0-9]{64}$" }))
18224
+ });
18225
+ });
18226
+
18190
18227
  // src/database-contracts.ts
18191
18228
  import { createHash as createHash13 } from "node:crypto";
18192
18229
  import { mkdir as mkdir5, readFile as readFile14 } from "node:fs/promises";
@@ -18211,8 +18248,23 @@ function parseDatabaseContractsOptions(value, directory) {
18211
18248
  throw new TypeError(`Missing database contracts ${name}`);
18212
18249
  return result;
18213
18250
  };
18214
- const graphql = Reflect.get(value, "graphql");
18215
- assertGraphqlOptions(graphql);
18251
+ const optionalPath = (name) => {
18252
+ if (!Object.hasOwn(value, name))
18253
+ return;
18254
+ return field(name);
18255
+ };
18256
+ const postgrestTypes = optionalPath("postgrestTypes");
18257
+ const drizzleSchema = optionalPath("drizzleSchema");
18258
+ const graphqlPresent = Object.hasOwn(value, "graphql");
18259
+ if (!postgrestTypes && !drizzleSchema && !graphqlPresent) {
18260
+ throw new TypeError("Database contracts require postgrestTypes, drizzleSchema, or graphql");
18261
+ }
18262
+ let graphql;
18263
+ if (graphqlPresent) {
18264
+ const candidate = Reflect.get(value, "graphql");
18265
+ assertGraphqlOptions(candidate);
18266
+ graphql = { ...candidate, schema: resolve25(directory, candidate.schema) };
18267
+ }
18216
18268
  const migrations = Reflect.get(value, "migrations");
18217
18269
  if (!Array.isArray(migrations) || !migrations.every((entry) => typeof entry === "string" && entry.endsWith(".sql"))) {
18218
18270
  throw new TypeError("migrations must be an ordered list of SQL files");
@@ -18220,22 +18272,20 @@ function parseDatabaseContractsOptions(value, directory) {
18220
18272
  return {
18221
18273
  rootDir: resolve25(directory, field("rootDir")),
18222
18274
  outDir: resolve25(directory, field("outDir")),
18223
- postgrestTypes: resolve25(directory, field("postgrestTypes")),
18224
- drizzleSchema: resolve25(directory, field("drizzleSchema")),
18275
+ ...postgrestTypes ? { postgrestTypes: resolve25(directory, postgrestTypes) } : {},
18276
+ ...drizzleSchema ? { drizzleSchema: resolve25(directory, drizzleSchema) } : {},
18225
18277
  role: field("role"),
18226
- graphql: { ...graphql, schema: resolve25(directory, graphql.schema) },
18278
+ ...graphql ? { graphql } : {},
18227
18279
  migrations: migrations.map((file) => resolve25(directory, file))
18228
18280
  };
18229
18281
  }
18230
- async function generateDatabaseContracts(options, check = false) {
18231
- const rootDir = resolve25(options.rootDir), outDir = resolve25(options.outDir);
18232
- const postgrestTypes = resolve25(rootDir, options.postgrestTypes), drizzleSchema = resolve25(rootDir, options.drizzleSchema);
18233
- const snapshot = await readFile14(postgrestTypes, "utf8");
18234
- const syntax = ts18.createSourceFile(postgrestTypes, snapshot, ts18.ScriptTarget.Latest, true);
18282
+ async function assertPostgrestSnapshot(path) {
18283
+ const snapshot = await readFile14(path, "utf8");
18284
+ const syntax = ts18.createSourceFile(path, snapshot, ts18.ScriptTarget.Latest, true);
18235
18285
  const database = syntax.statements.find((node) => (ts18.isTypeAliasDeclaration(node) || ts18.isInterfaceDeclaration(node)) && node.name.text === "Database" && node.modifiers?.some((modifier) => modifier.kind === ts18.SyntaxKind.ExportKeyword));
18236
18286
  if (!database)
18237
18287
  throw new Error("PostgREST snapshot must export Database from the official type generator");
18238
- const program = ts18.createProgram([postgrestTypes], {
18288
+ const program = ts18.createProgram([path], {
18239
18289
  strict: true,
18240
18290
  noEmit: true,
18241
18291
  skipLibCheck: true,
@@ -18246,11 +18296,17 @@ async function generateDatabaseContracts(options, check = false) {
18246
18296
  });
18247
18297
  if (ts18.getPreEmitDiagnostics(program).length)
18248
18298
  throw new Error("PostgREST snapshot has TypeScript errors");
18249
- const artifacts = await renderGraphql({
18250
- rootDir,
18251
- outDir,
18252
- graphql: options.graphql
18253
- });
18299
+ }
18300
+ async function generateDatabaseContracts(options, check = false) {
18301
+ const rootDir = resolve25(options.rootDir), outDir = resolve25(options.outDir);
18302
+ const postgrestTypes = options.postgrestTypes ? resolve25(rootDir, options.postgrestTypes) : undefined;
18303
+ const drizzleSchema = options.drizzleSchema ? resolve25(rootDir, options.drizzleSchema) : undefined;
18304
+ if (!postgrestTypes && !drizzleSchema && !options.graphql) {
18305
+ throw new TypeError("Database contracts require postgrestTypes, drizzleSchema, or graphql");
18306
+ }
18307
+ if (postgrestTypes)
18308
+ await assertPostgrestSnapshot(postgrestTypes);
18309
+ const artifacts = options.graphql ? await renderGraphql({ rootDir, outDir, graphql: options.graphql }) : { diagnostics: [], files: {} };
18254
18310
  if (artifacts.diagnostics.some((item) => item.severity === "error")) {
18255
18311
  throw new Error(artifacts.diagnostics.map((item) => item.message).join(`
18256
18312
  `));
@@ -18260,38 +18316,42 @@ async function generateDatabaseContracts(options, check = false) {
18260
18316
  const absolute = resolve25(rootDir, path);
18261
18317
  inputs[relative13(rootDir, absolute).replaceAll("\\", "/")] = hash4(await readFile14(absolute, "utf8"));
18262
18318
  };
18263
- await addInput(postgrestTypes);
18264
- await addInput(drizzleSchema);
18265
- const drizzleProgram = ts18.createProgram([drizzleSchema], {
18266
- noEmit: true,
18267
- moduleResolution: ts18.ModuleResolutionKind.Bundler,
18268
- module: ts18.ModuleKind.ESNext,
18269
- target: ts18.ScriptTarget.ES2022,
18270
- types: [],
18271
- skipLibCheck: true
18272
- });
18273
- for (const source of drizzleProgram.getSourceFiles()) {
18274
- if (!source.isDeclarationFile && !source.fileName.includes("/node_modules/"))
18275
- await addInput(source.fileName);
18319
+ if (postgrestTypes)
18320
+ await addInput(postgrestTypes);
18321
+ if (drizzleSchema) {
18322
+ await addInput(drizzleSchema);
18323
+ const drizzleProgram = ts18.createProgram([drizzleSchema], {
18324
+ noEmit: true,
18325
+ moduleResolution: ts18.ModuleResolutionKind.Bundler,
18326
+ module: ts18.ModuleKind.ESNext,
18327
+ target: ts18.ScriptTarget.ES2022,
18328
+ types: [],
18329
+ skipLibCheck: true
18330
+ });
18331
+ for (const source of drizzleProgram.getSourceFiles()) {
18332
+ if (!source.isDeclarationFile && !source.fileName.includes("/node_modules/"))
18333
+ await addInput(source.fileName);
18334
+ }
18276
18335
  }
18277
- await addInput(resolve25(rootDir, options.graphql.schema));
18336
+ if (options.graphql)
18337
+ await addInput(resolve25(rootDir, options.graphql.schema));
18278
18338
  if (new Set(options.migrations.map((path) => resolve25(rootDir, path))).size !== options.migrations.length) {
18279
18339
  throw new Error("Duplicate migration in database contracts configuration");
18280
18340
  }
18281
18341
  for (const path of options.migrations)
18282
18342
  await addInput(path);
18283
- const files = {
18284
- ...artifacts.files,
18285
- "database.ts": [
18286
- "// GENERATED BY @supacloud/compiler database-contracts. Do not edit.",
18287
- `export type { Database } from ${JSON.stringify(importPath(outDir, postgrestTypes))};`,
18288
- 'export type { QueryData, QueryResult, QueryError } from "@supabase/supabase-js";',
18289
- `export type DrizzleSchema = typeof import(${JSON.stringify(importPath(outDir, drizzleSchema))});`,
18290
- 'export * from "./graphql";',
18291
- ""
18292
- ].join(`
18293
- `)
18294
- };
18343
+ const barrel = ["// GENERATED BY @supacloud/compiler database-contracts. Do not edit."];
18344
+ if (postgrestTypes) {
18345
+ barrel.push(`export type { Database } from ${JSON.stringify(importPath(outDir, postgrestTypes))};`);
18346
+ barrel.push('export type { QueryData, QueryResult, QueryError } from "@supabase/supabase-js";');
18347
+ }
18348
+ if (drizzleSchema)
18349
+ barrel.push(`export type DrizzleSchema = typeof import(${JSON.stringify(importPath(outDir, drizzleSchema))});`);
18350
+ if (options.graphql)
18351
+ barrel.push('export * from "./graphql";');
18352
+ barrel.push("");
18353
+ const files = { ...artifacts.files, "database.ts": barrel.join(`
18354
+ `) };
18295
18355
  const manifest = {
18296
18356
  version: 1,
18297
18357
  role: options.role,
@@ -18299,7 +18359,12 @@ async function generateDatabaseContracts(options, check = false) {
18299
18359
  migrationOrder: options.migrations.map((path) => relative13(rootDir, resolve25(rootDir, path)).replaceAll("\\", "/")),
18300
18360
  outputs: Object.fromEntries(Object.entries(files).sort(([a], [b]) => a.localeCompare(b)).map(([file, text]) => [file, hash4(text)]))
18301
18361
  };
18302
- files["database.manifest.json"] = JSON.stringify(manifest, null, 2) + `
18362
+ const fingerprint = hash4(JSON.stringify({
18363
+ inputs: manifest.inputs,
18364
+ migrationOrder: manifest.migrationOrder
18365
+ }));
18366
+ const typedManifest = parseDatabaseContractManifest({ ...manifest, fingerprint });
18367
+ files["database.manifest.json"] = JSON.stringify(typedManifest, null, 2) + `
18303
18368
  `;
18304
18369
  const mismatches = [];
18305
18370
  for (const [file, content] of Object.entries(files)) {
@@ -18319,7 +18384,7 @@ async function generateDatabaseContracts(options, check = false) {
18319
18384
  for (const [file, content] of Object.entries(files))
18320
18385
  await writeFileIfChanged(resolve25(outDir, file), content);
18321
18386
  }
18322
- return { upToDate: mismatches.length === 0, mismatches, written: check ? [] : mismatches, manifest };
18387
+ return { upToDate: mismatches.length === 0, mismatches, written: check ? [] : mismatches, manifest: typedManifest };
18323
18388
  }
18324
18389
  async function runDatabaseContractsFile(path, check = false) {
18325
18390
  const absolute = resolve25(path);
@@ -18330,6 +18395,7 @@ var init_database_contracts = __esm(() => {
18330
18395
  init_graphql();
18331
18396
  init_generate();
18332
18397
  init_graphql_options();
18398
+ init_database_contract_manifest();
18333
18399
  });
18334
18400
 
18335
18401
  // src/graphql-schema.ts
@@ -25818,10 +25884,10 @@ import { readFileSync as readFileSync4 } from "node:fs";
25818
25884
  import { resolve as resolve19 } from "node:path";
25819
25885
 
25820
25886
  // src/migration-app-version.ts
25821
- var MIGRATION_APP_VERSION = "0.36.0";
25887
+ var MIGRATION_APP_VERSION = "0.36.1";
25822
25888
 
25823
25889
  // src/migration-elysia-version.ts
25824
- var MIGRATION_ELYSIA_VERSION = "0.38.0";
25890
+ var MIGRATION_ELYSIA_VERSION = "0.38.2";
25825
25891
 
25826
25892
  // src/migration-policy.ts
25827
25893
  var cachedCompilerVersion;
@@ -0,0 +1,9 @@
1
+ export interface DatabaseContractManifest {
2
+ readonly version: 1;
3
+ readonly role: string;
4
+ readonly fingerprint: string;
5
+ readonly inputs: Readonly<Record<string, string>>;
6
+ readonly migrationOrder: readonly string[];
7
+ readonly outputs: Readonly<Record<string, string>>;
8
+ }
9
+ export declare function parseDatabaseContractManifest(value: unknown): DatabaseContractManifest;
@@ -1,14 +1,16 @@
1
+ import { type DatabaseContractManifest } from "./database-contract-manifest";
1
2
  import type { GraphqlOptions } from "./types";
2
3
  export interface DatabaseContractsOptions {
3
4
  rootDir: string;
4
5
  outDir: string;
5
- /** Generated by the official Supabase/Postgres Meta exporter, not a handwritten table map. */
6
- postgrestTypes: string;
7
- /** Source module defining Drizzle tables; imported as types, never executed by this generator. */
8
- drizzleSchema: string;
6
+ /** Official PostgREST snapshot. Omit when the project does not use PostgREST. */
7
+ postgrestTypes?: string;
8
+ /** Drizzle query-builder module. Omit when the project does not use Drizzle. */
9
+ drizzleSchema?: string;
9
10
  /** Explicit identity/role label for reviewing snapshot provenance. Not an authorization proof. */
10
11
  role: string;
11
- graphql: GraphqlOptions;
12
+ /** Role-scoped pg_graphql snapshot exported by graphql-schema. Omit when GraphQL is unused. */
13
+ graphql?: GraphqlOptions;
12
14
  /** Ordered complete migration history. */
13
15
  migrations: readonly string[];
14
16
  }
@@ -18,31 +20,11 @@ export declare function generateDatabaseContracts(options: DatabaseContractsOpti
18
20
  upToDate: boolean;
19
21
  mismatches: string[];
20
22
  written: string[];
21
- manifest: {
22
- version: number;
23
- role: string;
24
- inputs: {
25
- [k: string]: string;
26
- };
27
- migrationOrder: string[];
28
- outputs: {
29
- [k: string]: string;
30
- };
31
- };
23
+ manifest: DatabaseContractManifest;
32
24
  }>;
33
25
  export declare function runDatabaseContractsFile(path: string, check?: boolean): Promise<{
34
26
  upToDate: boolean;
35
27
  mismatches: string[];
36
28
  written: string[];
37
- manifest: {
38
- version: number;
39
- role: string;
40
- inputs: {
41
- [k: string]: string;
42
- };
43
- migrationOrder: string[];
44
- outputs: {
45
- [k: string]: string;
46
- };
47
- };
29
+ manifest: DatabaseContractManifest;
48
30
  }>;
package/dist/index.d.ts CHANGED
@@ -60,3 +60,5 @@ export type { PullGraphqlSchemaOptions } from "./graphql-schema";
60
60
  export { checkGraphqlCompatibility, createRoleSnapshotReport, diffGraphqlSchemas, graphqlFeatureMatrix, introspectionQuery, readGraphqlSchema, schemaFromSource, } from "./graphql-governance";
61
61
  export type { GraphqlCompatibilityReport, GraphqlFeatureMatrix, GraphqlGovernanceDiagnostic, GraphqlGovernancePolicy, GraphqlOperationReport, GraphqlRoleSnapshot, GraphqlRoleSnapshotReport, GraphqlSchemaChange, GraphqlSchemaDiff, } from "./graphql-governance";
62
62
  export { generateDatabaseContracts, parseDatabaseContractsOptions, runDatabaseContractsFile, type DatabaseContractsOptions } from "./database-contracts";
63
+ export { parseDatabaseContractManifest } from "./database-contract-manifest";
64
+ export type { DatabaseContractManifest } from "./database-contract-manifest";
package/dist/index.js CHANGED
@@ -2122,6 +2122,9 @@ class ModuleGenerator {
2122
2122
  const idempotency = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency;
2123
2123
  const responseEntries = normalizedResponseEntries(route.responses);
2124
2124
  const hasResponseMap = responseEntries.length > 0;
2125
+ const handlerParams = route.handlerParams ?? [];
2126
+ const needsContext = handlerParams.some((parameter) => parameter.kind === "context");
2127
+ const direct = factoryOfScope(controller.scope) === "services" && route.command === undefined && route.effect === undefined && (this.module.aspects?.length ?? 0) === 0 && !handlerParams.some((parameter) => parameter.kind === "cookie") && (route.aspects?.length ?? 0) === 0 && !hasResponseMap && route.parse !== "none" && !needsContext && (route.guards?.length ?? 0) === 0 && (route.canMatch?.length ?? 0) === 0 && (route.canDeactivate?.length ?? 0) === 0 && (route.resolvers === undefined || Object.keys(route.resolvers).length === 0) && (route.paramTransforms === undefined || Object.keys(route.paramTransforms).length === 0) && (route.queryTransforms === undefined || Object.keys(route.queryTransforms).length === 0) && (route.paramDefaults === undefined || Object.keys(route.paramDefaults).length === 0) && (route.queryDefaults === undefined || Object.keys(route.queryDefaults).length === 0);
2125
2128
  const fields = [
2126
2129
  `method: ${JSON.stringify(route.method)}`,
2127
2130
  `path: ${JSON.stringify(route.path)}`,
@@ -2191,7 +2194,7 @@ class ModuleGenerator {
2191
2194
  fields.push(`aspects: ${this.renderAspects(route.aspects)}`);
2192
2195
  fields.push(`aspectPipeline: ${this.renderAspectPipeline(route.aspects)}`);
2193
2196
  }
2194
- const invokerArgs = (route.handlerParams ?? []).map((hp) => {
2197
+ const invokerArgs = handlerParams.map((hp) => {
2195
2198
  if (hp.kind === "param") {
2196
2199
  const accessor = `req.params?.[${JSON.stringify(hp.bindingName ?? hp.name)}]`;
2197
2200
  const fallback = hp.default !== undefined ? JSON.stringify(hp.default) : "undefined";
@@ -2231,7 +2234,17 @@ class ModuleGenerator {
2231
2234
  return "undefined";
2232
2235
  });
2233
2236
  const callArgs = invokerArgs.length > 0 ? invokerArgs.join(", ") : "req";
2234
- fields.push(`invoker: async (ctrl: unknown, req: { params?: Record<string, unknown>; query?: Record<string, unknown>; body?: unknown; headers?: Record<string, unknown>; cookie?: Record<string, unknown>; context?: unknown }) => { ` + `if (!isRecord(ctrl)) throw new TypeError("Route controller is not an object"); ` + `const handler = ctrl[${JSON.stringify(route.handler)}]; ` + `if (!isFunction(handler)) throw new TypeError("Route handler ${route.handler} is not callable"); ` + `return await Reflect.apply(handler, ctrl, [${callArgs}]); }`);
2237
+ fields.push(`invoker: (ctrl: unknown, req: { params?: Record<string, unknown>; query?: Record<string, unknown>; body?: unknown; headers?: Record<string, unknown>; cookie?: Record<string, unknown>; context?: unknown }) => { ` + `if (!isRecord(ctrl)) throw new TypeError("Route controller is not an object"); ` + `const handler = ctrl[${JSON.stringify(route.handler)}]; ` + `if (!isFunction(handler)) throw new TypeError("Route handler ${route.handler} is not callable"); ` + `return handler.call(ctrl, ${callArgs}); }`);
2238
+ if (direct) {
2239
+ const directArgs = invokerArgs.map((argument) => argument.replaceAll("req.", "context.").replaceAll("req)", "context)"));
2240
+ const directCallArgs = directArgs.length > 0 ? directArgs.join(", ") : "";
2241
+ const directContext = directArgs.length > 0 ? "const context = ctx as { params?: Record<string, unknown>; query?: Record<string, unknown>; body?: unknown; headers?: Record<string, unknown>; cookie?: Record<string, unknown>; context?: unknown }; " : "";
2242
+ fields.push(`directInvoker: (ctrl: unknown, ctx: unknown) => { ` + `if (!isRecord(ctrl)) throw new TypeError("Route controller is not an object"); ` + `const handler = ctrl[${JSON.stringify(route.handler)}]; ` + `if (!isFunction(handler)) throw new TypeError("Route handler ${route.handler} is not callable"); ` + `${directContext}return handler.call(ctrl, ${directCallArgs}); }`);
2243
+ fields.push(`directInvokerFast: (ctrl: Record<string, unknown>, handler: (...args: unknown[]) => unknown, ctx: unknown) => { ` + `${directContext}return handler.call(ctrl, ${directCallArgs}); }`);
2244
+ fields.push('executionMode: "direct"');
2245
+ } else {
2246
+ fields.push('executionMode: "pipeline"');
2247
+ }
2235
2248
  if (route.contract !== undefined) {
2236
2249
  fields.push(`contract: ${JSON.stringify(route.contract)}`);
2237
2250
  }
@@ -4067,7 +4080,7 @@ var HEADER = "// GENERATED BY @supacloud/compiler — do not edit", INTERFACES =
4067
4080
  data?: Record<string, unknown>;
4068
4081
  aspects?: CompiledAspect[];
4069
4082
  aspectPipeline?: CompiledAspectPipeline;
4070
- invoker?: (
4083
+ invoker: (
4071
4084
  controller: unknown,
4072
4085
  request: {
4073
4086
  params?: Record<string, unknown>;
@@ -4078,6 +4091,9 @@ var HEADER = "// GENERATED BY @supacloud/compiler — do not edit", INTERFACES =
4078
4091
  context?: unknown;
4079
4092
  },
4080
4093
  ) => Promise<unknown> | unknown;
4094
+ directInvoker?: (controller: unknown, context: unknown) => Promise<unknown> | unknown;
4095
+ directInvokerFast?: (controller: Record<string, unknown>, handler: (...args: unknown[]) => unknown, context: unknown) => Promise<unknown> | unknown;
4096
+ executionMode?: "direct" | "pipeline";
4081
4097
  }
4082
4098
 
4083
4099
  export interface CompiledCommand {
@@ -18219,6 +18235,27 @@ var init_graphql_schema = __esm(() => {
18219
18235
  init_graphql_schema_hashes();
18220
18236
  });
18221
18237
 
18238
+ // src/database-contract-manifest.ts
18239
+ function parseDatabaseContractManifest(value) {
18240
+ if (!Check2(databaseContractManifestSchema, value)) {
18241
+ throw new TypeError("Invalid database contract manifest");
18242
+ }
18243
+ return value;
18244
+ }
18245
+ var databaseContractManifestSchema;
18246
+ var init_database_contract_manifest = __esm(() => {
18247
+ init_build();
18248
+ init_value2();
18249
+ databaseContractManifestSchema = _Object_({
18250
+ version: Literal(1),
18251
+ role: String2({ minLength: 1 }),
18252
+ fingerprint: String2({ pattern: "^[a-f0-9]{64}$" }),
18253
+ inputs: Record(String2({ minLength: 1 }), String2({ pattern: "^[a-f0-9]{64}$" })),
18254
+ migrationOrder: _Array_(String2({ minLength: 1 })),
18255
+ outputs: Record(String2({ minLength: 1 }), String2({ pattern: "^[a-f0-9]{64}$" }))
18256
+ });
18257
+ });
18258
+
18222
18259
  // src/database-contracts.ts
18223
18260
  import { createHash as createHash12 } from "node:crypto";
18224
18261
  import { mkdir as mkdir6, readFile as readFile14 } from "node:fs/promises";
@@ -18243,8 +18280,23 @@ function parseDatabaseContractsOptions(value, directory) {
18243
18280
  throw new TypeError(`Missing database contracts ${name}`);
18244
18281
  return result;
18245
18282
  };
18246
- const graphql = Reflect.get(value, "graphql");
18247
- assertGraphqlOptions(graphql);
18283
+ const optionalPath = (name) => {
18284
+ if (!Object.hasOwn(value, name))
18285
+ return;
18286
+ return field(name);
18287
+ };
18288
+ const postgrestTypes = optionalPath("postgrestTypes");
18289
+ const drizzleSchema = optionalPath("drizzleSchema");
18290
+ const graphqlPresent = Object.hasOwn(value, "graphql");
18291
+ if (!postgrestTypes && !drizzleSchema && !graphqlPresent) {
18292
+ throw new TypeError("Database contracts require postgrestTypes, drizzleSchema, or graphql");
18293
+ }
18294
+ let graphql;
18295
+ if (graphqlPresent) {
18296
+ const candidate = Reflect.get(value, "graphql");
18297
+ assertGraphqlOptions(candidate);
18298
+ graphql = { ...candidate, schema: resolve24(directory, candidate.schema) };
18299
+ }
18248
18300
  const migrations = Reflect.get(value, "migrations");
18249
18301
  if (!Array.isArray(migrations) || !migrations.every((entry) => typeof entry === "string" && entry.endsWith(".sql"))) {
18250
18302
  throw new TypeError("migrations must be an ordered list of SQL files");
@@ -18252,22 +18304,20 @@ function parseDatabaseContractsOptions(value, directory) {
18252
18304
  return {
18253
18305
  rootDir: resolve24(directory, field("rootDir")),
18254
18306
  outDir: resolve24(directory, field("outDir")),
18255
- postgrestTypes: resolve24(directory, field("postgrestTypes")),
18256
- drizzleSchema: resolve24(directory, field("drizzleSchema")),
18307
+ ...postgrestTypes ? { postgrestTypes: resolve24(directory, postgrestTypes) } : {},
18308
+ ...drizzleSchema ? { drizzleSchema: resolve24(directory, drizzleSchema) } : {},
18257
18309
  role: field("role"),
18258
- graphql: { ...graphql, schema: resolve24(directory, graphql.schema) },
18310
+ ...graphql ? { graphql } : {},
18259
18311
  migrations: migrations.map((file) => resolve24(directory, file))
18260
18312
  };
18261
18313
  }
18262
- async function generateDatabaseContracts(options, check = false) {
18263
- const rootDir = resolve24(options.rootDir), outDir = resolve24(options.outDir);
18264
- const postgrestTypes = resolve24(rootDir, options.postgrestTypes), drizzleSchema = resolve24(rootDir, options.drizzleSchema);
18265
- const snapshot = await readFile14(postgrestTypes, "utf8");
18266
- const syntax = ts18.createSourceFile(postgrestTypes, snapshot, ts18.ScriptTarget.Latest, true);
18314
+ async function assertPostgrestSnapshot(path) {
18315
+ const snapshot = await readFile14(path, "utf8");
18316
+ const syntax = ts18.createSourceFile(path, snapshot, ts18.ScriptTarget.Latest, true);
18267
18317
  const database = syntax.statements.find((node) => (ts18.isTypeAliasDeclaration(node) || ts18.isInterfaceDeclaration(node)) && node.name.text === "Database" && node.modifiers?.some((modifier) => modifier.kind === ts18.SyntaxKind.ExportKeyword));
18268
18318
  if (!database)
18269
18319
  throw new Error("PostgREST snapshot must export Database from the official type generator");
18270
- const program = ts18.createProgram([postgrestTypes], {
18320
+ const program = ts18.createProgram([path], {
18271
18321
  strict: true,
18272
18322
  noEmit: true,
18273
18323
  skipLibCheck: true,
@@ -18278,11 +18328,17 @@ async function generateDatabaseContracts(options, check = false) {
18278
18328
  });
18279
18329
  if (ts18.getPreEmitDiagnostics(program).length)
18280
18330
  throw new Error("PostgREST snapshot has TypeScript errors");
18281
- const artifacts = await renderGraphql({
18282
- rootDir,
18283
- outDir,
18284
- graphql: options.graphql
18285
- });
18331
+ }
18332
+ async function generateDatabaseContracts(options, check = false) {
18333
+ const rootDir = resolve24(options.rootDir), outDir = resolve24(options.outDir);
18334
+ const postgrestTypes = options.postgrestTypes ? resolve24(rootDir, options.postgrestTypes) : undefined;
18335
+ const drizzleSchema = options.drizzleSchema ? resolve24(rootDir, options.drizzleSchema) : undefined;
18336
+ if (!postgrestTypes && !drizzleSchema && !options.graphql) {
18337
+ throw new TypeError("Database contracts require postgrestTypes, drizzleSchema, or graphql");
18338
+ }
18339
+ if (postgrestTypes)
18340
+ await assertPostgrestSnapshot(postgrestTypes);
18341
+ const artifacts = options.graphql ? await renderGraphql({ rootDir, outDir, graphql: options.graphql }) : { diagnostics: [], files: {} };
18286
18342
  if (artifacts.diagnostics.some((item) => item.severity === "error")) {
18287
18343
  throw new Error(artifacts.diagnostics.map((item) => item.message).join(`
18288
18344
  `));
@@ -18292,38 +18348,42 @@ async function generateDatabaseContracts(options, check = false) {
18292
18348
  const absolute = resolve24(rootDir, path);
18293
18349
  inputs[relative12(rootDir, absolute).replaceAll("\\", "/")] = hash4(await readFile14(absolute, "utf8"));
18294
18350
  };
18295
- await addInput(postgrestTypes);
18296
- await addInput(drizzleSchema);
18297
- const drizzleProgram = ts18.createProgram([drizzleSchema], {
18298
- noEmit: true,
18299
- moduleResolution: ts18.ModuleResolutionKind.Bundler,
18300
- module: ts18.ModuleKind.ESNext,
18301
- target: ts18.ScriptTarget.ES2022,
18302
- types: [],
18303
- skipLibCheck: true
18304
- });
18305
- for (const source of drizzleProgram.getSourceFiles()) {
18306
- if (!source.isDeclarationFile && !source.fileName.includes("/node_modules/"))
18307
- await addInput(source.fileName);
18351
+ if (postgrestTypes)
18352
+ await addInput(postgrestTypes);
18353
+ if (drizzleSchema) {
18354
+ await addInput(drizzleSchema);
18355
+ const drizzleProgram = ts18.createProgram([drizzleSchema], {
18356
+ noEmit: true,
18357
+ moduleResolution: ts18.ModuleResolutionKind.Bundler,
18358
+ module: ts18.ModuleKind.ESNext,
18359
+ target: ts18.ScriptTarget.ES2022,
18360
+ types: [],
18361
+ skipLibCheck: true
18362
+ });
18363
+ for (const source of drizzleProgram.getSourceFiles()) {
18364
+ if (!source.isDeclarationFile && !source.fileName.includes("/node_modules/"))
18365
+ await addInput(source.fileName);
18366
+ }
18308
18367
  }
18309
- await addInput(resolve24(rootDir, options.graphql.schema));
18368
+ if (options.graphql)
18369
+ await addInput(resolve24(rootDir, options.graphql.schema));
18310
18370
  if (new Set(options.migrations.map((path) => resolve24(rootDir, path))).size !== options.migrations.length) {
18311
18371
  throw new Error("Duplicate migration in database contracts configuration");
18312
18372
  }
18313
18373
  for (const path of options.migrations)
18314
18374
  await addInput(path);
18315
- const files = {
18316
- ...artifacts.files,
18317
- "database.ts": [
18318
- "// GENERATED BY @supacloud/compiler database-contracts. Do not edit.",
18319
- `export type { Database } from ${JSON.stringify(importPath(outDir, postgrestTypes))};`,
18320
- 'export type { QueryData, QueryResult, QueryError } from "@supabase/supabase-js";',
18321
- `export type DrizzleSchema = typeof import(${JSON.stringify(importPath(outDir, drizzleSchema))});`,
18322
- 'export * from "./graphql";',
18323
- ""
18324
- ].join(`
18325
- `)
18326
- };
18375
+ const barrel = ["// GENERATED BY @supacloud/compiler database-contracts. Do not edit."];
18376
+ if (postgrestTypes) {
18377
+ barrel.push(`export type { Database } from ${JSON.stringify(importPath(outDir, postgrestTypes))};`);
18378
+ barrel.push('export type { QueryData, QueryResult, QueryError } from "@supabase/supabase-js";');
18379
+ }
18380
+ if (drizzleSchema)
18381
+ barrel.push(`export type DrizzleSchema = typeof import(${JSON.stringify(importPath(outDir, drizzleSchema))});`);
18382
+ if (options.graphql)
18383
+ barrel.push('export * from "./graphql";');
18384
+ barrel.push("");
18385
+ const files = { ...artifacts.files, "database.ts": barrel.join(`
18386
+ `) };
18327
18387
  const manifest = {
18328
18388
  version: 1,
18329
18389
  role: options.role,
@@ -18331,7 +18391,12 @@ async function generateDatabaseContracts(options, check = false) {
18331
18391
  migrationOrder: options.migrations.map((path) => relative12(rootDir, resolve24(rootDir, path)).replaceAll("\\", "/")),
18332
18392
  outputs: Object.fromEntries(Object.entries(files).sort(([a], [b]) => a.localeCompare(b)).map(([file, text]) => [file, hash4(text)]))
18333
18393
  };
18334
- files["database.manifest.json"] = JSON.stringify(manifest, null, 2) + `
18394
+ const fingerprint = hash4(JSON.stringify({
18395
+ inputs: manifest.inputs,
18396
+ migrationOrder: manifest.migrationOrder
18397
+ }));
18398
+ const typedManifest = parseDatabaseContractManifest({ ...manifest, fingerprint });
18399
+ files["database.manifest.json"] = JSON.stringify(typedManifest, null, 2) + `
18335
18400
  `;
18336
18401
  const mismatches = [];
18337
18402
  for (const [file, content] of Object.entries(files)) {
@@ -18351,7 +18416,7 @@ async function generateDatabaseContracts(options, check = false) {
18351
18416
  for (const [file, content] of Object.entries(files))
18352
18417
  await writeFileIfChanged(resolve24(outDir, file), content);
18353
18418
  }
18354
- return { upToDate: mismatches.length === 0, mismatches, written: check ? [] : mismatches, manifest };
18419
+ return { upToDate: mismatches.length === 0, mismatches, written: check ? [] : mismatches, manifest: typedManifest };
18355
18420
  }
18356
18421
  async function runDatabaseContractsFile(path, check = false) {
18357
18422
  const absolute = resolve24(path);
@@ -18362,6 +18427,7 @@ var init_database_contracts = __esm(() => {
18362
18427
  init_graphql();
18363
18428
  init_generate();
18364
18429
  init_graphql_options();
18430
+ init_database_contract_manifest();
18365
18431
  });
18366
18432
 
18367
18433
  // src/analyze.ts
@@ -25070,10 +25136,10 @@ import { readFileSync as readFileSync4 } from "node:fs";
25070
25136
  import { resolve as resolve17 } from "node:path";
25071
25137
 
25072
25138
  // src/migration-app-version.ts
25073
- var MIGRATION_APP_VERSION = "0.36.0";
25139
+ var MIGRATION_APP_VERSION = "0.36.1";
25074
25140
 
25075
25141
  // src/migration-elysia-version.ts
25076
- var MIGRATION_ELYSIA_VERSION = "0.38.0";
25142
+ var MIGRATION_ELYSIA_VERSION = "0.38.2";
25077
25143
 
25078
25144
  // src/migration-policy.ts
25079
25145
  var cachedCompilerVersion;
@@ -26869,6 +26935,7 @@ function introspectionQuery() {
26869
26935
 
26870
26936
  // src/index.ts
26871
26937
  init_database_contracts();
26938
+ init_database_contract_manifest();
26872
26939
  export {
26873
26940
  ANGULAR_ENTERPRISE_RULES,
26874
26941
  APPLICATION_DEVELOPMENT_ARCHIVE_MAX_BYTES2 as APPLICATION_DEVELOPMENT_ARCHIVE_MAX_BYTES,
@@ -26955,6 +27022,7 @@ export {
26955
27022
  migrateProject,
26956
27023
  migrateRouteResponse,
26957
27024
  parseApplicationDevelopmentContext,
27025
+ parseDatabaseContractManifest,
26958
27026
  parseDatabaseContractsOptions,
26959
27027
  parseDeliveryBuildManifest,
26960
27028
  parseDeliveryBuildResult,
@@ -1 +1 @@
1
- export declare const MIGRATION_APP_VERSION = "0.36.0";
1
+ export declare const MIGRATION_APP_VERSION = "0.36.1";
@@ -1 +1 @@
1
- export declare const MIGRATION_ELYSIA_VERSION = "0.38.0";
1
+ export declare const MIGRATION_ELYSIA_VERSION = "0.38.2";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/compiler",
3
- "version": "0.54.0",
3
+ "version": "0.55.1",
4
4
  "description": "Static compiler for @supacloud/app metadata: builds the application graph from AST, validates it, and generates reflection-free factory code",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -57,7 +57,7 @@
57
57
  "directory": "packages/compiler"
58
58
  },
59
59
  "dependencies": {
60
- "@supacloud/db": "0.20.4",
60
+ "@supacloud/db": "0.20.5",
61
61
  "@supacloud/delivery": "0.24.0",
62
62
  "typebox": "1.4.3",
63
63
  "@graphql-codegen/core": "^6.2.2",