@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 +26 -12
- package/dist/cli.js +197 -60
- package/dist/database-contract-manifest.d.ts +9 -0
- package/dist/database-contracts.d.ts +9 -27
- package/dist/index.d.ts +2 -0
- package/dist/index.js +199 -60
- package/dist/migration-app-version.d.ts +1 -1
- package/dist/migration-elysia-version.d.ts +1 -1
- package/dist/types.d.ts +20 -0
- package/package.json +2 -2
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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
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: "
|
|
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).
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
18212
|
-
|
|
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,
|
|
18221
|
-
drizzleSchema: resolve25(directory,
|
|
18259
|
+
...postgrestTypes ? { postgrestTypes: resolve25(directory, postgrestTypes) } : {},
|
|
18260
|
+
...drizzleSchema ? { drizzleSchema: resolve25(directory, drizzleSchema) } : {},
|
|
18222
18261
|
role: field("role"),
|
|
18223
|
-
graphql
|
|
18262
|
+
...graphql ? { graphql } : {},
|
|
18224
18263
|
migrations: migrations.map((file) => resolve25(directory, file))
|
|
18225
18264
|
};
|
|
18226
18265
|
}
|
|
18227
|
-
async function
|
|
18228
|
-
const
|
|
18229
|
-
const
|
|
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([
|
|
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
|
-
|
|
18247
|
-
|
|
18248
|
-
|
|
18249
|
-
|
|
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
|
-
|
|
18261
|
-
|
|
18262
|
-
|
|
18263
|
-
|
|
18264
|
-
|
|
18265
|
-
|
|
18266
|
-
|
|
18267
|
-
|
|
18268
|
-
|
|
18269
|
-
|
|
18270
|
-
|
|
18271
|
-
|
|
18272
|
-
|
|
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
|
-
|
|
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
|
|
18281
|
-
|
|
18282
|
-
|
|
18283
|
-
|
|
18284
|
-
|
|
18285
|
-
|
|
18286
|
-
|
|
18287
|
-
|
|
18288
|
-
|
|
18289
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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({
|
|
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.
|
|
25871
|
+
var MIGRATION_APP_VERSION = "0.36.0";
|
|
25735
25872
|
|
|
25736
25873
|
// src/migration-elysia-version.ts
|
|
25737
|
-
var MIGRATION_ELYSIA_VERSION = "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.
|
|
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
|
-
/**
|
|
6
|
-
postgrestTypes
|
|
7
|
-
/**
|
|
8
|
-
drizzleSchema
|
|
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
|
|
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
|
|
18244
|
-
|
|
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,
|
|
18253
|
-
drizzleSchema: resolve24(directory,
|
|
18291
|
+
...postgrestTypes ? { postgrestTypes: resolve24(directory, postgrestTypes) } : {},
|
|
18292
|
+
...drizzleSchema ? { drizzleSchema: resolve24(directory, drizzleSchema) } : {},
|
|
18254
18293
|
role: field("role"),
|
|
18255
|
-
graphql
|
|
18294
|
+
...graphql ? { graphql } : {},
|
|
18256
18295
|
migrations: migrations.map((file) => resolve24(directory, file))
|
|
18257
18296
|
};
|
|
18258
18297
|
}
|
|
18259
|
-
async function
|
|
18260
|
-
const
|
|
18261
|
-
const
|
|
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([
|
|
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
|
-
|
|
18279
|
-
|
|
18280
|
-
|
|
18281
|
-
|
|
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
|
-
|
|
18293
|
-
|
|
18294
|
-
|
|
18295
|
-
|
|
18296
|
-
|
|
18297
|
-
|
|
18298
|
-
|
|
18299
|
-
|
|
18300
|
-
|
|
18301
|
-
|
|
18302
|
-
|
|
18303
|
-
|
|
18304
|
-
|
|
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
|
-
|
|
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
|
|
18313
|
-
|
|
18314
|
-
|
|
18315
|
-
|
|
18316
|
-
|
|
18317
|
-
|
|
18318
|
-
|
|
18319
|
-
|
|
18320
|
-
|
|
18321
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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({
|
|
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.
|
|
25123
|
+
var MIGRATION_APP_VERSION = "0.36.0";
|
|
24995
25124
|
|
|
24996
25125
|
// src/migration-elysia-version.ts
|
|
24997
|
-
var MIGRATION_ELYSIA_VERSION = "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.
|
|
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.
|
|
1
|
+
export declare const MIGRATION_APP_VERSION = "0.36.0";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const MIGRATION_ELYSIA_VERSION = "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.
|
|
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.
|
|
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",
|