@supacloud/compiler 0.53.1 → 0.55.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 CHANGED
@@ -3,22 +3,25 @@
3
3
  ## Enforced Database Governance
4
4
 
5
5
  Existing compiler callers retain `off` as a compatibility migration exception.
6
- Newly generated `command` starters set
7
- `databaseGovernance: { mode: "enforce", moduleFile: "db/modules.ts" }`:
8
- compile/check/build run the existing `@supacloud/db` source-boundary and
9
- contract-freshness gate, SQL lint and live Catalog reconciliation, including
6
+ Newly generated `command` starters keep `databaseGovernance: { mode: "check",
7
+ moduleFile: "db/modules.ts" }` for ordinary compile/check/build. `check` runs
8
+ the existing `@supacloud/db` source-boundary and contract-freshness gate plus SQL
9
+ lint, without importing project modules or opening a database connection. The
10
+ generated integration dev profile switches the same configuration to `enforce`
11
+ and supplies a loopback database URL explicitly.
12
+
13
+ `enforce` adds live Catalog reconciliation on top of `check`, including on
10
14
  incremental cache hits. Missing adoption files, stale contracts, SQL findings or
11
15
  Catalog drift block generation even with `writeOnError`; checks never generate
12
- contracts or migrations.
13
- The minimal/http/edge starters remain database-free.
16
+ contracts or migrations. The minimal/http/edge starters remain database-free.
14
17
 
15
18
  For an existing database project, adopt the governed path by declaring the real
16
- database modules and explicitly setting the following in `supacloud.config.ts`:
19
+ database modules and setting the following in `supacloud.config.ts`:
17
20
 
18
21
  ```ts
19
22
  export default {
20
23
  databaseGovernance: {
21
- mode: "enforce",
24
+ mode: "check",
22
25
  projectRoot: ".",
23
26
  moduleFile: "db/modules.ts",
24
27
  databaseUrlEnv: "SUPACLOUD_CHECK_DATABASE_URL",
@@ -27,11 +30,17 @@ export default {
27
30
  };
28
31
  ```
29
32
 
33
+ Keep `check` in committed project configuration so local builds and CI remain
34
+ hermetic. Run `enforce` only in an explicit release/deployment gate where the
35
+ platform injects a read-only PostgreSQL connection, or run the separate
36
+ `supacloud db module_check` command through the same controlled environment.
37
+ Application code and committed configuration must not contain a production
38
+ connection URL.
39
+
30
40
  `projectRoot` is relative to the configuration directory (or `rootDir` when
31
- calling `compileProject` / `checkProject` directly). Use a read-only PostgreSQL
32
- credential in the named environment variable, targeting an explicitly selected
33
- local or test database. The compiler does not load `.env` files for this worker,
34
- generate migrations, repair contracts, or apply SQL. Setting `mode: "off"` is
41
+ calling `compileProject` / `checkProject` directly). The compiler does not load
42
+ `.env` files for this worker, generate migrations, repair contracts, or apply
43
+ SQL. Never use a migration/owner role for this check. Setting `mode: "off"` is
35
44
  reserved for an explicit migration exception and should not be used in a new
36
45
  database starter.
37
46
 
@@ -76,6 +85,11 @@ attempts. The default policy requires `requireRouteEffects`,
76
85
 
77
86
  ## Unified Database Contracts
78
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
+
79
93
  `supacloud-compiler database-contracts database-contracts.json` generates a shared
80
94
  type entry point while preserving PostgREST, Drizzle and GraphQL native types:
81
95
 
package/dist/cli.js CHANGED
@@ -1475,6 +1475,9 @@ var init_validate = __esm(() => {
1475
1475
  "effect-direct-runtime-execution": { code: "SC3045", docsUrl: "https://supacloud.dev/errors/SC3045" },
1476
1476
  "effect-direct-throw": { code: "SC3046", docsUrl: "https://supacloud.dev/errors/SC3046" },
1477
1477
  "effect-promise-return": { code: "SC3047", docsUrl: "https://supacloud.dev/errors/SC3047" },
1478
+ "effect-retry-target-required": { code: "SC3048", docsUrl: "https://supacloud.dev/errors/SC3048" },
1479
+ "effect-retryable-invalid": { code: "SC3049", docsUrl: "https://supacloud.dev/errors/SC3049" },
1480
+ "effect-retry-required": { code: "SC3050", docsUrl: "https://supacloud.dev/errors/SC3050" },
1478
1481
  "invalid-route-response-map": { code: "SC3024", docsUrl: "https://supacloud.dev/errors/SC3024" },
1479
1482
  "invalid-route-response-selector": { code: "SC3025", docsUrl: "https://supacloud.dev/errors/SC3025" },
1480
1483
  "conflicting-route-response-schema": { code: "SC3026", docsUrl: "https://supacloud.dev/errors/SC3026" },
@@ -18184,6 +18187,27 @@ var init_release_evidence_cli = __esm(() => {
18184
18187
  init_release_evidence();
18185
18188
  });
18186
18189
 
18190
+ // src/database-contract-manifest.ts
18191
+ function parseDatabaseContractManifest(value) {
18192
+ if (!Check2(databaseContractManifestSchema, value)) {
18193
+ throw new TypeError("Invalid database contract manifest");
18194
+ }
18195
+ return value;
18196
+ }
18197
+ var databaseContractManifestSchema;
18198
+ var init_database_contract_manifest = __esm(() => {
18199
+ init_build();
18200
+ init_value2();
18201
+ databaseContractManifestSchema = _Object_({
18202
+ version: Literal(1),
18203
+ role: String2({ minLength: 1 }),
18204
+ fingerprint: String2({ pattern: "^[a-f0-9]{64}$" }),
18205
+ inputs: Record(String2({ minLength: 1 }), String2({ pattern: "^[a-f0-9]{64}$" })),
18206
+ migrationOrder: _Array_(String2({ minLength: 1 })),
18207
+ outputs: Record(String2({ minLength: 1 }), String2({ pattern: "^[a-f0-9]{64}$" }))
18208
+ });
18209
+ });
18210
+
18187
18211
  // src/database-contracts.ts
18188
18212
  import { createHash as createHash13 } from "node:crypto";
18189
18213
  import { mkdir as mkdir5, readFile as readFile14 } from "node:fs/promises";
@@ -18208,8 +18232,23 @@ function parseDatabaseContractsOptions(value, directory) {
18208
18232
  throw new TypeError(`Missing database contracts ${name}`);
18209
18233
  return result;
18210
18234
  };
18211
- const graphql = Reflect.get(value, "graphql");
18212
- assertGraphqlOptions(graphql);
18235
+ const optionalPath = (name) => {
18236
+ if (!Object.hasOwn(value, name))
18237
+ return;
18238
+ return field(name);
18239
+ };
18240
+ const postgrestTypes = optionalPath("postgrestTypes");
18241
+ const drizzleSchema = optionalPath("drizzleSchema");
18242
+ const graphqlPresent = Object.hasOwn(value, "graphql");
18243
+ if (!postgrestTypes && !drizzleSchema && !graphqlPresent) {
18244
+ throw new TypeError("Database contracts require postgrestTypes, drizzleSchema, or graphql");
18245
+ }
18246
+ let graphql;
18247
+ if (graphqlPresent) {
18248
+ const candidate = Reflect.get(value, "graphql");
18249
+ assertGraphqlOptions(candidate);
18250
+ graphql = { ...candidate, schema: resolve25(directory, candidate.schema) };
18251
+ }
18213
18252
  const migrations = Reflect.get(value, "migrations");
18214
18253
  if (!Array.isArray(migrations) || !migrations.every((entry) => typeof entry === "string" && entry.endsWith(".sql"))) {
18215
18254
  throw new TypeError("migrations must be an ordered list of SQL files");
@@ -18217,22 +18256,20 @@ function parseDatabaseContractsOptions(value, directory) {
18217
18256
  return {
18218
18257
  rootDir: resolve25(directory, field("rootDir")),
18219
18258
  outDir: resolve25(directory, field("outDir")),
18220
- postgrestTypes: resolve25(directory, field("postgrestTypes")),
18221
- drizzleSchema: resolve25(directory, field("drizzleSchema")),
18259
+ ...postgrestTypes ? { postgrestTypes: resolve25(directory, postgrestTypes) } : {},
18260
+ ...drizzleSchema ? { drizzleSchema: resolve25(directory, drizzleSchema) } : {},
18222
18261
  role: field("role"),
18223
- graphql: { ...graphql, schema: resolve25(directory, graphql.schema) },
18262
+ ...graphql ? { graphql } : {},
18224
18263
  migrations: migrations.map((file) => resolve25(directory, file))
18225
18264
  };
18226
18265
  }
18227
- async function generateDatabaseContracts(options, check = false) {
18228
- const rootDir = resolve25(options.rootDir), outDir = resolve25(options.outDir);
18229
- const postgrestTypes = resolve25(rootDir, options.postgrestTypes), drizzleSchema = resolve25(rootDir, options.drizzleSchema);
18230
- const snapshot = await readFile14(postgrestTypes, "utf8");
18231
- const syntax = ts18.createSourceFile(postgrestTypes, snapshot, ts18.ScriptTarget.Latest, true);
18266
+ async function assertPostgrestSnapshot(path) {
18267
+ const snapshot = await readFile14(path, "utf8");
18268
+ const syntax = ts18.createSourceFile(path, snapshot, ts18.ScriptTarget.Latest, true);
18232
18269
  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));
18233
18270
  if (!database)
18234
18271
  throw new Error("PostgREST snapshot must export Database from the official type generator");
18235
- const program = ts18.createProgram([postgrestTypes], {
18272
+ const program = ts18.createProgram([path], {
18236
18273
  strict: true,
18237
18274
  noEmit: true,
18238
18275
  skipLibCheck: true,
@@ -18243,11 +18280,17 @@ async function generateDatabaseContracts(options, check = false) {
18243
18280
  });
18244
18281
  if (ts18.getPreEmitDiagnostics(program).length)
18245
18282
  throw new Error("PostgREST snapshot has TypeScript errors");
18246
- const artifacts = await renderGraphql({
18247
- rootDir,
18248
- outDir,
18249
- graphql: options.graphql
18250
- });
18283
+ }
18284
+ async function generateDatabaseContracts(options, check = false) {
18285
+ const rootDir = resolve25(options.rootDir), outDir = resolve25(options.outDir);
18286
+ const postgrestTypes = options.postgrestTypes ? resolve25(rootDir, options.postgrestTypes) : undefined;
18287
+ const drizzleSchema = options.drizzleSchema ? resolve25(rootDir, options.drizzleSchema) : undefined;
18288
+ if (!postgrestTypes && !drizzleSchema && !options.graphql) {
18289
+ throw new TypeError("Database contracts require postgrestTypes, drizzleSchema, or graphql");
18290
+ }
18291
+ if (postgrestTypes)
18292
+ await assertPostgrestSnapshot(postgrestTypes);
18293
+ const artifacts = options.graphql ? await renderGraphql({ rootDir, outDir, graphql: options.graphql }) : { diagnostics: [], files: {} };
18251
18294
  if (artifacts.diagnostics.some((item) => item.severity === "error")) {
18252
18295
  throw new Error(artifacts.diagnostics.map((item) => item.message).join(`
18253
18296
  `));
@@ -18257,38 +18300,42 @@ async function generateDatabaseContracts(options, check = false) {
18257
18300
  const absolute = resolve25(rootDir, path);
18258
18301
  inputs[relative13(rootDir, absolute).replaceAll("\\", "/")] = hash4(await readFile14(absolute, "utf8"));
18259
18302
  };
18260
- await addInput(postgrestTypes);
18261
- await addInput(drizzleSchema);
18262
- const drizzleProgram = ts18.createProgram([drizzleSchema], {
18263
- noEmit: true,
18264
- moduleResolution: ts18.ModuleResolutionKind.Bundler,
18265
- module: ts18.ModuleKind.ESNext,
18266
- target: ts18.ScriptTarget.ES2022,
18267
- types: [],
18268
- skipLibCheck: true
18269
- });
18270
- for (const source of drizzleProgram.getSourceFiles()) {
18271
- if (!source.isDeclarationFile && !source.fileName.includes("/node_modules/"))
18272
- await addInput(source.fileName);
18303
+ if (postgrestTypes)
18304
+ await addInput(postgrestTypes);
18305
+ if (drizzleSchema) {
18306
+ await addInput(drizzleSchema);
18307
+ const drizzleProgram = ts18.createProgram([drizzleSchema], {
18308
+ noEmit: true,
18309
+ moduleResolution: ts18.ModuleResolutionKind.Bundler,
18310
+ module: ts18.ModuleKind.ESNext,
18311
+ target: ts18.ScriptTarget.ES2022,
18312
+ types: [],
18313
+ skipLibCheck: true
18314
+ });
18315
+ for (const source of drizzleProgram.getSourceFiles()) {
18316
+ if (!source.isDeclarationFile && !source.fileName.includes("/node_modules/"))
18317
+ await addInput(source.fileName);
18318
+ }
18273
18319
  }
18274
- await addInput(resolve25(rootDir, options.graphql.schema));
18320
+ if (options.graphql)
18321
+ await addInput(resolve25(rootDir, options.graphql.schema));
18275
18322
  if (new Set(options.migrations.map((path) => resolve25(rootDir, path))).size !== options.migrations.length) {
18276
18323
  throw new Error("Duplicate migration in database contracts configuration");
18277
18324
  }
18278
18325
  for (const path of options.migrations)
18279
18326
  await addInput(path);
18280
- const files = {
18281
- ...artifacts.files,
18282
- "database.ts": [
18283
- "// GENERATED BY @supacloud/compiler database-contracts. Do not edit.",
18284
- `export type { Database } from ${JSON.stringify(importPath(outDir, postgrestTypes))};`,
18285
- 'export type { QueryData, QueryResult, QueryError } from "@supabase/supabase-js";',
18286
- `export type DrizzleSchema = typeof import(${JSON.stringify(importPath(outDir, drizzleSchema))});`,
18287
- 'export * from "./graphql";',
18288
- ""
18289
- ].join(`
18290
- `)
18291
- };
18327
+ const barrel = ["// GENERATED BY @supacloud/compiler database-contracts. Do not edit."];
18328
+ if (postgrestTypes) {
18329
+ barrel.push(`export type { Database } from ${JSON.stringify(importPath(outDir, postgrestTypes))};`);
18330
+ barrel.push('export type { QueryData, QueryResult, QueryError } from "@supabase/supabase-js";');
18331
+ }
18332
+ if (drizzleSchema)
18333
+ barrel.push(`export type DrizzleSchema = typeof import(${JSON.stringify(importPath(outDir, drizzleSchema))});`);
18334
+ if (options.graphql)
18335
+ barrel.push('export * from "./graphql";');
18336
+ barrel.push("");
18337
+ const files = { ...artifacts.files, "database.ts": barrel.join(`
18338
+ `) };
18292
18339
  const manifest = {
18293
18340
  version: 1,
18294
18341
  role: options.role,
@@ -18296,7 +18343,12 @@ async function generateDatabaseContracts(options, check = false) {
18296
18343
  migrationOrder: options.migrations.map((path) => relative13(rootDir, resolve25(rootDir, path)).replaceAll("\\", "/")),
18297
18344
  outputs: Object.fromEntries(Object.entries(files).sort(([a], [b]) => a.localeCompare(b)).map(([file, text]) => [file, hash4(text)]))
18298
18345
  };
18299
- files["database.manifest.json"] = JSON.stringify(manifest, null, 2) + `
18346
+ const fingerprint = hash4(JSON.stringify({
18347
+ inputs: manifest.inputs,
18348
+ migrationOrder: manifest.migrationOrder
18349
+ }));
18350
+ const typedManifest = parseDatabaseContractManifest({ ...manifest, fingerprint });
18351
+ files["database.manifest.json"] = JSON.stringify(typedManifest, null, 2) + `
18300
18352
  `;
18301
18353
  const mismatches = [];
18302
18354
  for (const [file, content] of Object.entries(files)) {
@@ -18316,7 +18368,7 @@ async function generateDatabaseContracts(options, check = false) {
18316
18368
  for (const [file, content] of Object.entries(files))
18317
18369
  await writeFileIfChanged(resolve25(outDir, file), content);
18318
18370
  }
18319
- return { upToDate: mismatches.length === 0, mismatches, written: check ? [] : mismatches, manifest };
18371
+ return { upToDate: mismatches.length === 0, mismatches, written: check ? [] : mismatches, manifest: typedManifest };
18320
18372
  }
18321
18373
  async function runDatabaseContractsFile(path, check = false) {
18322
18374
  const absolute = resolve25(path);
@@ -18327,6 +18379,7 @@ var init_database_contracts = __esm(() => {
18327
18379
  init_graphql();
18328
18380
  init_generate();
18329
18381
  init_graphql_options();
18382
+ init_database_contract_manifest();
18330
18383
  });
18331
18384
 
18332
18385
  // src/graphql-schema.ts
@@ -19105,7 +19158,8 @@ var DEFAULT_EFFECT_ANALYSIS_OPTIONS = {
19105
19158
  requireExactDependencyTypes: true,
19106
19159
  requireTimeoutForDependencies: true,
19107
19160
  forbidDirectRuntimeExecution: true,
19108
- forbidDirectThrows: true
19161
+ forbidDirectThrows: true,
19162
+ requireRetryPolicy: true
19109
19163
  };
19110
19164
  function nodeText(node) {
19111
19165
  return node.getText(node.getSourceFile());
@@ -19178,7 +19232,8 @@ async function analyzeProject(rootDir, include, cache, changedPaths, effectOptio
19178
19232
  requireExactDependencyTypes: effectOptions.requireExactDependencyTypes ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.requireExactDependencyTypes,
19179
19233
  requireTimeoutForDependencies: effectOptions.requireTimeoutForDependencies ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.requireTimeoutForDependencies,
19180
19234
  forbidDirectRuntimeExecution: effectOptions.forbidDirectRuntimeExecution ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.forbidDirectRuntimeExecution,
19181
- forbidDirectThrows: effectOptions.forbidDirectThrows ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.forbidDirectThrows
19235
+ forbidDirectThrows: effectOptions.forbidDirectThrows ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.forbidDirectThrows,
19236
+ requireRetryPolicy: effectOptions.requireRetryPolicy ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.requireRetryPolicy
19182
19237
  }
19183
19238
  };
19184
19239
  if (ctx.effectOptions.forbidDirectRuntimeExecution) {
@@ -20001,6 +20056,21 @@ function resolveStaticObjectLiteral(input, ctx, seen = new Set) {
20001
20056
  }
20002
20057
  return ts7.isObjectLiteralExpression(input) ? input : undefined;
20003
20058
  }
20059
+ function resolveStaticArrayLiteral(input, ctx, seen = new Set) {
20060
+ if (!input || seen.has(input))
20061
+ return;
20062
+ seen.add(input);
20063
+ if (ts7.isAsExpression(input) || ts7.isSatisfiesExpression(input) || ts7.isParenthesizedExpression(input)) {
20064
+ return resolveStaticArrayLiteral(input.expression, ctx, seen);
20065
+ }
20066
+ if (ts7.isIdentifier(input)) {
20067
+ const declaration = resolveDeclaration(input, ctx).find(ts7.isVariableDeclaration);
20068
+ const list = declaration?.parent;
20069
+ const isConst = Boolean(list && ts7.isVariableDeclarationList(list) && (list.flags & ts7.NodeFlags.Const) !== 0);
20070
+ return isConst && declaration?.initializer ? resolveStaticArrayLiteral(declaration.initializer, ctx, seen) : undefined;
20071
+ }
20072
+ return ts7.isArrayLiteralExpression(input) ? input : undefined;
20073
+ }
20004
20074
  function routePolicyProperty(object, name, ctx, seen = new Set) {
20005
20075
  if (seen.has(object))
20006
20076
  return { unresolved: true };
@@ -20961,7 +21031,7 @@ function parseController(input, ctx) {
20961
21031
  file,
20962
21032
  line: lineOf(effectExpr),
20963
21033
  message: `Route ${route.handler} effect must be a statically resolvable object literal.`,
20964
- suggestion: "Use effect: { required: true, errors: [...], dependencies: [...] }."
21034
+ suggestion: 'Use effect: { required: true, errors: [...], dependencies: [...], retry: "none" }.'
20965
21035
  });
20966
21036
  } else {
20967
21037
  const effect = { required: true };
@@ -20992,6 +21062,8 @@ function parseController(input, ctx) {
20992
21062
  } else {
20993
21063
  effect.retry = retry;
20994
21064
  }
21065
+ } else if (ctx.effectOptions.requireRetryPolicy === false) {
21066
+ effect.retry = "none";
20995
21067
  }
20996
21068
  const maxAttemptsExpr = getProp(effectObject, "maxAttempts");
20997
21069
  if (maxAttemptsExpr !== undefined) {
@@ -21040,7 +21112,8 @@ function parseController(input, ctx) {
21040
21112
  }
21041
21113
  const dependenciesExpr = getProp(effectObject, "dependencies");
21042
21114
  if (dependenciesExpr !== undefined) {
21043
- if (!ts7.isArrayLiteralExpression(dependenciesExpr) || dependenciesExpr.elements.some((element) => !ts7.isStringLiteral(element) || element.text.trim().length === 0)) {
21115
+ const dependenciesArray = resolveStaticArrayLiteral(dependenciesExpr, ctx);
21116
+ if (!dependenciesArray || dependenciesArray.elements.some((element) => !ts7.isStringLiteral(element) || element.text.trim().length === 0)) {
21044
21117
  ctx.diagnostics.push({
21045
21118
  severity: "error",
21046
21119
  code: "invalid-effect-dependencies",
@@ -21048,10 +21121,10 @@ function parseController(input, ctx) {
21048
21121
  docsUrl: "https://supacloud.dev/errors/SC3033",
21049
21122
  file,
21050
21123
  line: lineOf(dependenciesExpr),
21051
- message: `Route ${route.handler} effect.dependencies must be an explicit string array.`
21124
+ message: `Route ${route.handler} effect.dependencies must be a string array or a const initialized with one.`
21052
21125
  });
21053
21126
  } else {
21054
- effect.dependencies = dependenciesExpr.elements.map((element) => element.text);
21127
+ effect.dependencies = dependenciesArray.elements.map((element) => element.text);
21055
21128
  if (new Set(effect.dependencies).size !== effect.dependencies.length) {
21056
21129
  ctx.diagnostics.push({
21057
21130
  severity: "error",
@@ -21068,7 +21141,8 @@ function parseController(input, ctx) {
21068
21141
  }
21069
21142
  const errorsExpr = getProp(effectObject, "errors");
21070
21143
  if (errorsExpr !== undefined) {
21071
- if (!ts7.isArrayLiteralExpression(errorsExpr)) {
21144
+ const errorsArray = resolveStaticArrayLiteral(errorsExpr, ctx);
21145
+ if (!errorsArray) {
21072
21146
  ctx.diagnostics.push({
21073
21147
  severity: "error",
21074
21148
  code: "invalid-effect-errors",
@@ -21076,11 +21150,11 @@ function parseController(input, ctx) {
21076
21150
  docsUrl: "https://supacloud.dev/errors/SC3034",
21077
21151
  file,
21078
21152
  line: lineOf(errorsExpr),
21079
- message: `Route ${route.handler} effect.errors must be an explicit object array.`
21153
+ message: `Route ${route.handler} effect.errors must be an object array or a const initialized with one.`
21080
21154
  });
21081
21155
  } else {
21082
21156
  const errors = [];
21083
- for (const errorExpr of errorsExpr.elements) {
21157
+ for (const errorExpr of errorsArray.elements) {
21084
21158
  if (!ts7.isObjectLiteralExpression(errorExpr)) {
21085
21159
  ctx.diagnostics.push({
21086
21160
  severity: "error",
@@ -21097,6 +21171,23 @@ function parseController(input, ctx) {
21097
21171
  const code = stringLiteralProp(errorExpr, "code");
21098
21172
  const status = numberLiteralProp(errorExpr, "status");
21099
21173
  const message = stringLiteralProp(errorExpr, "message");
21174
+ const codeFromError = booleanLiteralProp(errorExpr, "codeFromError");
21175
+ const messageFromError = booleanLiteralProp(errorExpr, "messageFromError");
21176
+ const statusFromError = booleanLiteralProp(errorExpr, "statusFromError");
21177
+ const retryable = booleanLiteralProp(errorExpr, "retryable");
21178
+ const outcomeUnknown = booleanLiteralProp(errorExpr, "outcomeUnknown");
21179
+ if (invalidBooleanProp(errorExpr, "codeFromError") || invalidBooleanProp(errorExpr, "messageFromError") || invalidBooleanProp(errorExpr, "statusFromError") || invalidBooleanProp(errorExpr, "retryable") || invalidBooleanProp(errorExpr, "outcomeUnknown")) {
21180
+ ctx.diagnostics.push({
21181
+ severity: "error",
21182
+ code: "invalid-effect-errors",
21183
+ errorCode: "SC3034",
21184
+ docsUrl: "https://supacloud.dev/errors/SC3034",
21185
+ file,
21186
+ line: lineOf(errorExpr),
21187
+ message: `Route ${route.handler} effect.errors instance flags must be boolean literals.`
21188
+ });
21189
+ continue;
21190
+ }
21100
21191
  if (!tag || !code || status === undefined || !Number.isInteger(status) || status < 400 || status > 599) {
21101
21192
  ctx.diagnostics.push({
21102
21193
  severity: "error",
@@ -21109,7 +21200,17 @@ function parseController(input, ctx) {
21109
21200
  });
21110
21201
  continue;
21111
21202
  }
21112
- errors.push({ tag, code, status, ...message === undefined ? {} : { message } });
21203
+ errors.push({
21204
+ tag,
21205
+ code,
21206
+ status,
21207
+ ...message === undefined ? {} : { message },
21208
+ ...codeFromError === undefined ? {} : { codeFromError },
21209
+ ...messageFromError === undefined ? {} : { messageFromError },
21210
+ ...statusFromError === undefined ? {} : { statusFromError },
21211
+ ...retryable === undefined ? {} : { retryable },
21212
+ ...outcomeUnknown === undefined ? {} : { outcomeUnknown }
21213
+ });
21113
21214
  }
21114
21215
  effect.errors = errors;
21115
21216
  if (new Set(errors.map((entry) => entry.tag)).size !== errors.length) {
@@ -21837,6 +21938,19 @@ function numberLiteralProp(obj, name) {
21837
21938
  const value = Number(expr.text);
21838
21939
  return Number.isFinite(value) ? value : undefined;
21839
21940
  }
21941
+ function booleanLiteralProp(obj, name) {
21942
+ const expr = getProp(obj, name);
21943
+ if (!expr || !isBooleanLiteral(expr))
21944
+ return;
21945
+ return expr.kind === ts7.SyntaxKind.TrueKeyword;
21946
+ }
21947
+ function isBooleanLiteral(expr) {
21948
+ return expr.kind === ts7.SyntaxKind.TrueKeyword || expr.kind === ts7.SyntaxKind.FalseKeyword;
21949
+ }
21950
+ function invalidBooleanProp(obj, name) {
21951
+ const expr = getProp(obj, name);
21952
+ return expr !== undefined && !isBooleanLiteral(expr);
21953
+ }
21840
21954
  function arrayProp(obj, name) {
21841
21955
  const expr = getProp(obj, name);
21842
21956
  return expr && ts7.isArrayLiteralExpression(expr) ? [...expr.elements] : [];
@@ -22777,6 +22891,9 @@ function validateEffectPolicies(graph, options = {}) {
22777
22891
  if (options.requireDependencies && effect.dependencies === undefined) {
22778
22892
  diagnostics.push(diagnostic("effect-dependencies-required", `Route ${label} declares an Effect contract without dependencies.`, controller.file, "Declare the logical services required by the Effect environment."));
22779
22893
  }
22894
+ if (effect.retry === undefined && options.requireRetryPolicy !== false) {
22895
+ diagnostics.push(diagnostic("effect-retry-required", `Route ${label} declares an Effect contract but does not declare retry.`, controller.file, 'Set effect.retry to "none". That is the required default; use "explicit" only with maxAttempts and a retryable 5xx allow-list.'));
22896
+ }
22780
22897
  if (effect.retry === "explicit" && effect.maxAttempts === undefined) {
22781
22898
  diagnostics.push(diagnostic("invalid-effect-retry", `Route ${label} enables explicit Effect retry without maxAttempts.`, controller.file, "Declare a positive maxAttempts value including the initial attempt."));
22782
22899
  }
@@ -22787,6 +22904,18 @@ function validateEffectPolicies(graph, options = {}) {
22787
22904
  if (command && command.idempotency !== "required") {
22788
22905
  diagnostics.push(diagnostic("effect-command-retry-forbidden", `Route ${label} enables explicit Effect retry for non-idempotent command "${command.name}".`, controller.file, 'Set command idempotency to "required" or use effect.retry: "none".'));
22789
22906
  }
22907
+ const retryable = (effect.errors ?? []).filter((entry) => entry.retryable === true);
22908
+ if (retryable.length === 0) {
22909
+ diagnostics.push(diagnostic("effect-retry-target-required", `Route ${label} enables explicit Effect retry without a transient failure allow-list.`, controller.file, 'Mark only 5xx failures that did not commit with retryable: true, or use effect.retry: "none".'));
22910
+ }
22911
+ }
22912
+ for (const entry of effect.errors ?? []) {
22913
+ if (entry.retryable !== true)
22914
+ continue;
22915
+ const transient = effect.retry === "explicit" && entry.outcomeUnknown !== true && entry.statusFromError !== true && entry.status >= 500 && entry.status <= 599;
22916
+ if (!transient) {
22917
+ diagnostics.push(diagnostic("effect-retryable-invalid", `Route ${label} marks "${entry.tag}" retryable, but in-process retry is only for declared 5xx failures that are not outcome-unknown.`, controller.file, 'Remove retryable, or keep it on a 5xx mapping with retry: "explicit" and without outcomeUnknown or statusFromError.'));
22918
+ }
22790
22919
  }
22791
22920
  }
22792
22921
  }
@@ -23787,7 +23916,9 @@ var DEFAULT_SUPACLOUD_CONFIG = {
23787
23916
  requireExactDependencyTypes: true,
23788
23917
  requireTimeoutForDependencies: true,
23789
23918
  forbidDirectRuntimeExecution: true,
23790
- forbidDirectThrows: true
23919
+ forbidDirectThrows: true,
23920
+ requireRetryPolicy: true,
23921
+ defaultRetry: "none"
23791
23922
  }
23792
23923
  };
23793
23924
  function defineSupacloudConfig(config = {}) {
@@ -23869,7 +24000,9 @@ function validateGovernanceConfig(config) {
23869
24000
  "requireExactDependencyTypes",
23870
24001
  "requireTimeoutForDependencies",
23871
24002
  "forbidDirectRuntimeExecution",
23872
- "forbidDirectThrows"
24003
+ "forbidDirectThrows",
24004
+ "requireRetryPolicy",
24005
+ "defaultRetry"
23873
24006
  ].includes(key))) {
23874
24007
  throw new Error("effect contains an unsupported governance option.");
23875
24008
  }
@@ -23881,12 +24014,16 @@ function validateGovernanceConfig(config) {
23881
24014
  "requireExactDependencyTypes",
23882
24015
  "requireTimeoutForDependencies",
23883
24016
  "forbidDirectRuntimeExecution",
23884
- "forbidDirectThrows"
24017
+ "forbidDirectThrows",
24018
+ "requireRetryPolicy"
23885
24019
  ]) {
23886
24020
  if (config.effect[key] !== undefined && typeof config.effect[key] !== "boolean") {
23887
24021
  throw new Error(`effect.${key} must be boolean.`);
23888
24022
  }
23889
24023
  }
24024
+ if (config.effect["defaultRetry"] !== undefined && config.effect["defaultRetry"] !== "none") {
24025
+ throw new Error('effect.defaultRetry must be "none". explicit can only be written on a route, with maxAttempts and a retryable 5xx allow-list.');
24026
+ }
23890
24027
  }
23891
24028
  }
23892
24029
  function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
@@ -25731,10 +25868,10 @@ import { readFileSync as readFileSync4 } from "node:fs";
25731
25868
  import { resolve as resolve19 } from "node:path";
25732
25869
 
25733
25870
  // src/migration-app-version.ts
25734
- var MIGRATION_APP_VERSION = "0.35.0";
25871
+ var MIGRATION_APP_VERSION = "0.36.0";
25735
25872
 
25736
25873
  // src/migration-elysia-version.ts
25737
- var MIGRATION_ELYSIA_VERSION = "0.37.0";
25874
+ var MIGRATION_ELYSIA_VERSION = "0.38.1";
25738
25875
 
25739
25876
  // src/migration-policy.ts
25740
25877
  var cachedCompilerVersion;
@@ -25753,7 +25890,7 @@ function migrationDependencies() {
25753
25890
  "@supacloud/app": MIGRATION_APP_VERSION,
25754
25891
  "@supacloud/compiler": compilerVersion(),
25755
25892
  "@supacloud/elysia": MIGRATION_ELYSIA_VERSION,
25756
- elysia: "2.0.0-rc.0",
25893
+ elysia: "2.0.0-rc.1",
25757
25894
  typescript: "7.0.2"
25758
25895
  };
25759
25896
  }
@@ -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
@@ -1474,6 +1474,9 @@ var init_validate = __esm(() => {
1474
1474
  "effect-direct-runtime-execution": { code: "SC3045", docsUrl: "https://supacloud.dev/errors/SC3045" },
1475
1475
  "effect-direct-throw": { code: "SC3046", docsUrl: "https://supacloud.dev/errors/SC3046" },
1476
1476
  "effect-promise-return": { code: "SC3047", docsUrl: "https://supacloud.dev/errors/SC3047" },
1477
+ "effect-retry-target-required": { code: "SC3048", docsUrl: "https://supacloud.dev/errors/SC3048" },
1478
+ "effect-retryable-invalid": { code: "SC3049", docsUrl: "https://supacloud.dev/errors/SC3049" },
1479
+ "effect-retry-required": { code: "SC3050", docsUrl: "https://supacloud.dev/errors/SC3050" },
1477
1480
  "invalid-route-response-map": { code: "SC3024", docsUrl: "https://supacloud.dev/errors/SC3024" },
1478
1481
  "invalid-route-response-selector": { code: "SC3025", docsUrl: "https://supacloud.dev/errors/SC3025" },
1479
1482
  "conflicting-route-response-schema": { code: "SC3026", docsUrl: "https://supacloud.dev/errors/SC3026" },
@@ -18216,6 +18219,27 @@ var init_graphql_schema = __esm(() => {
18216
18219
  init_graphql_schema_hashes();
18217
18220
  });
18218
18221
 
18222
+ // src/database-contract-manifest.ts
18223
+ function parseDatabaseContractManifest(value) {
18224
+ if (!Check2(databaseContractManifestSchema, value)) {
18225
+ throw new TypeError("Invalid database contract manifest");
18226
+ }
18227
+ return value;
18228
+ }
18229
+ var databaseContractManifestSchema;
18230
+ var init_database_contract_manifest = __esm(() => {
18231
+ init_build();
18232
+ init_value2();
18233
+ databaseContractManifestSchema = _Object_({
18234
+ version: Literal(1),
18235
+ role: String2({ minLength: 1 }),
18236
+ fingerprint: String2({ pattern: "^[a-f0-9]{64}$" }),
18237
+ inputs: Record(String2({ minLength: 1 }), String2({ pattern: "^[a-f0-9]{64}$" })),
18238
+ migrationOrder: _Array_(String2({ minLength: 1 })),
18239
+ outputs: Record(String2({ minLength: 1 }), String2({ pattern: "^[a-f0-9]{64}$" }))
18240
+ });
18241
+ });
18242
+
18219
18243
  // src/database-contracts.ts
18220
18244
  import { createHash as createHash12 } from "node:crypto";
18221
18245
  import { mkdir as mkdir6, readFile as readFile14 } from "node:fs/promises";
@@ -18240,8 +18264,23 @@ function parseDatabaseContractsOptions(value, directory) {
18240
18264
  throw new TypeError(`Missing database contracts ${name}`);
18241
18265
  return result;
18242
18266
  };
18243
- const graphql = Reflect.get(value, "graphql");
18244
- assertGraphqlOptions(graphql);
18267
+ const optionalPath = (name) => {
18268
+ if (!Object.hasOwn(value, name))
18269
+ return;
18270
+ return field(name);
18271
+ };
18272
+ const postgrestTypes = optionalPath("postgrestTypes");
18273
+ const drizzleSchema = optionalPath("drizzleSchema");
18274
+ const graphqlPresent = Object.hasOwn(value, "graphql");
18275
+ if (!postgrestTypes && !drizzleSchema && !graphqlPresent) {
18276
+ throw new TypeError("Database contracts require postgrestTypes, drizzleSchema, or graphql");
18277
+ }
18278
+ let graphql;
18279
+ if (graphqlPresent) {
18280
+ const candidate = Reflect.get(value, "graphql");
18281
+ assertGraphqlOptions(candidate);
18282
+ graphql = { ...candidate, schema: resolve24(directory, candidate.schema) };
18283
+ }
18245
18284
  const migrations = Reflect.get(value, "migrations");
18246
18285
  if (!Array.isArray(migrations) || !migrations.every((entry) => typeof entry === "string" && entry.endsWith(".sql"))) {
18247
18286
  throw new TypeError("migrations must be an ordered list of SQL files");
@@ -18249,22 +18288,20 @@ function parseDatabaseContractsOptions(value, directory) {
18249
18288
  return {
18250
18289
  rootDir: resolve24(directory, field("rootDir")),
18251
18290
  outDir: resolve24(directory, field("outDir")),
18252
- postgrestTypes: resolve24(directory, field("postgrestTypes")),
18253
- drizzleSchema: resolve24(directory, field("drizzleSchema")),
18291
+ ...postgrestTypes ? { postgrestTypes: resolve24(directory, postgrestTypes) } : {},
18292
+ ...drizzleSchema ? { drizzleSchema: resolve24(directory, drizzleSchema) } : {},
18254
18293
  role: field("role"),
18255
- graphql: { ...graphql, schema: resolve24(directory, graphql.schema) },
18294
+ ...graphql ? { graphql } : {},
18256
18295
  migrations: migrations.map((file) => resolve24(directory, file))
18257
18296
  };
18258
18297
  }
18259
- async function generateDatabaseContracts(options, check = false) {
18260
- const rootDir = resolve24(options.rootDir), outDir = resolve24(options.outDir);
18261
- const postgrestTypes = resolve24(rootDir, options.postgrestTypes), drizzleSchema = resolve24(rootDir, options.drizzleSchema);
18262
- const snapshot = await readFile14(postgrestTypes, "utf8");
18263
- const syntax = ts18.createSourceFile(postgrestTypes, snapshot, ts18.ScriptTarget.Latest, true);
18298
+ async function assertPostgrestSnapshot(path) {
18299
+ const snapshot = await readFile14(path, "utf8");
18300
+ const syntax = ts18.createSourceFile(path, snapshot, ts18.ScriptTarget.Latest, true);
18264
18301
  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));
18265
18302
  if (!database)
18266
18303
  throw new Error("PostgREST snapshot must export Database from the official type generator");
18267
- const program = ts18.createProgram([postgrestTypes], {
18304
+ const program = ts18.createProgram([path], {
18268
18305
  strict: true,
18269
18306
  noEmit: true,
18270
18307
  skipLibCheck: true,
@@ -18275,11 +18312,17 @@ async function generateDatabaseContracts(options, check = false) {
18275
18312
  });
18276
18313
  if (ts18.getPreEmitDiagnostics(program).length)
18277
18314
  throw new Error("PostgREST snapshot has TypeScript errors");
18278
- const artifacts = await renderGraphql({
18279
- rootDir,
18280
- outDir,
18281
- graphql: options.graphql
18282
- });
18315
+ }
18316
+ async function generateDatabaseContracts(options, check = false) {
18317
+ const rootDir = resolve24(options.rootDir), outDir = resolve24(options.outDir);
18318
+ const postgrestTypes = options.postgrestTypes ? resolve24(rootDir, options.postgrestTypes) : undefined;
18319
+ const drizzleSchema = options.drizzleSchema ? resolve24(rootDir, options.drizzleSchema) : undefined;
18320
+ if (!postgrestTypes && !drizzleSchema && !options.graphql) {
18321
+ throw new TypeError("Database contracts require postgrestTypes, drizzleSchema, or graphql");
18322
+ }
18323
+ if (postgrestTypes)
18324
+ await assertPostgrestSnapshot(postgrestTypes);
18325
+ const artifacts = options.graphql ? await renderGraphql({ rootDir, outDir, graphql: options.graphql }) : { diagnostics: [], files: {} };
18283
18326
  if (artifacts.diagnostics.some((item) => item.severity === "error")) {
18284
18327
  throw new Error(artifacts.diagnostics.map((item) => item.message).join(`
18285
18328
  `));
@@ -18289,38 +18332,42 @@ async function generateDatabaseContracts(options, check = false) {
18289
18332
  const absolute = resolve24(rootDir, path);
18290
18333
  inputs[relative12(rootDir, absolute).replaceAll("\\", "/")] = hash4(await readFile14(absolute, "utf8"));
18291
18334
  };
18292
- await addInput(postgrestTypes);
18293
- await addInput(drizzleSchema);
18294
- const drizzleProgram = ts18.createProgram([drizzleSchema], {
18295
- noEmit: true,
18296
- moduleResolution: ts18.ModuleResolutionKind.Bundler,
18297
- module: ts18.ModuleKind.ESNext,
18298
- target: ts18.ScriptTarget.ES2022,
18299
- types: [],
18300
- skipLibCheck: true
18301
- });
18302
- for (const source of drizzleProgram.getSourceFiles()) {
18303
- if (!source.isDeclarationFile && !source.fileName.includes("/node_modules/"))
18304
- await addInput(source.fileName);
18335
+ if (postgrestTypes)
18336
+ await addInput(postgrestTypes);
18337
+ if (drizzleSchema) {
18338
+ await addInput(drizzleSchema);
18339
+ const drizzleProgram = ts18.createProgram([drizzleSchema], {
18340
+ noEmit: true,
18341
+ moduleResolution: ts18.ModuleResolutionKind.Bundler,
18342
+ module: ts18.ModuleKind.ESNext,
18343
+ target: ts18.ScriptTarget.ES2022,
18344
+ types: [],
18345
+ skipLibCheck: true
18346
+ });
18347
+ for (const source of drizzleProgram.getSourceFiles()) {
18348
+ if (!source.isDeclarationFile && !source.fileName.includes("/node_modules/"))
18349
+ await addInput(source.fileName);
18350
+ }
18305
18351
  }
18306
- await addInput(resolve24(rootDir, options.graphql.schema));
18352
+ if (options.graphql)
18353
+ await addInput(resolve24(rootDir, options.graphql.schema));
18307
18354
  if (new Set(options.migrations.map((path) => resolve24(rootDir, path))).size !== options.migrations.length) {
18308
18355
  throw new Error("Duplicate migration in database contracts configuration");
18309
18356
  }
18310
18357
  for (const path of options.migrations)
18311
18358
  await addInput(path);
18312
- const files = {
18313
- ...artifacts.files,
18314
- "database.ts": [
18315
- "// GENERATED BY @supacloud/compiler database-contracts. Do not edit.",
18316
- `export type { Database } from ${JSON.stringify(importPath(outDir, postgrestTypes))};`,
18317
- 'export type { QueryData, QueryResult, QueryError } from "@supabase/supabase-js";',
18318
- `export type DrizzleSchema = typeof import(${JSON.stringify(importPath(outDir, drizzleSchema))});`,
18319
- 'export * from "./graphql";',
18320
- ""
18321
- ].join(`
18322
- `)
18323
- };
18359
+ const barrel = ["// GENERATED BY @supacloud/compiler database-contracts. Do not edit."];
18360
+ if (postgrestTypes) {
18361
+ barrel.push(`export type { Database } from ${JSON.stringify(importPath(outDir, postgrestTypes))};`);
18362
+ barrel.push('export type { QueryData, QueryResult, QueryError } from "@supabase/supabase-js";');
18363
+ }
18364
+ if (drizzleSchema)
18365
+ barrel.push(`export type DrizzleSchema = typeof import(${JSON.stringify(importPath(outDir, drizzleSchema))});`);
18366
+ if (options.graphql)
18367
+ barrel.push('export * from "./graphql";');
18368
+ barrel.push("");
18369
+ const files = { ...artifacts.files, "database.ts": barrel.join(`
18370
+ `) };
18324
18371
  const manifest = {
18325
18372
  version: 1,
18326
18373
  role: options.role,
@@ -18328,7 +18375,12 @@ async function generateDatabaseContracts(options, check = false) {
18328
18375
  migrationOrder: options.migrations.map((path) => relative12(rootDir, resolve24(rootDir, path)).replaceAll("\\", "/")),
18329
18376
  outputs: Object.fromEntries(Object.entries(files).sort(([a], [b]) => a.localeCompare(b)).map(([file, text]) => [file, hash4(text)]))
18330
18377
  };
18331
- files["database.manifest.json"] = JSON.stringify(manifest, null, 2) + `
18378
+ const fingerprint = hash4(JSON.stringify({
18379
+ inputs: manifest.inputs,
18380
+ migrationOrder: manifest.migrationOrder
18381
+ }));
18382
+ const typedManifest = parseDatabaseContractManifest({ ...manifest, fingerprint });
18383
+ files["database.manifest.json"] = JSON.stringify(typedManifest, null, 2) + `
18332
18384
  `;
18333
18385
  const mismatches = [];
18334
18386
  for (const [file, content] of Object.entries(files)) {
@@ -18348,7 +18400,7 @@ async function generateDatabaseContracts(options, check = false) {
18348
18400
  for (const [file, content] of Object.entries(files))
18349
18401
  await writeFileIfChanged(resolve24(outDir, file), content);
18350
18402
  }
18351
- return { upToDate: mismatches.length === 0, mismatches, written: check ? [] : mismatches, manifest };
18403
+ return { upToDate: mismatches.length === 0, mismatches, written: check ? [] : mismatches, manifest: typedManifest };
18352
18404
  }
18353
18405
  async function runDatabaseContractsFile(path, check = false) {
18354
18406
  const absolute = resolve24(path);
@@ -18359,6 +18411,7 @@ var init_database_contracts = __esm(() => {
18359
18411
  init_graphql();
18360
18412
  init_generate();
18361
18413
  init_graphql_options();
18414
+ init_database_contract_manifest();
18362
18415
  });
18363
18416
 
18364
18417
  // src/analyze.ts
@@ -19064,7 +19117,8 @@ var DEFAULT_EFFECT_ANALYSIS_OPTIONS = {
19064
19117
  requireExactDependencyTypes: true,
19065
19118
  requireTimeoutForDependencies: true,
19066
19119
  forbidDirectRuntimeExecution: true,
19067
- forbidDirectThrows: true
19120
+ forbidDirectThrows: true,
19121
+ requireRetryPolicy: true
19068
19122
  };
19069
19123
  function nodeText(node) {
19070
19124
  return node.getText(node.getSourceFile());
@@ -19137,7 +19191,8 @@ async function analyzeProject(rootDir, include, cache, changedPaths, effectOptio
19137
19191
  requireExactDependencyTypes: effectOptions.requireExactDependencyTypes ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.requireExactDependencyTypes,
19138
19192
  requireTimeoutForDependencies: effectOptions.requireTimeoutForDependencies ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.requireTimeoutForDependencies,
19139
19193
  forbidDirectRuntimeExecution: effectOptions.forbidDirectRuntimeExecution ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.forbidDirectRuntimeExecution,
19140
- forbidDirectThrows: effectOptions.forbidDirectThrows ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.forbidDirectThrows
19194
+ forbidDirectThrows: effectOptions.forbidDirectThrows ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.forbidDirectThrows,
19195
+ requireRetryPolicy: effectOptions.requireRetryPolicy ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.requireRetryPolicy
19141
19196
  }
19142
19197
  };
19143
19198
  if (ctx.effectOptions.forbidDirectRuntimeExecution) {
@@ -19960,6 +20015,21 @@ function resolveStaticObjectLiteral(input, ctx, seen = new Set) {
19960
20015
  }
19961
20016
  return ts7.isObjectLiteralExpression(input) ? input : undefined;
19962
20017
  }
20018
+ function resolveStaticArrayLiteral(input, ctx, seen = new Set) {
20019
+ if (!input || seen.has(input))
20020
+ return;
20021
+ seen.add(input);
20022
+ if (ts7.isAsExpression(input) || ts7.isSatisfiesExpression(input) || ts7.isParenthesizedExpression(input)) {
20023
+ return resolveStaticArrayLiteral(input.expression, ctx, seen);
20024
+ }
20025
+ if (ts7.isIdentifier(input)) {
20026
+ const declaration = resolveDeclaration(input, ctx).find(ts7.isVariableDeclaration);
20027
+ const list = declaration?.parent;
20028
+ const isConst = Boolean(list && ts7.isVariableDeclarationList(list) && (list.flags & ts7.NodeFlags.Const) !== 0);
20029
+ return isConst && declaration?.initializer ? resolveStaticArrayLiteral(declaration.initializer, ctx, seen) : undefined;
20030
+ }
20031
+ return ts7.isArrayLiteralExpression(input) ? input : undefined;
20032
+ }
19963
20033
  function routePolicyProperty(object, name, ctx, seen = new Set) {
19964
20034
  if (seen.has(object))
19965
20035
  return { unresolved: true };
@@ -20920,7 +20990,7 @@ function parseController(input, ctx) {
20920
20990
  file,
20921
20991
  line: lineOf(effectExpr),
20922
20992
  message: `Route ${route.handler} effect must be a statically resolvable object literal.`,
20923
- suggestion: "Use effect: { required: true, errors: [...], dependencies: [...] }."
20993
+ suggestion: 'Use effect: { required: true, errors: [...], dependencies: [...], retry: "none" }.'
20924
20994
  });
20925
20995
  } else {
20926
20996
  const effect = { required: true };
@@ -20951,6 +21021,8 @@ function parseController(input, ctx) {
20951
21021
  } else {
20952
21022
  effect.retry = retry;
20953
21023
  }
21024
+ } else if (ctx.effectOptions.requireRetryPolicy === false) {
21025
+ effect.retry = "none";
20954
21026
  }
20955
21027
  const maxAttemptsExpr = getProp(effectObject, "maxAttempts");
20956
21028
  if (maxAttemptsExpr !== undefined) {
@@ -20999,7 +21071,8 @@ function parseController(input, ctx) {
20999
21071
  }
21000
21072
  const dependenciesExpr = getProp(effectObject, "dependencies");
21001
21073
  if (dependenciesExpr !== undefined) {
21002
- if (!ts7.isArrayLiteralExpression(dependenciesExpr) || dependenciesExpr.elements.some((element) => !ts7.isStringLiteral(element) || element.text.trim().length === 0)) {
21074
+ const dependenciesArray = resolveStaticArrayLiteral(dependenciesExpr, ctx);
21075
+ if (!dependenciesArray || dependenciesArray.elements.some((element) => !ts7.isStringLiteral(element) || element.text.trim().length === 0)) {
21003
21076
  ctx.diagnostics.push({
21004
21077
  severity: "error",
21005
21078
  code: "invalid-effect-dependencies",
@@ -21007,10 +21080,10 @@ function parseController(input, ctx) {
21007
21080
  docsUrl: "https://supacloud.dev/errors/SC3033",
21008
21081
  file,
21009
21082
  line: lineOf(dependenciesExpr),
21010
- message: `Route ${route.handler} effect.dependencies must be an explicit string array.`
21083
+ message: `Route ${route.handler} effect.dependencies must be a string array or a const initialized with one.`
21011
21084
  });
21012
21085
  } else {
21013
- effect.dependencies = dependenciesExpr.elements.map((element) => element.text);
21086
+ effect.dependencies = dependenciesArray.elements.map((element) => element.text);
21014
21087
  if (new Set(effect.dependencies).size !== effect.dependencies.length) {
21015
21088
  ctx.diagnostics.push({
21016
21089
  severity: "error",
@@ -21027,7 +21100,8 @@ function parseController(input, ctx) {
21027
21100
  }
21028
21101
  const errorsExpr = getProp(effectObject, "errors");
21029
21102
  if (errorsExpr !== undefined) {
21030
- if (!ts7.isArrayLiteralExpression(errorsExpr)) {
21103
+ const errorsArray = resolveStaticArrayLiteral(errorsExpr, ctx);
21104
+ if (!errorsArray) {
21031
21105
  ctx.diagnostics.push({
21032
21106
  severity: "error",
21033
21107
  code: "invalid-effect-errors",
@@ -21035,11 +21109,11 @@ function parseController(input, ctx) {
21035
21109
  docsUrl: "https://supacloud.dev/errors/SC3034",
21036
21110
  file,
21037
21111
  line: lineOf(errorsExpr),
21038
- message: `Route ${route.handler} effect.errors must be an explicit object array.`
21112
+ message: `Route ${route.handler} effect.errors must be an object array or a const initialized with one.`
21039
21113
  });
21040
21114
  } else {
21041
21115
  const errors = [];
21042
- for (const errorExpr of errorsExpr.elements) {
21116
+ for (const errorExpr of errorsArray.elements) {
21043
21117
  if (!ts7.isObjectLiteralExpression(errorExpr)) {
21044
21118
  ctx.diagnostics.push({
21045
21119
  severity: "error",
@@ -21056,6 +21130,23 @@ function parseController(input, ctx) {
21056
21130
  const code = stringLiteralProp(errorExpr, "code");
21057
21131
  const status = numberLiteralProp(errorExpr, "status");
21058
21132
  const message = stringLiteralProp(errorExpr, "message");
21133
+ const codeFromError = booleanLiteralProp(errorExpr, "codeFromError");
21134
+ const messageFromError = booleanLiteralProp(errorExpr, "messageFromError");
21135
+ const statusFromError = booleanLiteralProp(errorExpr, "statusFromError");
21136
+ const retryable = booleanLiteralProp(errorExpr, "retryable");
21137
+ const outcomeUnknown = booleanLiteralProp(errorExpr, "outcomeUnknown");
21138
+ if (invalidBooleanProp(errorExpr, "codeFromError") || invalidBooleanProp(errorExpr, "messageFromError") || invalidBooleanProp(errorExpr, "statusFromError") || invalidBooleanProp(errorExpr, "retryable") || invalidBooleanProp(errorExpr, "outcomeUnknown")) {
21139
+ ctx.diagnostics.push({
21140
+ severity: "error",
21141
+ code: "invalid-effect-errors",
21142
+ errorCode: "SC3034",
21143
+ docsUrl: "https://supacloud.dev/errors/SC3034",
21144
+ file,
21145
+ line: lineOf(errorExpr),
21146
+ message: `Route ${route.handler} effect.errors instance flags must be boolean literals.`
21147
+ });
21148
+ continue;
21149
+ }
21059
21150
  if (!tag || !code || status === undefined || !Number.isInteger(status) || status < 400 || status > 599) {
21060
21151
  ctx.diagnostics.push({
21061
21152
  severity: "error",
@@ -21068,7 +21159,17 @@ function parseController(input, ctx) {
21068
21159
  });
21069
21160
  continue;
21070
21161
  }
21071
- errors.push({ tag, code, status, ...message === undefined ? {} : { message } });
21162
+ errors.push({
21163
+ tag,
21164
+ code,
21165
+ status,
21166
+ ...message === undefined ? {} : { message },
21167
+ ...codeFromError === undefined ? {} : { codeFromError },
21168
+ ...messageFromError === undefined ? {} : { messageFromError },
21169
+ ...statusFromError === undefined ? {} : { statusFromError },
21170
+ ...retryable === undefined ? {} : { retryable },
21171
+ ...outcomeUnknown === undefined ? {} : { outcomeUnknown }
21172
+ });
21072
21173
  }
21073
21174
  effect.errors = errors;
21074
21175
  if (new Set(errors.map((entry) => entry.tag)).size !== errors.length) {
@@ -21796,6 +21897,19 @@ function numberLiteralProp(obj, name) {
21796
21897
  const value = Number(expr.text);
21797
21898
  return Number.isFinite(value) ? value : undefined;
21798
21899
  }
21900
+ function booleanLiteralProp(obj, name) {
21901
+ const expr = getProp(obj, name);
21902
+ if (!expr || !isBooleanLiteral(expr))
21903
+ return;
21904
+ return expr.kind === ts7.SyntaxKind.TrueKeyword;
21905
+ }
21906
+ function isBooleanLiteral(expr) {
21907
+ return expr.kind === ts7.SyntaxKind.TrueKeyword || expr.kind === ts7.SyntaxKind.FalseKeyword;
21908
+ }
21909
+ function invalidBooleanProp(obj, name) {
21910
+ const expr = getProp(obj, name);
21911
+ return expr !== undefined && !isBooleanLiteral(expr);
21912
+ }
21799
21913
  function arrayProp(obj, name) {
21800
21914
  const expr = getProp(obj, name);
21801
21915
  return expr && ts7.isArrayLiteralExpression(expr) ? [...expr.elements] : [];
@@ -22738,6 +22852,9 @@ function validateEffectPolicies(graph, options = {}) {
22738
22852
  if (options.requireDependencies && effect.dependencies === undefined) {
22739
22853
  diagnostics.push(diagnostic("effect-dependencies-required", `Route ${label} declares an Effect contract without dependencies.`, controller.file, "Declare the logical services required by the Effect environment."));
22740
22854
  }
22855
+ if (effect.retry === undefined && options.requireRetryPolicy !== false) {
22856
+ diagnostics.push(diagnostic("effect-retry-required", `Route ${label} declares an Effect contract but does not declare retry.`, controller.file, 'Set effect.retry to "none". That is the required default; use "explicit" only with maxAttempts and a retryable 5xx allow-list.'));
22857
+ }
22741
22858
  if (effect.retry === "explicit" && effect.maxAttempts === undefined) {
22742
22859
  diagnostics.push(diagnostic("invalid-effect-retry", `Route ${label} enables explicit Effect retry without maxAttempts.`, controller.file, "Declare a positive maxAttempts value including the initial attempt."));
22743
22860
  }
@@ -22748,6 +22865,18 @@ function validateEffectPolicies(graph, options = {}) {
22748
22865
  if (command && command.idempotency !== "required") {
22749
22866
  diagnostics.push(diagnostic("effect-command-retry-forbidden", `Route ${label} enables explicit Effect retry for non-idempotent command "${command.name}".`, controller.file, 'Set command idempotency to "required" or use effect.retry: "none".'));
22750
22867
  }
22868
+ const retryable = (effect.errors ?? []).filter((entry) => entry.retryable === true);
22869
+ if (retryable.length === 0) {
22870
+ diagnostics.push(diagnostic("effect-retry-target-required", `Route ${label} enables explicit Effect retry without a transient failure allow-list.`, controller.file, 'Mark only 5xx failures that did not commit with retryable: true, or use effect.retry: "none".'));
22871
+ }
22872
+ }
22873
+ for (const entry of effect.errors ?? []) {
22874
+ if (entry.retryable !== true)
22875
+ continue;
22876
+ const transient = effect.retry === "explicit" && entry.outcomeUnknown !== true && entry.statusFromError !== true && entry.status >= 500 && entry.status <= 599;
22877
+ if (!transient) {
22878
+ diagnostics.push(diagnostic("effect-retryable-invalid", `Route ${label} marks "${entry.tag}" retryable, but in-process retry is only for declared 5xx failures that are not outcome-unknown.`, controller.file, 'Remove retryable, or keep it on a 5xx mapping with retry: "explicit" and without outcomeUnknown or statusFromError.'));
22879
+ }
22751
22880
  }
22752
22881
  }
22753
22882
  }
@@ -24991,10 +25120,10 @@ import { readFileSync as readFileSync4 } from "node:fs";
24991
25120
  import { resolve as resolve17 } from "node:path";
24992
25121
 
24993
25122
  // src/migration-app-version.ts
24994
- var MIGRATION_APP_VERSION = "0.35.0";
25123
+ var MIGRATION_APP_VERSION = "0.36.0";
24995
25124
 
24996
25125
  // src/migration-elysia-version.ts
24997
- var MIGRATION_ELYSIA_VERSION = "0.37.0";
25126
+ var MIGRATION_ELYSIA_VERSION = "0.38.1";
24998
25127
 
24999
25128
  // src/migration-policy.ts
25000
25129
  var cachedCompilerVersion;
@@ -25013,7 +25142,7 @@ function migrationDependencies() {
25013
25142
  "@supacloud/app": MIGRATION_APP_VERSION,
25014
25143
  "@supacloud/compiler": compilerVersion(),
25015
25144
  "@supacloud/elysia": MIGRATION_ELYSIA_VERSION,
25016
- elysia: "2.0.0-rc.0",
25145
+ elysia: "2.0.0-rc.1",
25017
25146
  typescript: "7.0.2"
25018
25147
  };
25019
25148
  }
@@ -26357,7 +26486,9 @@ var DEFAULT_SUPACLOUD_CONFIG = {
26357
26486
  requireExactDependencyTypes: true,
26358
26487
  requireTimeoutForDependencies: true,
26359
26488
  forbidDirectRuntimeExecution: true,
26360
- forbidDirectThrows: true
26489
+ forbidDirectThrows: true,
26490
+ requireRetryPolicy: true,
26491
+ defaultRetry: "none"
26361
26492
  }
26362
26493
  };
26363
26494
  function defineSupacloudConfig(config = {}) {
@@ -26439,7 +26570,9 @@ function validateGovernanceConfig(config) {
26439
26570
  "requireExactDependencyTypes",
26440
26571
  "requireTimeoutForDependencies",
26441
26572
  "forbidDirectRuntimeExecution",
26442
- "forbidDirectThrows"
26573
+ "forbidDirectThrows",
26574
+ "requireRetryPolicy",
26575
+ "defaultRetry"
26443
26576
  ].includes(key))) {
26444
26577
  throw new Error("effect contains an unsupported governance option.");
26445
26578
  }
@@ -26451,12 +26584,16 @@ function validateGovernanceConfig(config) {
26451
26584
  "requireExactDependencyTypes",
26452
26585
  "requireTimeoutForDependencies",
26453
26586
  "forbidDirectRuntimeExecution",
26454
- "forbidDirectThrows"
26587
+ "forbidDirectThrows",
26588
+ "requireRetryPolicy"
26455
26589
  ]) {
26456
26590
  if (config.effect[key] !== undefined && typeof config.effect[key] !== "boolean") {
26457
26591
  throw new Error(`effect.${key} must be boolean.`);
26458
26592
  }
26459
26593
  }
26594
+ if (config.effect["defaultRetry"] !== undefined && config.effect["defaultRetry"] !== "none") {
26595
+ throw new Error('effect.defaultRetry must be "none". explicit can only be written on a route, with maxAttempts and a retryable 5xx allow-list.');
26596
+ }
26460
26597
  }
26461
26598
  }
26462
26599
  function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
@@ -26782,6 +26919,7 @@ function introspectionQuery() {
26782
26919
 
26783
26920
  // src/index.ts
26784
26921
  init_database_contracts();
26922
+ init_database_contract_manifest();
26785
26923
  export {
26786
26924
  ANGULAR_ENTERPRISE_RULES,
26787
26925
  APPLICATION_DEVELOPMENT_ARCHIVE_MAX_BYTES2 as APPLICATION_DEVELOPMENT_ARCHIVE_MAX_BYTES,
@@ -26868,6 +27006,7 @@ export {
26868
27006
  migrateProject,
26869
27007
  migrateRouteResponse,
26870
27008
  parseApplicationDevelopmentContext,
27009
+ parseDatabaseContractManifest,
26871
27010
  parseDatabaseContractsOptions,
26872
27011
  parseDeliveryBuildManifest,
26873
27012
  parseDeliveryBuildResult,
@@ -1 +1 @@
1
- export declare const MIGRATION_APP_VERSION = "0.35.0";
1
+ export declare const MIGRATION_APP_VERSION = "0.36.0";
@@ -1 +1 @@
1
- export declare const MIGRATION_ELYSIA_VERSION = "0.37.0";
1
+ export declare const MIGRATION_ELYSIA_VERSION = "0.38.1";
package/dist/types.d.ts CHANGED
@@ -191,6 +191,19 @@ export interface EffectErrorMappingNode {
191
191
  status: number;
192
192
  code: string;
193
193
  message?: string;
194
+ /** Use the failure value's own `code` string when it is non-empty and bounded. */
195
+ codeFromError?: boolean;
196
+ /** Use the failure value's own `message` string when it is non-empty and bounded. */
197
+ messageFromError?: boolean;
198
+ /** Use the failure value's own HTTP status when it is an integer from 400 to 599. */
199
+ statusFromError?: boolean;
200
+ /**
201
+ * In-process retry allow-list. Only a declared 5xx failure that did not
202
+ * commit may be retried by the framework adapter.
203
+ */
204
+ retryable?: boolean;
205
+ /** The write may already have happened. The framework adapter must not retry it. */
206
+ outcomeUnknown?: boolean;
194
207
  }
195
208
  export interface EffectRouteNode {
196
209
  required: true;
@@ -511,6 +524,13 @@ export interface EffectCompilerOptions {
511
524
  forbidDirectRuntimeExecution?: boolean;
512
525
  /** Reject direct throw statements inside governed route handlers. */
513
526
  forbidDirectThrows?: boolean;
527
+ /** Require every route that declares an Effect contract to write an explicit retry policy. */
528
+ requireRetryPolicy?: boolean;
529
+ /**
530
+ * Materialize an omitted retry policy when requireRetryPolicy is disabled.
531
+ * The only project-level default is "none"; "explicit" stays route-local.
532
+ */
533
+ defaultRetry?: "none";
514
534
  }
515
535
  export interface GraphqlOptions {
516
536
  /** Additionally emit standard TypedDocumentNode artifacts for GraphQL ecosystem clients. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/compiler",
3
- "version": "0.53.1",
3
+ "version": "0.55.0",
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.3",
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",