@supacloud/cli 0.71.1 → 0.73.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 +12 -0
- package/dist/index.js +533 -62
- package/package.json +4 -3
- package/skills/supacloud-cli/SKILL.md +15 -6
package/README.md
CHANGED
|
@@ -532,6 +532,7 @@ supacloud-cli database query --sql "select now()"
|
|
|
532
532
|
supacloud-cli database query --ref abc123 --file ./queries/vector-search.sql
|
|
533
533
|
supacloud-cli database migration_inventory --ref abc123
|
|
534
534
|
supacloud-cli database lint_migrations --dir supabase/migrations
|
|
535
|
+
supacloud-cli database rebase_migrations --dir supabase/migrations --baseline_file backups/schema.sql --baseline_version 20261008000000 --output_dir supabase/migrations-rebased
|
|
535
536
|
supacloud-cli database push_migrations --ref abc123 --dir supabase/migrations --dry_run
|
|
536
537
|
supacloud-cli supabase migration_new --name add_accounts
|
|
537
538
|
supacloud-cli supabase db_diff --schema public --name add_accounts
|
|
@@ -638,6 +639,17 @@ even when strict destructive-risk mode is not enabled.
|
|
|
638
639
|
`CREATE/DROP INDEX CONCURRENTLY`, `VACUUM`, and other non-transactional work must
|
|
639
640
|
run through an approved maintenance path outside `push_migrations`.
|
|
640
641
|
|
|
642
|
+
`database rebase_migrations` is local-only and never contacts the Management
|
|
643
|
+
API. Without `--write` it only prints the plan; with `--write` it writes a
|
|
644
|
+
reconstructed baseline, the retained forward migrations, and a
|
|
645
|
+
`migration-rebase.manifest.json` (with SHA-256 records for the complete source
|
|
646
|
+
history) into a separate `--output_dir`, never the source directory. Capture and
|
|
647
|
+
review a schema-only snapshot first with `--baseline_file` and
|
|
648
|
+
`--baseline_version`, then use `--retain_after_version` to keep newer
|
|
649
|
+
migrations. Review the baseline for omitted data backfills, reference data,
|
|
650
|
+
extensions, grants, RLS, functions and triggers, replay it in a clean database,
|
|
651
|
+
and archive the original directory until clean replay and remote read-back pass.
|
|
652
|
+
|
|
641
653
|
Migration applications and DDL executions send PostgREST schema reload
|
|
642
654
|
notifications (`NOTIFY pgrst_<ref>, 'reload schema'`) so schema changes become
|
|
643
655
|
visible without a manual PostgREST restart. Responses distinguish `notified`
|
package/dist/index.js
CHANGED
|
@@ -207715,7 +207715,7 @@ var ACTION_POLICY = {
|
|
|
207715
207715
|
read: ["status", "logs"],
|
|
207716
207716
|
write: ["upload", "configure", "deploy", "rollback", "reconcile", "retire"]
|
|
207717
207717
|
},
|
|
207718
|
-
db: { local: ["lint", "explain"], read: ["module_check"] },
|
|
207718
|
+
db: { local: ["context", "lint", "explain"], read: ["module_check"] },
|
|
207719
207719
|
dev: { read: ["status"], write: ["sync", "watch", "migrate"] }
|
|
207720
207720
|
};
|
|
207721
207721
|
function declaredMode(moduleName, action) {
|
|
@@ -213546,21 +213546,18 @@ export default app;
|
|
|
213546
213546
|
scripts: { build: "vite build" },
|
|
213547
213547
|
devDependencies: {
|
|
213548
213548
|
"@supacloud/function-adapter": "^0.1.0",
|
|
213549
|
-
"@sveltejs/kit": "^
|
|
213549
|
+
"@sveltejs/kit": "^3.0.1",
|
|
213550
213550
|
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|
|
213551
213551
|
svelte: "^5.57.0",
|
|
213552
213552
|
vite: "^8.2.2"
|
|
213553
213553
|
}
|
|
213554
213554
|
}, null, 2) + `
|
|
213555
|
-
`,
|
|
213556
|
-
"svelte.config.js": `import adapter from "@supacloud/function-adapter/sveltekit-adapter";
|
|
213557
|
-
|
|
213558
|
-
export default { kit: { adapter: adapter() } };
|
|
213559
213555
|
`,
|
|
213560
213556
|
"vite.config.ts": `import { sveltekit } from "@sveltejs/kit/vite";
|
|
213557
|
+
import adapter from "@supacloud/function-adapter/sveltekit-adapter";
|
|
213561
213558
|
import { defineConfig } from "vite";
|
|
213562
213559
|
|
|
213563
|
-
export default defineConfig({ plugins: [sveltekit()] });
|
|
213560
|
+
export default defineConfig({ plugins: [sveltekit({ adapter: adapter() })] });
|
|
213564
213561
|
`,
|
|
213565
213562
|
"src/routes/+server.ts": `export function GET() {
|
|
213566
213563
|
return Response.json({ function: "${slug}", framework: "sveltekit-function" });
|
|
@@ -221501,6 +221498,17 @@ var init_validate = __esm2(() => {
|
|
|
221501
221498
|
"route-contract-required": { code: "SC3021", docsUrl: "https://supacloud.dev/errors/SC3021" },
|
|
221502
221499
|
"route-contract-unverified": { code: "SC3022", docsUrl: "https://supacloud.dev/errors/SC3022" },
|
|
221503
221500
|
"route-contract-evidence-required": { code: "SC3023", docsUrl: "https://supacloud.dev/errors/SC3023" },
|
|
221501
|
+
"invalid-effect-contract": { code: "SC3030", docsUrl: "https://supacloud.dev/errors/SC3030" },
|
|
221502
|
+
"invalid-effect-retry": { code: "SC3031", docsUrl: "https://supacloud.dev/errors/SC3031" },
|
|
221503
|
+
"invalid-effect-timeout": { code: "SC3032", docsUrl: "https://supacloud.dev/errors/SC3032" },
|
|
221504
|
+
"invalid-effect-dependencies": { code: "SC3033", docsUrl: "https://supacloud.dev/errors/SC3033" },
|
|
221505
|
+
"invalid-effect-errors": { code: "SC3034", docsUrl: "https://supacloud.dev/errors/SC3034" },
|
|
221506
|
+
"effect-return-type": { code: "SC3035", docsUrl: "https://supacloud.dev/errors/SC3035" },
|
|
221507
|
+
"effect-contract-required": { code: "SC3036", docsUrl: "https://supacloud.dev/errors/SC3036" },
|
|
221508
|
+
"effect-error-mapping-required": { code: "SC3037", docsUrl: "https://supacloud.dev/errors/SC3037" },
|
|
221509
|
+
"effect-dependencies-required": { code: "SC3038", docsUrl: "https://supacloud.dev/errors/SC3038" },
|
|
221510
|
+
"effect-command-retry-forbidden": { code: "SC3039", docsUrl: "https://supacloud.dev/errors/SC3039" },
|
|
221511
|
+
"effect-dependency-type": { code: "SC3040", docsUrl: "https://supacloud.dev/errors/SC3040" },
|
|
221504
221512
|
"invalid-route-response-map": { code: "SC3024", docsUrl: "https://supacloud.dev/errors/SC3024" },
|
|
221505
221513
|
"invalid-route-response-selector": { code: "SC3025", docsUrl: "https://supacloud.dev/errors/SC3025" },
|
|
221506
221514
|
"conflicting-route-response-schema": { code: "SC3026", docsUrl: "https://supacloud.dev/errors/SC3026" },
|
|
@@ -222029,6 +222037,9 @@ class ModuleGenerator {
|
|
|
222029
222037
|
});
|
|
222030
222038
|
fields.push(`responses: { ${responseFields.join(", ")} }`);
|
|
222031
222039
|
}
|
|
222040
|
+
if (route.effect !== undefined) {
|
|
222041
|
+
fields.push(`effect: ${JSON.stringify(route.effect)}`);
|
|
222042
|
+
}
|
|
222032
222043
|
if (route.command)
|
|
222033
222044
|
fields.push(`command: ${JSON.stringify(route.command)}`);
|
|
222034
222045
|
if (route.guards && route.guards.length > 0) {
|
|
@@ -223841,6 +223852,14 @@ var INTERFACES = `export interface CompiledRoute {
|
|
|
223841
223852
|
cookie?: unknown;
|
|
223842
223853
|
response?: unknown;
|
|
223843
223854
|
responses?: Record<string | number, unknown>;
|
|
223855
|
+
effect?: {
|
|
223856
|
+
required: true;
|
|
223857
|
+
errors?: Array<{ tag: string; status: number; code: string; message?: string }>;
|
|
223858
|
+
dependencies?: string[];
|
|
223859
|
+
retry?: "none" | "explicit";
|
|
223860
|
+
maxAttempts?: number;
|
|
223861
|
+
timeoutMs?: number;
|
|
223862
|
+
};
|
|
223844
223863
|
/** Compile-time ownership and transport classification for the route boundary. */
|
|
223845
223864
|
contract?: {
|
|
223846
223865
|
body?: "framework" | "domain";
|
|
@@ -246268,6 +246287,188 @@ function parseController(input, ctx) {
|
|
|
246268
246287
|
Object.assign(route.contract, { [field]: value });
|
|
246269
246288
|
}
|
|
246270
246289
|
}
|
|
246290
|
+
const effectExpr = getProp(optionsObject, "effect");
|
|
246291
|
+
if (effectExpr) {
|
|
246292
|
+
const effectObject = resolveStaticObjectLiteral(effectExpr, ctx);
|
|
246293
|
+
if (!effectObject) {
|
|
246294
|
+
ctx.diagnostics.push({
|
|
246295
|
+
severity: "error",
|
|
246296
|
+
code: "invalid-effect-contract",
|
|
246297
|
+
errorCode: "SC3030",
|
|
246298
|
+
docsUrl: "https://supacloud.dev/errors/SC3030",
|
|
246299
|
+
file,
|
|
246300
|
+
line: lineOf2(effectExpr),
|
|
246301
|
+
message: `Route ${route.handler} effect must be a statically resolvable object literal.`,
|
|
246302
|
+
suggestion: "Use effect: { required: true, errors: [...], dependencies: [...] }."
|
|
246303
|
+
});
|
|
246304
|
+
} else {
|
|
246305
|
+
const effect = { required: true };
|
|
246306
|
+
const requiredExpr = getProp(effectObject, "required");
|
|
246307
|
+
if (requiredExpr !== undefined && booleanProp(effectObject, "required") !== true) {
|
|
246308
|
+
ctx.diagnostics.push({
|
|
246309
|
+
severity: "error",
|
|
246310
|
+
code: "invalid-effect-contract",
|
|
246311
|
+
errorCode: "SC3030",
|
|
246312
|
+
docsUrl: "https://supacloud.dev/errors/SC3030",
|
|
246313
|
+
file,
|
|
246314
|
+
line: lineOf2(requiredExpr),
|
|
246315
|
+
message: `Route ${route.handler} effect.required must be the literal true.`
|
|
246316
|
+
});
|
|
246317
|
+
}
|
|
246318
|
+
const retry = stringLiteralProp(effectObject, "retry");
|
|
246319
|
+
if (retry !== undefined) {
|
|
246320
|
+
if (retry !== "none" && retry !== "explicit") {
|
|
246321
|
+
ctx.diagnostics.push({
|
|
246322
|
+
severity: "error",
|
|
246323
|
+
code: "invalid-effect-retry",
|
|
246324
|
+
errorCode: "SC3031",
|
|
246325
|
+
docsUrl: "https://supacloud.dev/errors/SC3031",
|
|
246326
|
+
file,
|
|
246327
|
+
line: lineOf2(getProp(effectObject, "retry")),
|
|
246328
|
+
message: `Route ${route.handler} effect.retry must be "none" or "explicit".`
|
|
246329
|
+
});
|
|
246330
|
+
} else {
|
|
246331
|
+
effect.retry = retry;
|
|
246332
|
+
}
|
|
246333
|
+
}
|
|
246334
|
+
const maxAttemptsExpr = getProp(effectObject, "maxAttempts");
|
|
246335
|
+
if (maxAttemptsExpr !== undefined) {
|
|
246336
|
+
const maxAttempts = numberLiteralProp(effectObject, "maxAttempts");
|
|
246337
|
+
if (maxAttempts === undefined || maxAttempts < 1 || !Number.isInteger(maxAttempts)) {
|
|
246338
|
+
ctx.diagnostics.push({
|
|
246339
|
+
severity: "error",
|
|
246340
|
+
code: "invalid-effect-retry",
|
|
246341
|
+
errorCode: "SC3031",
|
|
246342
|
+
docsUrl: "https://supacloud.dev/errors/SC3031",
|
|
246343
|
+
file,
|
|
246344
|
+
line: lineOf2(maxAttemptsExpr),
|
|
246345
|
+
message: `Route ${route.handler} effect.maxAttempts must be a positive integer.`
|
|
246346
|
+
});
|
|
246347
|
+
} else {
|
|
246348
|
+
effect.maxAttempts = maxAttempts;
|
|
246349
|
+
}
|
|
246350
|
+
}
|
|
246351
|
+
if (effect.retry === "explicit" && effect.maxAttempts === undefined) {
|
|
246352
|
+
ctx.diagnostics.push({
|
|
246353
|
+
severity: "error",
|
|
246354
|
+
code: "invalid-effect-retry",
|
|
246355
|
+
errorCode: "SC3031",
|
|
246356
|
+
docsUrl: "https://supacloud.dev/errors/SC3031",
|
|
246357
|
+
file,
|
|
246358
|
+
line: lineOf2(effectExpr),
|
|
246359
|
+
message: `Route ${route.handler} effect.maxAttempts is required when retry is "explicit".`
|
|
246360
|
+
});
|
|
246361
|
+
}
|
|
246362
|
+
const timeoutExpr = getProp(effectObject, "timeoutMs");
|
|
246363
|
+
if (timeoutExpr !== undefined) {
|
|
246364
|
+
const timeoutMs = numberLiteralProp(effectObject, "timeoutMs");
|
|
246365
|
+
if (timeoutMs === undefined || timeoutMs <= 0 || !Number.isInteger(timeoutMs)) {
|
|
246366
|
+
ctx.diagnostics.push({
|
|
246367
|
+
severity: "error",
|
|
246368
|
+
code: "invalid-effect-timeout",
|
|
246369
|
+
errorCode: "SC3032",
|
|
246370
|
+
docsUrl: "https://supacloud.dev/errors/SC3032",
|
|
246371
|
+
file,
|
|
246372
|
+
line: lineOf2(timeoutExpr),
|
|
246373
|
+
message: `Route ${route.handler} effect.timeoutMs must be a positive integer.`
|
|
246374
|
+
});
|
|
246375
|
+
} else {
|
|
246376
|
+
effect.timeoutMs = timeoutMs;
|
|
246377
|
+
}
|
|
246378
|
+
}
|
|
246379
|
+
const dependenciesExpr = getProp(effectObject, "dependencies");
|
|
246380
|
+
if (dependenciesExpr !== undefined) {
|
|
246381
|
+
if (!ts7.isArrayLiteralExpression(dependenciesExpr) || dependenciesExpr.elements.some((element) => !ts7.isStringLiteral(element) || element.text.trim().length === 0)) {
|
|
246382
|
+
ctx.diagnostics.push({
|
|
246383
|
+
severity: "error",
|
|
246384
|
+
code: "invalid-effect-dependencies",
|
|
246385
|
+
errorCode: "SC3033",
|
|
246386
|
+
docsUrl: "https://supacloud.dev/errors/SC3033",
|
|
246387
|
+
file,
|
|
246388
|
+
line: lineOf2(dependenciesExpr),
|
|
246389
|
+
message: `Route ${route.handler} effect.dependencies must be an explicit string array.`
|
|
246390
|
+
});
|
|
246391
|
+
} else {
|
|
246392
|
+
effect.dependencies = dependenciesExpr.elements.map((element) => element.text);
|
|
246393
|
+
}
|
|
246394
|
+
}
|
|
246395
|
+
const errorsExpr = getProp(effectObject, "errors");
|
|
246396
|
+
if (errorsExpr !== undefined) {
|
|
246397
|
+
if (!ts7.isArrayLiteralExpression(errorsExpr)) {
|
|
246398
|
+
ctx.diagnostics.push({
|
|
246399
|
+
severity: "error",
|
|
246400
|
+
code: "invalid-effect-errors",
|
|
246401
|
+
errorCode: "SC3034",
|
|
246402
|
+
docsUrl: "https://supacloud.dev/errors/SC3034",
|
|
246403
|
+
file,
|
|
246404
|
+
line: lineOf2(errorsExpr),
|
|
246405
|
+
message: `Route ${route.handler} effect.errors must be an explicit object array.`
|
|
246406
|
+
});
|
|
246407
|
+
} else {
|
|
246408
|
+
const errors = [];
|
|
246409
|
+
for (const errorExpr of errorsExpr.elements) {
|
|
246410
|
+
if (!ts7.isObjectLiteralExpression(errorExpr)) {
|
|
246411
|
+
ctx.diagnostics.push({
|
|
246412
|
+
severity: "error",
|
|
246413
|
+
code: "invalid-effect-errors",
|
|
246414
|
+
errorCode: "SC3034",
|
|
246415
|
+
docsUrl: "https://supacloud.dev/errors/SC3034",
|
|
246416
|
+
file,
|
|
246417
|
+
line: lineOf2(errorExpr),
|
|
246418
|
+
message: `Route ${route.handler} effect.errors entries must be object literals.`
|
|
246419
|
+
});
|
|
246420
|
+
continue;
|
|
246421
|
+
}
|
|
246422
|
+
const tag = stringLiteralProp(errorExpr, "tag");
|
|
246423
|
+
const code = stringLiteralProp(errorExpr, "code");
|
|
246424
|
+
const status = numberLiteralProp(errorExpr, "status");
|
|
246425
|
+
const message = stringLiteralProp(errorExpr, "message");
|
|
246426
|
+
if (!tag || !code || status === undefined || !Number.isInteger(status) || status < 400 || status > 599) {
|
|
246427
|
+
ctx.diagnostics.push({
|
|
246428
|
+
severity: "error",
|
|
246429
|
+
code: "invalid-effect-errors",
|
|
246430
|
+
errorCode: "SC3034",
|
|
246431
|
+
docsUrl: "https://supacloud.dev/errors/SC3034",
|
|
246432
|
+
file,
|
|
246433
|
+
line: lineOf2(errorExpr),
|
|
246434
|
+
message: `Route ${route.handler} effect.errors entries require tag, code and an HTTP status from 400 to 599.`
|
|
246435
|
+
});
|
|
246436
|
+
continue;
|
|
246437
|
+
}
|
|
246438
|
+
errors.push({ tag, code, status, ...message === undefined ? {} : { message } });
|
|
246439
|
+
}
|
|
246440
|
+
effect.errors = errors;
|
|
246441
|
+
}
|
|
246442
|
+
}
|
|
246443
|
+
route.effect = effect;
|
|
246444
|
+
if (!resultType || !/\bEffect(?:\.Effect)?(?:<|\s)/.test(resultType)) {
|
|
246445
|
+
ctx.diagnostics.push({
|
|
246446
|
+
severity: "error",
|
|
246447
|
+
code: "effect-return-type",
|
|
246448
|
+
errorCode: "SC3035",
|
|
246449
|
+
docsUrl: "https://supacloud.dev/errors/SC3035",
|
|
246450
|
+
file,
|
|
246451
|
+
line: lineOf2(method),
|
|
246452
|
+
message: `Route ${route.handler} declares an Effect contract but returns ${resultType ?? "an unresolved type"}.`,
|
|
246453
|
+
suggestion: "Return Effect.Effect<Success, Failure, Requirements> from the handler."
|
|
246454
|
+
});
|
|
246455
|
+
}
|
|
246456
|
+
for (const dependency of effect.dependencies ?? []) {
|
|
246457
|
+
if (!resultType?.includes(dependency)) {
|
|
246458
|
+
ctx.diagnostics.push({
|
|
246459
|
+
severity: "error",
|
|
246460
|
+
code: "effect-dependency-type",
|
|
246461
|
+
errorCode: "SC3040",
|
|
246462
|
+
docsUrl: "https://supacloud.dev/errors/SC3040",
|
|
246463
|
+
file,
|
|
246464
|
+
line: lineOf2(method),
|
|
246465
|
+
message: `Route ${route.handler} declares Effect dependency "${dependency}", but it is absent from the return type environment ${resultType ?? "unknown"}.`,
|
|
246466
|
+
suggestion: "Keep effect.dependencies aligned with the third Effect type parameter."
|
|
246467
|
+
});
|
|
246468
|
+
}
|
|
246469
|
+
}
|
|
246470
|
+
}
|
|
246471
|
+
}
|
|
246271
246472
|
for (const field of ["body", "params", "query", "headers", "cookie", "response"]) {
|
|
246272
246473
|
const schemaExpr = getProp(optionsObject, field);
|
|
246273
246474
|
if (schemaExpr && ts7.isIdentifier(schemaExpr)) {
|
|
@@ -246851,6 +247052,13 @@ function stringLiteralProp(obj, name) {
|
|
|
246851
247052
|
const expr = getProp(obj, name);
|
|
246852
247053
|
return expr && ts7.isStringLiteral(expr) ? expr.text : undefined;
|
|
246853
247054
|
}
|
|
247055
|
+
function numberLiteralProp(obj, name) {
|
|
247056
|
+
const expr = getProp(obj, name);
|
|
247057
|
+
if (!expr || !ts7.isNumericLiteral(expr))
|
|
247058
|
+
return;
|
|
247059
|
+
const value = Number(expr.text);
|
|
247060
|
+
return Number.isFinite(value) ? value : undefined;
|
|
247061
|
+
}
|
|
246854
247062
|
function arrayProp(obj, name) {
|
|
246855
247063
|
const expr = getProp(obj, name);
|
|
246856
247064
|
return expr && ts7.isArrayLiteralExpression(expr) ? [...expr.elements] : [];
|
|
@@ -247492,6 +247700,55 @@ function validateClientSchemaBoundaries(graph, options) {
|
|
|
247492
247700
|
}
|
|
247493
247701
|
return diagnostics;
|
|
247494
247702
|
}
|
|
247703
|
+
init_validate();
|
|
247704
|
+
function diagnostic(code, message, file, suggestion) {
|
|
247705
|
+
const metadata = COMPILER_DIAGNOSTIC_CODES[code];
|
|
247706
|
+
return {
|
|
247707
|
+
severity: "error",
|
|
247708
|
+
code,
|
|
247709
|
+
message,
|
|
247710
|
+
...file === undefined ? {} : { file },
|
|
247711
|
+
...suggestion === undefined ? {} : { suggestion },
|
|
247712
|
+
...metadata ? { errorCode: metadata.code, docsUrl: metadata.docsUrl } : {}
|
|
247713
|
+
};
|
|
247714
|
+
}
|
|
247715
|
+
function routeLabel(route, controllerPath) {
|
|
247716
|
+
return `${route.method} ${controllerPath}${route.path}`;
|
|
247717
|
+
}
|
|
247718
|
+
function validateEffectPolicies(graph, options = {}) {
|
|
247719
|
+
const diagnostics = [];
|
|
247720
|
+
for (const module of graph.modules) {
|
|
247721
|
+
const commands = new Map(module.commands.map((command) => [command.className, command]));
|
|
247722
|
+
for (const controller of module.controllers) {
|
|
247723
|
+
for (const route of controller.routes) {
|
|
247724
|
+
const label = routeLabel(route, controller.path);
|
|
247725
|
+
if (options.requireRouteEffects && route.effect === undefined) {
|
|
247726
|
+
diagnostics.push(diagnostic("effect-contract-required", `Route ${label} must declare an Effect contract.`, controller.file, 'Add effect: { dependencies: [...], errors: [...], retry: "none" } to the route options.'));
|
|
247727
|
+
continue;
|
|
247728
|
+
}
|
|
247729
|
+
const effect = route.effect;
|
|
247730
|
+
if (effect === undefined)
|
|
247731
|
+
continue;
|
|
247732
|
+
if (options.requireErrorMappings && effect.errors === undefined) {
|
|
247733
|
+
diagnostics.push(diagnostic("effect-error-mapping-required", `Route ${label} declares an Effect contract without public error mappings.`, controller.file, "Map every expected domain failure to a stable HTTP status and public code."));
|
|
247734
|
+
}
|
|
247735
|
+
if (options.requireDependencies && effect.dependencies === undefined) {
|
|
247736
|
+
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."));
|
|
247737
|
+
}
|
|
247738
|
+
if (effect.retry === "explicit" && effect.maxAttempts === undefined) {
|
|
247739
|
+
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."));
|
|
247740
|
+
}
|
|
247741
|
+
if (route.command && effect.retry === "explicit") {
|
|
247742
|
+
const command = commands.get(route.command);
|
|
247743
|
+
if (command && command.idempotency !== "required") {
|
|
247744
|
+
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".'));
|
|
247745
|
+
}
|
|
247746
|
+
}
|
|
247747
|
+
}
|
|
247748
|
+
}
|
|
247749
|
+
}
|
|
247750
|
+
return diagnostics;
|
|
247751
|
+
}
|
|
247495
247752
|
function withDefaultGovernance(options) {
|
|
247496
247753
|
return options.commandCapabilities === undefined ? { ...options, commandCapabilities: {
|
|
247497
247754
|
requirePersistentAdapters: true,
|
|
@@ -247510,7 +247767,8 @@ async function compileProject(options) {
|
|
|
247510
247767
|
const diagnostics = [
|
|
247511
247768
|
...graph.diagnostics ?? [],
|
|
247512
247769
|
...validateGraph(graph, options),
|
|
247513
|
-
...validateClientSchemaBoundaries(graph, options)
|
|
247770
|
+
...validateClientSchemaBoundaries(graph, options),
|
|
247771
|
+
...validateEffectPolicies(graph, options.effect)
|
|
247514
247772
|
];
|
|
247515
247773
|
if (diagnostics.some((item) => item.code === "runtime-injection-disallowed")) {
|
|
247516
247774
|
return { diagnostics, graph, written: [] };
|
|
@@ -247574,7 +247832,8 @@ async function checkProject(options) {
|
|
|
247574
247832
|
const diagnostics = [
|
|
247575
247833
|
...graph.diagnostics ?? [],
|
|
247576
247834
|
...validateGraph(graph, options),
|
|
247577
|
-
...validateClientSchemaBoundaries(graph, options)
|
|
247835
|
+
...validateClientSchemaBoundaries(graph, options),
|
|
247836
|
+
...validateEffectPolicies(graph, options.effect)
|
|
247578
247837
|
];
|
|
247579
247838
|
if (diagnostics.some((item) => item.code === "runtime-injection-disallowed")) {
|
|
247580
247839
|
return { diagnostics, graph, upToDate: false, mismatches: ["Runtime DI must be migrated to constructor injection."] };
|
|
@@ -247656,7 +247915,7 @@ init_delivery_schema();
|
|
|
247656
247915
|
init_validate();
|
|
247657
247916
|
var compare2 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
247658
247917
|
var sorted = (values) => [...new Set(values)].sort(compare2);
|
|
247659
|
-
function
|
|
247918
|
+
function diagnostic2(error) {
|
|
247660
247919
|
return {
|
|
247661
247920
|
severity: error.severity,
|
|
247662
247921
|
code: error.code,
|
|
@@ -247681,8 +247940,8 @@ function createDeliveryPlan(graph, input) {
|
|
|
247681
247940
|
}] };
|
|
247682
247941
|
}
|
|
247683
247942
|
const diagnostics = [
|
|
247684
|
-
...(graph.diagnostics ?? []).map(
|
|
247685
|
-
...validateGraph(graph, { strict: true }).map(
|
|
247943
|
+
...(graph.diagnostics ?? []).map(diagnostic2),
|
|
247944
|
+
...validateGraph(graph, { strict: true }).map(diagnostic2)
|
|
247686
247945
|
];
|
|
247687
247946
|
const fail = (code, message, suggestion) => {
|
|
247688
247947
|
diagnostics.push({ severity: "error", code, message, suggestion });
|
|
@@ -249137,7 +249396,12 @@ var DEFAULT_SUPACLOUD_CONFIG = {
|
|
|
249137
249396
|
idempotency: true,
|
|
249138
249397
|
transaction: true
|
|
249139
249398
|
},
|
|
249140
|
-
moduleBoundaryPreset: "modular-monolith"
|
|
249399
|
+
moduleBoundaryPreset: "modular-monolith",
|
|
249400
|
+
effect: {
|
|
249401
|
+
requireRouteEffects: true,
|
|
249402
|
+
requireErrorMappings: true,
|
|
249403
|
+
requireDependencies: true
|
|
249404
|
+
}
|
|
249141
249405
|
};
|
|
249142
249406
|
function defineSupacloudConfig(config = {}) {
|
|
249143
249407
|
if (config.delivery !== undefined)
|
|
@@ -249149,7 +249413,8 @@ function defineSupacloudConfig(config = {}) {
|
|
|
249149
249413
|
...DEFAULT_SUPACLOUD_CONFIG,
|
|
249150
249414
|
...config,
|
|
249151
249415
|
include: config.include ?? [...DEFAULT_SUPACLOUD_CONFIG.include],
|
|
249152
|
-
graphql: config.graphql ?? DEFAULT_SUPACLOUD_CONFIG.graphql
|
|
249416
|
+
graphql: config.graphql ?? DEFAULT_SUPACLOUD_CONFIG.graphql,
|
|
249417
|
+
effect: config.effect === undefined ? { ...DEFAULT_SUPACLOUD_CONFIG.effect } : { ...DEFAULT_SUPACLOUD_CONFIG.effect, ...config.effect }
|
|
249153
249418
|
};
|
|
249154
249419
|
}
|
|
249155
249420
|
function validateGovernanceConfig(config) {
|
|
@@ -249206,6 +249471,16 @@ function validateGovernanceConfig(config) {
|
|
|
249206
249471
|
if (config[key] !== undefined && typeof config[key] !== "boolean")
|
|
249207
249472
|
throw new Error(`${key} must be a boolean.`);
|
|
249208
249473
|
}
|
|
249474
|
+
if (config.effect !== undefined) {
|
|
249475
|
+
if (!isRecord(config.effect) || Object.keys(config.effect).some((key) => !["requireRouteEffects", "requireErrorMappings", "requireDependencies"].includes(key))) {
|
|
249476
|
+
throw new Error("effect accepts requireRouteEffects, requireErrorMappings and requireDependencies.");
|
|
249477
|
+
}
|
|
249478
|
+
for (const key of ["requireRouteEffects", "requireErrorMappings", "requireDependencies"]) {
|
|
249479
|
+
if (config.effect[key] !== undefined && typeof config.effect[key] !== "boolean") {
|
|
249480
|
+
throw new Error(`effect.${key} must be boolean.`);
|
|
249481
|
+
}
|
|
249482
|
+
}
|
|
249483
|
+
}
|
|
249209
249484
|
}
|
|
249210
249485
|
function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
|
|
249211
249486
|
const resolved = defineSupacloudConfig(config);
|
|
@@ -249226,6 +249501,7 @@ function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
|
|
|
249226
249501
|
...resolved.allowRouteCommandBindings === undefined ? {} : { allowRouteCommandBindings: resolved.allowRouteCommandBindings },
|
|
249227
249502
|
...resolved.disallowControllerDirectDb === undefined ? {} : { disallowControllerDirectDb: resolved.disallowControllerDirectDb },
|
|
249228
249503
|
...resolved.detectOrphanModules === undefined ? {} : { detectOrphanModules: resolved.detectOrphanModules },
|
|
249504
|
+
effect: resolved.effect === undefined ? { ...DEFAULT_SUPACLOUD_CONFIG.effect } : { ...DEFAULT_SUPACLOUD_CONFIG.effect, ...resolved.effect },
|
|
249229
249505
|
treeShakeUnusedProviders: resolved.treeShakeUnusedProviders ?? DEFAULT_SUPACLOUD_CONFIG.treeShakeUnusedProviders,
|
|
249230
249506
|
graphql: resolved.graphql ? {
|
|
249231
249507
|
...resolved.graphql,
|
|
@@ -249374,16 +249650,16 @@ var starter_metadata_default = {
|
|
|
249374
249650
|
generatedBy: "supacloud/starter-metadata-v1",
|
|
249375
249651
|
packages: {
|
|
249376
249652
|
compiler: {
|
|
249377
|
-
version: "0.
|
|
249653
|
+
version: "0.41.0"
|
|
249378
249654
|
},
|
|
249379
249655
|
app: {
|
|
249380
|
-
version: "0.
|
|
249656
|
+
version: "0.24.0",
|
|
249381
249657
|
dependencies: {
|
|
249382
249658
|
rxjs: "7.8.2"
|
|
249383
249659
|
}
|
|
249384
249660
|
},
|
|
249385
249661
|
elysia: {
|
|
249386
|
-
version: "0.
|
|
249662
|
+
version: "0.28.0"
|
|
249387
249663
|
},
|
|
249388
249664
|
commands: {
|
|
249389
249665
|
version: "0.9.1"
|
|
@@ -249737,6 +250013,7 @@ function baseFiles(name, sdkDependencies) {
|
|
|
249737
250013
|
dependencies: {
|
|
249738
250014
|
"@supacloud/app": `^${appMetadata.version}`,
|
|
249739
250015
|
"@supacloud/elysia": `^${elysiaMetadata.version}`,
|
|
250016
|
+
effect: "3.22.1",
|
|
249740
250017
|
elysia: "2.0.0-beta.21",
|
|
249741
250018
|
...sdkDependencies ?? { rxjs: appMetadata.dependencies.rxjs }
|
|
249742
250019
|
},
|
|
@@ -249781,6 +250058,11 @@ export default defineSupacloudConfig({
|
|
|
249781
250058
|
outDir: "generated",
|
|
249782
250059
|
strict: true,
|
|
249783
250060
|
moduleBoundaryPreset: "modular-monolith",
|
|
250061
|
+
effect: {
|
|
250062
|
+
requireRouteEffects: true,
|
|
250063
|
+
requireErrorMappings: true,
|
|
250064
|
+
requireDependencies: true,
|
|
250065
|
+
},
|
|
249784
250066
|
typeSafety: { scanProductionSource: true, noAnyInGenerated: true },
|
|
249785
250067
|
disallowControllerDirectDb: true,
|
|
249786
250068
|
detectOrphanModules: true,
|
|
@@ -249809,20 +250091,34 @@ export const CreateOrderBody = t.Object({ name: t.String({ minLength: 1 }) });
|
|
|
249809
250091
|
export const OrderResult = t.Object({ id: t.String(), name: t.String() });
|
|
249810
250092
|
export const HealthResult = t.Object({ ok: t.Boolean() });
|
|
249811
250093
|
`,
|
|
249812
|
-
"src/orders/orders.ts": `import
|
|
250094
|
+
"src/orders/orders.ts": `import * as Effect from "effect/Effect";
|
|
250095
|
+
import { Body, Controller, Get, Param, Post, defineFeatureSlice } from "@supacloud/app";
|
|
249813
250096
|
import { status } from "elysia";
|
|
249814
250097
|
import { OrderParams, CreateOrderBody, OrderResult, HealthResult } from "./contracts";
|
|
249815
250098
|
@Controller("/orders")
|
|
249816
250099
|
export class OrdersController {
|
|
249817
|
-
@Get("/health", {
|
|
249818
|
-
|
|
249819
|
-
|
|
249820
|
-
|
|
249821
|
-
|
|
249822
|
-
|
|
249823
|
-
@
|
|
250100
|
+
@Get("/health", {
|
|
250101
|
+
responses: { 200: HealthResult },
|
|
250102
|
+
effect: { required: true, dependencies: [], errors: [], retry: "none" },
|
|
250103
|
+
})
|
|
250104
|
+
health(): Effect.Effect<{ ok: boolean }, never, never> { return Effect.succeed({ ok: true }); }
|
|
250105
|
+
|
|
250106
|
+
@Get("/:id", {
|
|
250107
|
+
params: OrderParams,
|
|
250108
|
+
responses: { 200: OrderResult },
|
|
250109
|
+
effect: { required: true, dependencies: [], errors: [], retry: "none" },
|
|
250110
|
+
})
|
|
250111
|
+
get(@Param("id") id: string): Effect.Effect<{ id: string; name: string }, never, never> {
|
|
250112
|
+
return Effect.succeed({ id, name: "demo" });
|
|
250113
|
+
}
|
|
250114
|
+
|
|
250115
|
+
@Post("/", {
|
|
250116
|
+
body: CreateOrderBody,
|
|
250117
|
+
responses: { 201: OrderResult },
|
|
250118
|
+
effect: { required: true, dependencies: [], errors: [], retry: "none" },
|
|
250119
|
+
})
|
|
249824
250120
|
create(@Body() body: { name: string }) {
|
|
249825
|
-
return status(201, { id: "demo", name: body.name });
|
|
250121
|
+
return Effect.succeed(status(201, { id: "demo", name: body.name }));
|
|
249826
250122
|
}
|
|
249827
250123
|
}
|
|
249828
250124
|
|
|
@@ -249996,22 +250292,27 @@ function minimalTemplate(name, sdkDependencies) {
|
|
|
249996
250292
|
"src/features/health/contracts.ts": `import { t } from "elysia";
|
|
249997
250293
|
export const HealthResult = t.Object({ ok: t.Boolean() });
|
|
249998
250294
|
`,
|
|
249999
|
-
"src/features/health/health.ts": `import
|
|
250295
|
+
"src/features/health/health.ts": `import * as Effect from "effect/Effect";
|
|
250296
|
+
import { Controller, Get, Module } from "@supacloud/app/core";
|
|
250000
250297
|
import { HealthResult } from "./contracts";
|
|
250001
250298
|
@Controller("/health")
|
|
250002
250299
|
export class HealthController {
|
|
250003
|
-
@Get("/", {
|
|
250004
|
-
|
|
250300
|
+
@Get("/", {
|
|
250301
|
+
responses: { 200: HealthResult },
|
|
250302
|
+
effect: { required: true, dependencies: [], errors: [], retry: "none" },
|
|
250303
|
+
})
|
|
250304
|
+
health(): Effect.Effect<{ ok: boolean }, never, never> { return Effect.succeed({ ok: true }); }
|
|
250005
250305
|
}
|
|
250006
250306
|
|
|
250007
250307
|
@Module({ name: "health", tags: ["type:feature"], controllers: [HealthController] })
|
|
250008
250308
|
export class HealthModule {}
|
|
250009
250309
|
`,
|
|
250010
|
-
"src/features/health/health.test.ts": `import
|
|
250310
|
+
"src/features/health/health.test.ts": `import * as Effect from "effect/Effect";
|
|
250311
|
+
import { expect, test } from "bun:test";
|
|
250011
250312
|
import { HealthController } from "./health";
|
|
250012
250313
|
|
|
250013
|
-
test("health is a public, side-effect-free operation", () => {
|
|
250014
|
-
expect(new HealthController().health()).toEqual({ ok: true });
|
|
250314
|
+
test("health is a public, side-effect-free operation", async () => {
|
|
250315
|
+
expect(await Effect.runPromise(new HealthController().health())).toEqual({ ok: true });
|
|
250015
250316
|
});
|
|
250016
250317
|
`,
|
|
250017
250318
|
"src/app.module.ts": `import { Module } from "@supacloud/app/core";
|
|
@@ -250536,7 +250837,8 @@ export async function createDeliveryWorker(modules: CompiledModule[], lifecycle:
|
|
|
250536
250837
|
`;
|
|
250537
250838
|
|
|
250538
250839
|
// src/shared/tools/app-starter-upload.ts
|
|
250539
|
-
var STARTER_UPLOAD_FEATURE = `import
|
|
250840
|
+
var STARTER_UPLOAD_FEATURE = `import * as Effect from "effect/Effect";
|
|
250841
|
+
import { Body, Controller, Inject, Injectable, Param, Post, DB_CLIENT, REQUEST_CONTEXT } from "@supacloud/app";
|
|
250540
250842
|
import { ApplicationError } from "@supacloud/elysia";
|
|
250541
250843
|
import { UploadBody, UploadLocation, UploadParams, UploadResult } from "./contracts";
|
|
250542
250844
|
|
|
@@ -250566,13 +250868,47 @@ export class ReviewUploads {
|
|
|
250566
250868
|
export class ReviewUploadsController {
|
|
250567
250869
|
constructor(@Inject(ReviewUploads) private readonly uploads: ReviewUploads) {}
|
|
250568
250870
|
|
|
250569
|
-
@Post("/:id/attachment-upload", {
|
|
250570
|
-
|
|
250571
|
-
|
|
250572
|
-
|
|
250573
|
-
|
|
250574
|
-
|
|
250575
|
-
|
|
250871
|
+
@Post("/:id/attachment-upload", {
|
|
250872
|
+
params: UploadParams,
|
|
250873
|
+
body: UploadBody,
|
|
250874
|
+
responses: { 200: UploadLocation },
|
|
250875
|
+
effect: {
|
|
250876
|
+
required: true,
|
|
250877
|
+
dependencies: [],
|
|
250878
|
+
errors: [
|
|
250879
|
+
{ tag: "AUTHENTICATION_REQUIRED", status: 401, code: "AUTHENTICATION_REQUIRED" },
|
|
250880
|
+
{ tag: "UPLOAD_INPUT_INVALID", status: 400, code: "UPLOAD_INPUT_INVALID" },
|
|
250881
|
+
{ tag: "UPLOAD_DENIED", status: 403, code: "UPLOAD_DENIED" },
|
|
250882
|
+
{ tag: "UPLOADS_UNAVAILABLE", status: 501, code: "UPLOADS_UNAVAILABLE" },
|
|
250883
|
+
],
|
|
250884
|
+
retry: "none",
|
|
250885
|
+
},
|
|
250886
|
+
})
|
|
250887
|
+
preview(@Param("id") id: string, @Body() body: { artifactId: string; expectedVersion: number }): Effect.Effect<ReviewUploadLocation, unknown, never> {
|
|
250888
|
+
return Effect.promise(() => this.uploads.preview({ reviewId: id, artifactId: body.artifactId, expectedVersion: body.expectedVersion }));
|
|
250889
|
+
}
|
|
250890
|
+
@Post("/:id/attachment-registration", {
|
|
250891
|
+
params: UploadParams,
|
|
250892
|
+
body: UploadBody,
|
|
250893
|
+
responses: { 200: UploadResult },
|
|
250894
|
+
effect: {
|
|
250895
|
+
required: true,
|
|
250896
|
+
dependencies: [],
|
|
250897
|
+
errors: [
|
|
250898
|
+
{ tag: "AUTHENTICATION_REQUIRED", status: 401, code: "AUTHENTICATION_REQUIRED" },
|
|
250899
|
+
{ tag: "UPLOAD_INPUT_INVALID", status: 400, code: "UPLOAD_INPUT_INVALID" },
|
|
250900
|
+
{ tag: "UPLOAD_DENIED", status: 403, code: "UPLOAD_DENIED" },
|
|
250901
|
+
{ tag: "UPLOAD_CONTENT_INVALID", status: 400, code: "UPLOAD_CONTENT_INVALID" },
|
|
250902
|
+
{ tag: "COMMAND_IDEMPOTENCY_CONFLICT", status: 409, code: "COMMAND_IDEMPOTENCY_CONFLICT" },
|
|
250903
|
+
{ tag: "COMMAND_REJECTED", status: 403, code: "COMMAND_REJECTED" },
|
|
250904
|
+
{ tag: "UPLOADS_UNAVAILABLE", status: 501, code: "UPLOADS_UNAVAILABLE" },
|
|
250905
|
+
{ tag: "UPLOAD_OUTCOME_UNKNOWN", status: 503, code: "UPLOAD_OUTCOME_UNKNOWN" },
|
|
250906
|
+
],
|
|
250907
|
+
retry: "none",
|
|
250908
|
+
},
|
|
250909
|
+
})
|
|
250910
|
+
register(@Param("id") id: string, @Body() body: { artifactId: string; expectedVersion: number }): Effect.Effect<ReviewUploadResult, unknown, never> {
|
|
250911
|
+
return Effect.promise(() => this.uploads.register({ reviewId: id, artifactId: body.artifactId, expectedVersion: body.expectedVersion }));
|
|
250576
250912
|
}
|
|
250577
250913
|
}
|
|
250578
250914
|
`;
|
|
@@ -250856,6 +251192,10 @@ function rows(value: unknown): Record<string, unknown>[] {
|
|
|
250856
251192
|
if (!Array.isArray(value)) throw new TypeError("Invalid database result");
|
|
250857
251193
|
return value.map(record);
|
|
250858
251194
|
}
|
|
251195
|
+
function isPublicConflict(value: unknown): boolean {
|
|
251196
|
+
return value instanceof ApplicationError && value.status === 409
|
|
251197
|
+
|| isRecord(value) && value.expose === true && value.status === 409 && typeof value.code === "string";
|
|
251198
|
+
}
|
|
250859
251199
|
function input(value: unknown): Approval {
|
|
250860
251200
|
const item = record(value);
|
|
250861
251201
|
if (typeof item.id !== "string" || !item.id.length || typeof item.expectedVersion !== "number"
|
|
@@ -250970,7 +251310,7 @@ export async function createReviewPostgresAdapters(options: ReviewPostgresOption
|
|
|
250970
251310
|
execute: tx => scope.run(tx, async () => {
|
|
250971
251311
|
try { return await next(); }
|
|
250972
251312
|
catch (error) {
|
|
250973
|
-
if (error
|
|
251313
|
+
if (isPublicConflict(error)) {
|
|
250974
251314
|
throw new CommandError("COMMAND_IDEMPOTENCY_CONFLICT");
|
|
250975
251315
|
}
|
|
250976
251316
|
throw error;
|
|
@@ -251279,6 +251619,7 @@ function appStarterFiles(name) {
|
|
|
251279
251619
|
"drizzle-orm": dbMetadata.peerDependencies["drizzle-orm"],
|
|
251280
251620
|
"@supacloud/js": `^${sdkMetadata.version}`,
|
|
251281
251621
|
"@supabase/supabase-js": sdkMetadata.peerDependencies["@supabase/supabase-js"],
|
|
251622
|
+
effect: "3.22.1",
|
|
251282
251623
|
elysia: "2.0.0-beta.21",
|
|
251283
251624
|
rxjs: appMetadata2.dependencies.rxjs
|
|
251284
251625
|
},
|
|
@@ -251334,6 +251675,11 @@ export default defineSupacloudConfig({
|
|
|
251334
251675
|
outDir: "generated",
|
|
251335
251676
|
strict: true,
|
|
251336
251677
|
moduleBoundaryPreset: "modular-monolith",
|
|
251678
|
+
effect: {
|
|
251679
|
+
requireRouteEffects: true,
|
|
251680
|
+
requireErrorMappings: true,
|
|
251681
|
+
requireDependencies: true,
|
|
251682
|
+
},
|
|
251337
251683
|
typeSafety: { scanProductionSource: true, noAnyInGenerated: true },
|
|
251338
251684
|
disallowControllerDirectDb: true,
|
|
251339
251685
|
detectOrphanModules: true,
|
|
@@ -251534,7 +251880,8 @@ export function createCompiledModules(): never {
|
|
|
251534
251880
|
throw new Error("Run bun run compile before starting the application");
|
|
251535
251881
|
}
|
|
251536
251882
|
`,
|
|
251537
|
-
"src/review/review.ts": `import
|
|
251883
|
+
"src/review/review.ts": `import * as Effect from "effect/Effect";
|
|
251884
|
+
import {
|
|
251538
251885
|
Body, Command, Controller, DB_CLIENT, Get, Inject, Param, Post,
|
|
251539
251886
|
defineFeatureSlice, defineFeatureSpec, type Aspect,
|
|
251540
251887
|
} from "@supacloud/app";
|
|
@@ -251604,12 +251951,36 @@ export class ApproveReview {
|
|
|
251604
251951
|
export class ReviewController {
|
|
251605
251952
|
constructor(@Inject(ApproveReview) private readonly approveReview: ApproveReview) {}
|
|
251606
251953
|
|
|
251607
|
-
@Get("/health", {
|
|
251608
|
-
|
|
251609
|
-
|
|
251610
|
-
|
|
251611
|
-
|
|
251612
|
-
|
|
251954
|
+
@Get("/health", {
|
|
251955
|
+
responses: { 200: HealthResult },
|
|
251956
|
+
effect: { required: true, dependencies: [], errors: [], retry: "none" },
|
|
251957
|
+
})
|
|
251958
|
+
health(): Effect.Effect<{ ok: boolean }, never, never> { return Effect.succeed({ ok: true }); }
|
|
251959
|
+
|
|
251960
|
+
@Post("/:id/approve", {
|
|
251961
|
+
command: ApproveReview,
|
|
251962
|
+
params: Params,
|
|
251963
|
+
body: ApproveBody,
|
|
251964
|
+
responses: { 200: ReviewResult },
|
|
251965
|
+
effect: {
|
|
251966
|
+
required: true,
|
|
251967
|
+
dependencies: [],
|
|
251968
|
+
errors: [
|
|
251969
|
+
{ tag: "AUTHENTICATION_REQUIRED", status: 401, code: "AUTHENTICATION_REQUIRED" },
|
|
251970
|
+
{ tag: "REVIEW_NOT_FOUND", status: 404, code: "REVIEW_NOT_FOUND" },
|
|
251971
|
+
{ tag: "REVIEW_PERMISSION_DENIED", status: 403, code: "REVIEW_PERMISSION_DENIED" },
|
|
251972
|
+
{ tag: "REVIEW_CONFLICT", status: 409, code: "REVIEW_CONFLICT" },
|
|
251973
|
+
{ tag: "FEATURE_TRANSITION_CONFLICT", status: 409, code: "FEATURE_TRANSITION_CONFLICT" },
|
|
251974
|
+
{ tag: "COMMAND_IDEMPOTENCY_CONFLICT", status: 409, code: "COMMAND_IDEMPOTENCY_CONFLICT" },
|
|
251975
|
+
{ tag: "COMMAND_REJECTED", status: 403, code: "COMMAND_REJECTED" },
|
|
251976
|
+
{ tag: "REVIEW_COMMAND_UNSUPPORTED", status: 501, code: "REVIEW_COMMAND_UNSUPPORTED" },
|
|
251977
|
+
{ tag: "COMMAND_OUTCOME_UNKNOWN", status: 503, code: "COMMAND_OUTCOME_UNKNOWN" },
|
|
251978
|
+
],
|
|
251979
|
+
retry: "none",
|
|
251980
|
+
},
|
|
251981
|
+
})
|
|
251982
|
+
approve(@Param("id") id: string, @Body() body: { expectedVersion: number }): Effect.Effect<Review, unknown, never> {
|
|
251983
|
+
return Effect.promise(() => this.approveReview.execute(id, body.expectedVersion));
|
|
251613
251984
|
}
|
|
251614
251985
|
}
|
|
251615
251986
|
|
|
@@ -252148,7 +252519,8 @@ export class ${className}Service {
|
|
|
252148
252519
|
}
|
|
252149
252520
|
}
|
|
252150
252521
|
`,
|
|
252151
|
-
[`${name}.controller.ts`]: `import
|
|
252522
|
+
[`${name}.controller.ts`]: `import * as Effect from "effect/Effect";
|
|
252523
|
+
import { Controller, Get, Inject, Param } from "@supacloud/app";
|
|
252152
252524
|
import { ${className}Params, ${className}Response, type ${className}Result } from "./${name}.model";
|
|
252153
252525
|
import { ${className}Service } from "./${name}.service";
|
|
252154
252526
|
|
|
@@ -252156,9 +252528,13 @@ import { ${className}Service } from "./${name}.service";
|
|
|
252156
252528
|
export class ${className}Controller {
|
|
252157
252529
|
constructor(@Inject(${className}Service) readonly service: Pick<${className}Service, "find">) {}
|
|
252158
252530
|
|
|
252159
|
-
@Get("/:id", {
|
|
252160
|
-
|
|
252161
|
-
|
|
252531
|
+
@Get("/:id", {
|
|
252532
|
+
params: ${className}Params,
|
|
252533
|
+
responses: { 200: ${className}Response },
|
|
252534
|
+
effect: { required: true, dependencies: [], errors: [], retry: "none" },
|
|
252535
|
+
})
|
|
252536
|
+
find(@Param("id") id: string): Effect.Effect<${className}Result, unknown, never> {
|
|
252537
|
+
return Effect.promise(() => this.service.find(id));
|
|
252162
252538
|
}
|
|
252163
252539
|
}
|
|
252164
252540
|
`,
|
|
@@ -252193,19 +252569,20 @@ test("${name} preserves permission denial without a privileged fallback", async
|
|
|
252193
252569
|
await expect(service.find("example")).rejects.toBe(denied);
|
|
252194
252570
|
});
|
|
252195
252571
|
`,
|
|
252196
|
-
[`${name}.controller.test.ts`]: `import
|
|
252572
|
+
[`${name}.controller.test.ts`]: `import * as Effect from "effect/Effect";
|
|
252573
|
+
import { expect, test } from "bun:test";
|
|
252197
252574
|
import { ${className}Controller } from "./${name}.controller";
|
|
252198
252575
|
|
|
252199
252576
|
test("${name} controller delegates to the supplied asynchronous read port", async () => {
|
|
252200
252577
|
// Unit-test double only; the generated runtime service remains fail-closed.
|
|
252201
252578
|
const controller = new ${className}Controller({ find: async (id) => ({ id }) });
|
|
252202
|
-
expect(await controller.find("example")).toEqual({ id: "example" });
|
|
252579
|
+
expect(await Effect.runPromise(controller.find("example"))).toEqual({ id: "example" });
|
|
252203
252580
|
});
|
|
252204
252581
|
|
|
252205
252582
|
test("${name} controller preserves an asynchronous read failure", async () => {
|
|
252206
252583
|
const failure = new Error("read port unavailable");
|
|
252207
252584
|
const controller = new ${className}Controller({ find: async () => { throw failure; } });
|
|
252208
|
-
await expect(controller.find("example")).rejects.
|
|
252585
|
+
await expect(Effect.runPromise(controller.find("example"))).rejects.toThrow("read port unavailable");
|
|
252209
252586
|
});
|
|
252210
252587
|
`
|
|
252211
252588
|
};
|
|
@@ -253729,6 +254106,91 @@ function formatReconcileReport(report) {
|
|
|
253729
254106
|
return lines.join(`
|
|
253730
254107
|
`);
|
|
253731
254108
|
}
|
|
254109
|
+
function moduleObjects2(module) {
|
|
254110
|
+
return [
|
|
254111
|
+
...module.tables.map((table) => ({
|
|
254112
|
+
kind: "table",
|
|
254113
|
+
name: table,
|
|
254114
|
+
module: module.name,
|
|
254115
|
+
source: null,
|
|
254116
|
+
tests: []
|
|
254117
|
+
})),
|
|
254118
|
+
...module.policies.map((policy) => ({
|
|
254119
|
+
kind: "policy",
|
|
254120
|
+
name: policy.name,
|
|
254121
|
+
module: module.name,
|
|
254122
|
+
table: policy.table,
|
|
254123
|
+
source: policy.source,
|
|
254124
|
+
tests: policy.tests ?? []
|
|
254125
|
+
})),
|
|
254126
|
+
...module.functions.map((fn) => ({
|
|
254127
|
+
kind: "function",
|
|
254128
|
+
name: fn.name,
|
|
254129
|
+
module: module.name,
|
|
254130
|
+
source: fn.source,
|
|
254131
|
+
tests: fn.tests ?? [],
|
|
254132
|
+
security: fn.security,
|
|
254133
|
+
permission: fn.permission,
|
|
254134
|
+
transaction: fn.transaction,
|
|
254135
|
+
audit: fn.audit,
|
|
254136
|
+
idempotency: fn.idempotency
|
|
254137
|
+
})),
|
|
254138
|
+
...module.triggers.map((trigger) => ({
|
|
254139
|
+
kind: "trigger",
|
|
254140
|
+
name: trigger.name,
|
|
254141
|
+
module: module.name,
|
|
254142
|
+
table: trigger.table,
|
|
254143
|
+
source: trigger.source,
|
|
254144
|
+
tests: []
|
|
254145
|
+
})),
|
|
254146
|
+
...module.grants.map((grant) => ({
|
|
254147
|
+
kind: "grant",
|
|
254148
|
+
name: `${grant.object}:${grant.privilege}:${grant.role}`,
|
|
254149
|
+
module: module.name,
|
|
254150
|
+
source: grant.source,
|
|
254151
|
+
tests: []
|
|
254152
|
+
}))
|
|
254153
|
+
];
|
|
254154
|
+
}
|
|
254155
|
+
function objectMatchesTarget(object, target) {
|
|
254156
|
+
return object.name === target || object.table === target || object.kind === "policy" && `${object.table}.${object.name}` === target || object.kind === "grant" && object.name.startsWith(`${target}:`);
|
|
254157
|
+
}
|
|
254158
|
+
function buildDatabaseAiContext(modules, root, moduleName, target) {
|
|
254159
|
+
const selected = moduleName ? modules.filter((module) => module.name === moduleName) : [...modules];
|
|
254160
|
+
if (moduleName && selected.length === 0) {
|
|
254161
|
+
throw new Error(`未找到数据库模块: ${moduleName}(可用: ${modules.map((module) => module.name).join(", ") || "none"})`);
|
|
254162
|
+
}
|
|
254163
|
+
const allObjects = selected.flatMap(moduleObjects2);
|
|
254164
|
+
const objects = target ? allObjects.filter((object) => objectMatchesTarget(object, target)) : allObjects;
|
|
254165
|
+
if (target && objects.length === 0)
|
|
254166
|
+
throw new Error(`未找到数据库对象: ${target}`);
|
|
254167
|
+
return {
|
|
254168
|
+
version: 1,
|
|
254169
|
+
scope: "database-ai-context",
|
|
254170
|
+
root,
|
|
254171
|
+
selectedModules: selected.map((module) => module.name),
|
|
254172
|
+
target: target ?? null,
|
|
254173
|
+
objects,
|
|
254174
|
+
maintenance: {
|
|
254175
|
+
structureSource: "drizzle",
|
|
254176
|
+
behaviorSource: "sql",
|
|
254177
|
+
generatedPaths: ["generated/", "bootstrap/schema.sql", "output/database-audit/"],
|
|
254178
|
+
protectedPaths: ["migrations/", "supabase/migrations/", "generated/"],
|
|
254179
|
+
forwardMigrationPath: "migrations/<timestamp>_<intent>.sql",
|
|
254180
|
+
rules: [
|
|
254181
|
+
"Edit the selected maintained source, not a generated snapshot.",
|
|
254182
|
+
"Keep RPC, RLS, trigger and grant behavior in explicit SQL sources.",
|
|
254183
|
+
"Create a new forward migration; never rewrite an applied migration.",
|
|
254184
|
+
"Run lint, focused behavior tests and catalog reconciliation after edits."
|
|
254185
|
+
]
|
|
254186
|
+
},
|
|
254187
|
+
verification: [
|
|
254188
|
+
"supacloud-cli db lint --module_file db/modules.ts",
|
|
254189
|
+
"supacloud-cli db explain --target <object>",
|
|
254190
|
+
'supacloud-cli db module_check --module_file db/modules.ts --database_url "$DATABASE_URL"'
|
|
254191
|
+
]
|
|
254192
|
+
};
|
|
254193
|
+
}
|
|
253732
254194
|
async function runModuleCheck(module, executor, schemas = ["public"]) {
|
|
253733
254195
|
const catalog = await readCatalog(executor, schemas);
|
|
253734
254196
|
return reconcileModule(module, catalog);
|
|
@@ -253772,6 +254234,10 @@ async function runExplain2(args, root) {
|
|
|
253772
254234
|
const text = explainObject(manifest, target);
|
|
253773
254235
|
return textResult2(text, text.startsWith("未找到对象"));
|
|
253774
254236
|
}
|
|
254237
|
+
async function runContext2(args, root) {
|
|
254238
|
+
const modules = await loadDatabaseModules(root, args.module_file);
|
|
254239
|
+
return textResult2(JSON.stringify(buildDatabaseAiContext(modules, root, args.module, args.target), null, 2));
|
|
254240
|
+
}
|
|
253775
254241
|
var defaultLiteSpawn = async (command, cwd) => {
|
|
253776
254242
|
const proc = Bun.spawn({ cmd: command, cwd, stdout: "pipe", stderr: "pipe" });
|
|
253777
254243
|
const [exitCode, stdout, stderr] = await Promise.all([
|
|
@@ -253829,12 +254295,12 @@ function registerDbGovernanceTools(server2, options = {}) {
|
|
|
253829
254295
|
const environment = options.environment || process.env;
|
|
253830
254296
|
const fallbackRoot = options.currentWorkingDirectory || process.cwd();
|
|
253831
254297
|
const liteSpawn = options.liteSpawn || defaultLiteSpawn;
|
|
253832
|
-
server2.tool("db", "Local database governance (@supacloud/db): lint declared modules, explain objects, reconcile against a live catalog or a local SupaCloud Lite project. Actions: lint, explain, module_check", {
|
|
253833
|
-
action: withDescription(stringEnum(["lint", "explain", "module_check"]), "Database governance action"),
|
|
253834
|
-
module: optional(String2(), "[lint] Only
|
|
254298
|
+
server2.tool("db", "Local database governance (@supacloud/db): provide bounded AI context, lint declared modules, explain objects, reconcile against a live catalog or a local SupaCloud Lite project. Actions: context, lint, explain, module_check", {
|
|
254299
|
+
action: withDescription(stringEnum(["context", "lint", "explain", "module_check"]), "Database governance action"),
|
|
254300
|
+
module: optional(String2(), "[context/lint] Only select this manifest module (default: all)"),
|
|
253835
254301
|
root: optional(String2(), "[*] Project root (default: current directory)"),
|
|
253836
254302
|
module_file: optional(String2(), "[*] File exporting defineDatabaseModule(...) (default: <root>/db/modules.ts)"),
|
|
253837
|
-
target: optional(String2(), "[explain] Policy / function / table name to explain"),
|
|
254303
|
+
target: optional(String2(), "[context/explain] Policy / function / table name to explain"),
|
|
253838
254304
|
database_url: optional(String2(), "[module_check] Postgres connection URL (default: DATABASE_URL)"),
|
|
253839
254305
|
schema: optional(String2(), "[module_check] Comma-separated schemas to inspect (default: public)"),
|
|
253840
254306
|
lite: optional(Boolean2(), "[module_check] Reconcile against a local SupaCloud Lite project (runs supacloud-lite db check)"),
|
|
@@ -253842,6 +254308,8 @@ function registerDbGovernanceTools(server2, options = {}) {
|
|
|
253842
254308
|
}, async (request) => {
|
|
253843
254309
|
const root = resolve25(request.root || fallbackRoot);
|
|
253844
254310
|
switch (request.action) {
|
|
254311
|
+
case "context":
|
|
254312
|
+
return runContext2(request, root);
|
|
253845
254313
|
case "lint":
|
|
253846
254314
|
return runLint(request, root);
|
|
253847
254315
|
case "explain":
|
|
@@ -256655,7 +257123,7 @@ function registerRemoteDevTools(server2, options = {}) {
|
|
|
256655
257123
|
// package.json
|
|
256656
257124
|
var package_default = {
|
|
256657
257125
|
name: "@supacloud/cli",
|
|
256658
|
-
version: "0.
|
|
257126
|
+
version: "0.73.0",
|
|
256659
257127
|
description: "Project-scoped CLI for SupaCloud users",
|
|
256660
257128
|
type: "module",
|
|
256661
257129
|
main: "./dist/index.js",
|
|
@@ -256689,7 +257157,7 @@ var package_default = {
|
|
|
256689
257157
|
directory: "packages/cli"
|
|
256690
257158
|
},
|
|
256691
257159
|
dependencies: {
|
|
256692
|
-
"@supacloud/compiler": "^0.
|
|
257160
|
+
"@supacloud/compiler": "^0.41.0",
|
|
256693
257161
|
"@supacloud/delivery": "0.8.0",
|
|
256694
257162
|
"@supacloud/db": "^0.14.0",
|
|
256695
257163
|
fflate: "^0.8.3",
|
|
@@ -256700,8 +257168,9 @@ var package_default = {
|
|
|
256700
257168
|
"@graphql-tools/utils": "^12.0.3"
|
|
256701
257169
|
},
|
|
256702
257170
|
devDependencies: {
|
|
256703
|
-
"@supacloud/elysia": "^0.
|
|
257171
|
+
"@supacloud/elysia": "^0.24.1",
|
|
256704
257172
|
"@types/bun": "^1.4.2",
|
|
257173
|
+
effect: "3.22.1",
|
|
256705
257174
|
elysia: "2.0.0-beta.21",
|
|
256706
257175
|
typescript: "^7.0.2"
|
|
256707
257176
|
}
|
|
@@ -256984,6 +257453,8 @@ EXAMPLES
|
|
|
256984
257453
|
${preferredCommand} context --format json
|
|
256985
257454
|
${preferredCommand} context --target CaseModule --format json
|
|
256986
257455
|
${preferredCommand} doctor
|
|
257456
|
+
${preferredCommand} db context --root . --module_file db/modules.ts --module cases
|
|
257457
|
+
${preferredCommand} db context --root . --module_file db/modules.ts --target public.case_create
|
|
256987
257458
|
${preferredCommand} db lint --root . --module_file db/modules.ts
|
|
256988
257459
|
${preferredCommand} db explain --target public.cases --module_file db/modules.ts
|
|
256989
257460
|
${preferredCommand} db module_check --module_file db/modules.ts --database_url "postgresql://..."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supacloud/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.73.0",
|
|
4
4
|
"description": "Project-scoped CLI for SupaCloud users",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"directory": "packages/cli"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@supacloud/compiler": "^0.
|
|
37
|
+
"@supacloud/compiler": "^0.41.0",
|
|
38
38
|
"@supacloud/delivery": "0.8.0",
|
|
39
39
|
"@supacloud/db": "^0.14.0",
|
|
40
40
|
"fflate": "^0.8.3",
|
|
@@ -45,8 +45,9 @@
|
|
|
45
45
|
"@graphql-tools/utils": "^12.0.3"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@supacloud/elysia": "^0.
|
|
48
|
+
"@supacloud/elysia": "^0.24.1",
|
|
49
49
|
"@types/bun": "^1.4.2",
|
|
50
|
+
"effect": "3.22.1",
|
|
50
51
|
"elysia": "2.0.0-beta.21",
|
|
51
52
|
"typescript": "^7.0.2"
|
|
52
53
|
}
|
|
@@ -31,12 +31,21 @@ Use `supacloud-cli` as the project-level control surface and keep durable change
|
|
|
31
31
|
|
|
32
32
|
1. Classify the request using [references/command-map.md](references/command-map.md).
|
|
33
33
|
2. For database work, load [references/database-workflow.md](references/database-workflow.md) and choose either new-change or historical-drift reconciliation.
|
|
34
|
-
3.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
3. For AI-assisted database changes, first request bounded context:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
supacloud-cli db context --module_file db/modules.ts --module <module>
|
|
38
|
+
supacloud-cli db context --module_file db/modules.ts --target <object>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Use the returned source/test paths and ownership boundary; do not scan or
|
|
42
|
+
edit the complete schema dump.
|
|
43
|
+
4. Prefer the smallest read-only inspection that proves current state.
|
|
44
|
+
5. Create or update version-controlled artifacts before remote writes.
|
|
45
|
+
6. Verify locally, inspect the diff, and run the narrowest relevant tests.
|
|
46
|
+
7. Preview remote changes with `--dry_run`.
|
|
47
|
+
8. Apply only within the user-authorized environment and scope.
|
|
48
|
+
9. Read back migration history and affected resources; report exact evidence and any remaining drift.
|
|
40
49
|
|
|
41
50
|
## Project endpoint inspection
|
|
42
51
|
|