@supacloud/cli 0.72.0 → 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/dist/index.js +529 -55
- package/package.json +3 -2
- package/skills/supacloud-cli/SKILL.md +15 -6
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) {
|
|
@@ -221498,6 +221498,17 @@ var init_validate = __esm2(() => {
|
|
|
221498
221498
|
"route-contract-required": { code: "SC3021", docsUrl: "https://supacloud.dev/errors/SC3021" },
|
|
221499
221499
|
"route-contract-unverified": { code: "SC3022", docsUrl: "https://supacloud.dev/errors/SC3022" },
|
|
221500
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" },
|
|
221501
221512
|
"invalid-route-response-map": { code: "SC3024", docsUrl: "https://supacloud.dev/errors/SC3024" },
|
|
221502
221513
|
"invalid-route-response-selector": { code: "SC3025", docsUrl: "https://supacloud.dev/errors/SC3025" },
|
|
221503
221514
|
"conflicting-route-response-schema": { code: "SC3026", docsUrl: "https://supacloud.dev/errors/SC3026" },
|
|
@@ -222026,6 +222037,9 @@ class ModuleGenerator {
|
|
|
222026
222037
|
});
|
|
222027
222038
|
fields.push(`responses: { ${responseFields.join(", ")} }`);
|
|
222028
222039
|
}
|
|
222040
|
+
if (route.effect !== undefined) {
|
|
222041
|
+
fields.push(`effect: ${JSON.stringify(route.effect)}`);
|
|
222042
|
+
}
|
|
222029
222043
|
if (route.command)
|
|
222030
222044
|
fields.push(`command: ${JSON.stringify(route.command)}`);
|
|
222031
222045
|
if (route.guards && route.guards.length > 0) {
|
|
@@ -223838,6 +223852,14 @@ var INTERFACES = `export interface CompiledRoute {
|
|
|
223838
223852
|
cookie?: unknown;
|
|
223839
223853
|
response?: unknown;
|
|
223840
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
|
+
};
|
|
223841
223863
|
/** Compile-time ownership and transport classification for the route boundary. */
|
|
223842
223864
|
contract?: {
|
|
223843
223865
|
body?: "framework" | "domain";
|
|
@@ -246265,6 +246287,188 @@ function parseController(input, ctx) {
|
|
|
246265
246287
|
Object.assign(route.contract, { [field]: value });
|
|
246266
246288
|
}
|
|
246267
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
|
+
}
|
|
246268
246472
|
for (const field of ["body", "params", "query", "headers", "cookie", "response"]) {
|
|
246269
246473
|
const schemaExpr = getProp(optionsObject, field);
|
|
246270
246474
|
if (schemaExpr && ts7.isIdentifier(schemaExpr)) {
|
|
@@ -246848,6 +247052,13 @@ function stringLiteralProp(obj, name) {
|
|
|
246848
247052
|
const expr = getProp(obj, name);
|
|
246849
247053
|
return expr && ts7.isStringLiteral(expr) ? expr.text : undefined;
|
|
246850
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
|
+
}
|
|
246851
247062
|
function arrayProp(obj, name) {
|
|
246852
247063
|
const expr = getProp(obj, name);
|
|
246853
247064
|
return expr && ts7.isArrayLiteralExpression(expr) ? [...expr.elements] : [];
|
|
@@ -247489,6 +247700,55 @@ function validateClientSchemaBoundaries(graph, options) {
|
|
|
247489
247700
|
}
|
|
247490
247701
|
return diagnostics;
|
|
247491
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
|
+
}
|
|
247492
247752
|
function withDefaultGovernance(options) {
|
|
247493
247753
|
return options.commandCapabilities === undefined ? { ...options, commandCapabilities: {
|
|
247494
247754
|
requirePersistentAdapters: true,
|
|
@@ -247507,7 +247767,8 @@ async function compileProject(options) {
|
|
|
247507
247767
|
const diagnostics = [
|
|
247508
247768
|
...graph.diagnostics ?? [],
|
|
247509
247769
|
...validateGraph(graph, options),
|
|
247510
|
-
...validateClientSchemaBoundaries(graph, options)
|
|
247770
|
+
...validateClientSchemaBoundaries(graph, options),
|
|
247771
|
+
...validateEffectPolicies(graph, options.effect)
|
|
247511
247772
|
];
|
|
247512
247773
|
if (diagnostics.some((item) => item.code === "runtime-injection-disallowed")) {
|
|
247513
247774
|
return { diagnostics, graph, written: [] };
|
|
@@ -247571,7 +247832,8 @@ async function checkProject(options) {
|
|
|
247571
247832
|
const diagnostics = [
|
|
247572
247833
|
...graph.diagnostics ?? [],
|
|
247573
247834
|
...validateGraph(graph, options),
|
|
247574
|
-
...validateClientSchemaBoundaries(graph, options)
|
|
247835
|
+
...validateClientSchemaBoundaries(graph, options),
|
|
247836
|
+
...validateEffectPolicies(graph, options.effect)
|
|
247575
247837
|
];
|
|
247576
247838
|
if (diagnostics.some((item) => item.code === "runtime-injection-disallowed")) {
|
|
247577
247839
|
return { diagnostics, graph, upToDate: false, mismatches: ["Runtime DI must be migrated to constructor injection."] };
|
|
@@ -247653,7 +247915,7 @@ init_delivery_schema();
|
|
|
247653
247915
|
init_validate();
|
|
247654
247916
|
var compare2 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
247655
247917
|
var sorted = (values) => [...new Set(values)].sort(compare2);
|
|
247656
|
-
function
|
|
247918
|
+
function diagnostic2(error) {
|
|
247657
247919
|
return {
|
|
247658
247920
|
severity: error.severity,
|
|
247659
247921
|
code: error.code,
|
|
@@ -247678,8 +247940,8 @@ function createDeliveryPlan(graph, input) {
|
|
|
247678
247940
|
}] };
|
|
247679
247941
|
}
|
|
247680
247942
|
const diagnostics = [
|
|
247681
|
-
...(graph.diagnostics ?? []).map(
|
|
247682
|
-
...validateGraph(graph, { strict: true }).map(
|
|
247943
|
+
...(graph.diagnostics ?? []).map(diagnostic2),
|
|
247944
|
+
...validateGraph(graph, { strict: true }).map(diagnostic2)
|
|
247683
247945
|
];
|
|
247684
247946
|
const fail = (code, message, suggestion) => {
|
|
247685
247947
|
diagnostics.push({ severity: "error", code, message, suggestion });
|
|
@@ -249134,7 +249396,12 @@ var DEFAULT_SUPACLOUD_CONFIG = {
|
|
|
249134
249396
|
idempotency: true,
|
|
249135
249397
|
transaction: true
|
|
249136
249398
|
},
|
|
249137
|
-
moduleBoundaryPreset: "modular-monolith"
|
|
249399
|
+
moduleBoundaryPreset: "modular-monolith",
|
|
249400
|
+
effect: {
|
|
249401
|
+
requireRouteEffects: true,
|
|
249402
|
+
requireErrorMappings: true,
|
|
249403
|
+
requireDependencies: true
|
|
249404
|
+
}
|
|
249138
249405
|
};
|
|
249139
249406
|
function defineSupacloudConfig(config = {}) {
|
|
249140
249407
|
if (config.delivery !== undefined)
|
|
@@ -249146,7 +249413,8 @@ function defineSupacloudConfig(config = {}) {
|
|
|
249146
249413
|
...DEFAULT_SUPACLOUD_CONFIG,
|
|
249147
249414
|
...config,
|
|
249148
249415
|
include: config.include ?? [...DEFAULT_SUPACLOUD_CONFIG.include],
|
|
249149
|
-
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 }
|
|
249150
249418
|
};
|
|
249151
249419
|
}
|
|
249152
249420
|
function validateGovernanceConfig(config) {
|
|
@@ -249203,6 +249471,16 @@ function validateGovernanceConfig(config) {
|
|
|
249203
249471
|
if (config[key] !== undefined && typeof config[key] !== "boolean")
|
|
249204
249472
|
throw new Error(`${key} must be a boolean.`);
|
|
249205
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
|
+
}
|
|
249206
249484
|
}
|
|
249207
249485
|
function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
|
|
249208
249486
|
const resolved = defineSupacloudConfig(config);
|
|
@@ -249223,6 +249501,7 @@ function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
|
|
|
249223
249501
|
...resolved.allowRouteCommandBindings === undefined ? {} : { allowRouteCommandBindings: resolved.allowRouteCommandBindings },
|
|
249224
249502
|
...resolved.disallowControllerDirectDb === undefined ? {} : { disallowControllerDirectDb: resolved.disallowControllerDirectDb },
|
|
249225
249503
|
...resolved.detectOrphanModules === undefined ? {} : { detectOrphanModules: resolved.detectOrphanModules },
|
|
249504
|
+
effect: resolved.effect === undefined ? { ...DEFAULT_SUPACLOUD_CONFIG.effect } : { ...DEFAULT_SUPACLOUD_CONFIG.effect, ...resolved.effect },
|
|
249226
249505
|
treeShakeUnusedProviders: resolved.treeShakeUnusedProviders ?? DEFAULT_SUPACLOUD_CONFIG.treeShakeUnusedProviders,
|
|
249227
249506
|
graphql: resolved.graphql ? {
|
|
249228
249507
|
...resolved.graphql,
|
|
@@ -249371,16 +249650,16 @@ var starter_metadata_default = {
|
|
|
249371
249650
|
generatedBy: "supacloud/starter-metadata-v1",
|
|
249372
249651
|
packages: {
|
|
249373
249652
|
compiler: {
|
|
249374
|
-
version: "0.
|
|
249653
|
+
version: "0.41.0"
|
|
249375
249654
|
},
|
|
249376
249655
|
app: {
|
|
249377
|
-
version: "0.
|
|
249656
|
+
version: "0.24.0",
|
|
249378
249657
|
dependencies: {
|
|
249379
249658
|
rxjs: "7.8.2"
|
|
249380
249659
|
}
|
|
249381
249660
|
},
|
|
249382
249661
|
elysia: {
|
|
249383
|
-
version: "0.
|
|
249662
|
+
version: "0.28.0"
|
|
249384
249663
|
},
|
|
249385
249664
|
commands: {
|
|
249386
249665
|
version: "0.9.1"
|
|
@@ -249734,6 +250013,7 @@ function baseFiles(name, sdkDependencies) {
|
|
|
249734
250013
|
dependencies: {
|
|
249735
250014
|
"@supacloud/app": `^${appMetadata.version}`,
|
|
249736
250015
|
"@supacloud/elysia": `^${elysiaMetadata.version}`,
|
|
250016
|
+
effect: "3.22.1",
|
|
249737
250017
|
elysia: "2.0.0-beta.21",
|
|
249738
250018
|
...sdkDependencies ?? { rxjs: appMetadata.dependencies.rxjs }
|
|
249739
250019
|
},
|
|
@@ -249778,6 +250058,11 @@ export default defineSupacloudConfig({
|
|
|
249778
250058
|
outDir: "generated",
|
|
249779
250059
|
strict: true,
|
|
249780
250060
|
moduleBoundaryPreset: "modular-monolith",
|
|
250061
|
+
effect: {
|
|
250062
|
+
requireRouteEffects: true,
|
|
250063
|
+
requireErrorMappings: true,
|
|
250064
|
+
requireDependencies: true,
|
|
250065
|
+
},
|
|
249781
250066
|
typeSafety: { scanProductionSource: true, noAnyInGenerated: true },
|
|
249782
250067
|
disallowControllerDirectDb: true,
|
|
249783
250068
|
detectOrphanModules: true,
|
|
@@ -249806,20 +250091,34 @@ export const CreateOrderBody = t.Object({ name: t.String({ minLength: 1 }) });
|
|
|
249806
250091
|
export const OrderResult = t.Object({ id: t.String(), name: t.String() });
|
|
249807
250092
|
export const HealthResult = t.Object({ ok: t.Boolean() });
|
|
249808
250093
|
`,
|
|
249809
|
-
"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";
|
|
249810
250096
|
import { status } from "elysia";
|
|
249811
250097
|
import { OrderParams, CreateOrderBody, OrderResult, HealthResult } from "./contracts";
|
|
249812
250098
|
@Controller("/orders")
|
|
249813
250099
|
export class OrdersController {
|
|
249814
|
-
@Get("/health", {
|
|
249815
|
-
|
|
249816
|
-
|
|
249817
|
-
|
|
249818
|
-
|
|
249819
|
-
|
|
249820
|
-
@
|
|
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
|
+
})
|
|
249821
250120
|
create(@Body() body: { name: string }) {
|
|
249822
|
-
return status(201, { id: "demo", name: body.name });
|
|
250121
|
+
return Effect.succeed(status(201, { id: "demo", name: body.name }));
|
|
249823
250122
|
}
|
|
249824
250123
|
}
|
|
249825
250124
|
|
|
@@ -249993,22 +250292,27 @@ function minimalTemplate(name, sdkDependencies) {
|
|
|
249993
250292
|
"src/features/health/contracts.ts": `import { t } from "elysia";
|
|
249994
250293
|
export const HealthResult = t.Object({ ok: t.Boolean() });
|
|
249995
250294
|
`,
|
|
249996
|
-
"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";
|
|
249997
250297
|
import { HealthResult } from "./contracts";
|
|
249998
250298
|
@Controller("/health")
|
|
249999
250299
|
export class HealthController {
|
|
250000
|
-
@Get("/", {
|
|
250001
|
-
|
|
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 }); }
|
|
250002
250305
|
}
|
|
250003
250306
|
|
|
250004
250307
|
@Module({ name: "health", tags: ["type:feature"], controllers: [HealthController] })
|
|
250005
250308
|
export class HealthModule {}
|
|
250006
250309
|
`,
|
|
250007
|
-
"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";
|
|
250008
250312
|
import { HealthController } from "./health";
|
|
250009
250313
|
|
|
250010
|
-
test("health is a public, side-effect-free operation", () => {
|
|
250011
|
-
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 });
|
|
250012
250316
|
});
|
|
250013
250317
|
`,
|
|
250014
250318
|
"src/app.module.ts": `import { Module } from "@supacloud/app/core";
|
|
@@ -250533,7 +250837,8 @@ export async function createDeliveryWorker(modules: CompiledModule[], lifecycle:
|
|
|
250533
250837
|
`;
|
|
250534
250838
|
|
|
250535
250839
|
// src/shared/tools/app-starter-upload.ts
|
|
250536
|
-
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";
|
|
250537
250842
|
import { ApplicationError } from "@supacloud/elysia";
|
|
250538
250843
|
import { UploadBody, UploadLocation, UploadParams, UploadResult } from "./contracts";
|
|
250539
250844
|
|
|
@@ -250563,13 +250868,47 @@ export class ReviewUploads {
|
|
|
250563
250868
|
export class ReviewUploadsController {
|
|
250564
250869
|
constructor(@Inject(ReviewUploads) private readonly uploads: ReviewUploads) {}
|
|
250565
250870
|
|
|
250566
|
-
@Post("/:id/attachment-upload", {
|
|
250567
|
-
|
|
250568
|
-
|
|
250569
|
-
|
|
250570
|
-
|
|
250571
|
-
|
|
250572
|
-
|
|
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 }));
|
|
250573
250912
|
}
|
|
250574
250913
|
}
|
|
250575
250914
|
`;
|
|
@@ -250853,6 +251192,10 @@ function rows(value: unknown): Record<string, unknown>[] {
|
|
|
250853
251192
|
if (!Array.isArray(value)) throw new TypeError("Invalid database result");
|
|
250854
251193
|
return value.map(record);
|
|
250855
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
|
+
}
|
|
250856
251199
|
function input(value: unknown): Approval {
|
|
250857
251200
|
const item = record(value);
|
|
250858
251201
|
if (typeof item.id !== "string" || !item.id.length || typeof item.expectedVersion !== "number"
|
|
@@ -250967,7 +251310,7 @@ export async function createReviewPostgresAdapters(options: ReviewPostgresOption
|
|
|
250967
251310
|
execute: tx => scope.run(tx, async () => {
|
|
250968
251311
|
try { return await next(); }
|
|
250969
251312
|
catch (error) {
|
|
250970
|
-
if (error
|
|
251313
|
+
if (isPublicConflict(error)) {
|
|
250971
251314
|
throw new CommandError("COMMAND_IDEMPOTENCY_CONFLICT");
|
|
250972
251315
|
}
|
|
250973
251316
|
throw error;
|
|
@@ -251276,6 +251619,7 @@ function appStarterFiles(name) {
|
|
|
251276
251619
|
"drizzle-orm": dbMetadata.peerDependencies["drizzle-orm"],
|
|
251277
251620
|
"@supacloud/js": `^${sdkMetadata.version}`,
|
|
251278
251621
|
"@supabase/supabase-js": sdkMetadata.peerDependencies["@supabase/supabase-js"],
|
|
251622
|
+
effect: "3.22.1",
|
|
251279
251623
|
elysia: "2.0.0-beta.21",
|
|
251280
251624
|
rxjs: appMetadata2.dependencies.rxjs
|
|
251281
251625
|
},
|
|
@@ -251331,6 +251675,11 @@ export default defineSupacloudConfig({
|
|
|
251331
251675
|
outDir: "generated",
|
|
251332
251676
|
strict: true,
|
|
251333
251677
|
moduleBoundaryPreset: "modular-monolith",
|
|
251678
|
+
effect: {
|
|
251679
|
+
requireRouteEffects: true,
|
|
251680
|
+
requireErrorMappings: true,
|
|
251681
|
+
requireDependencies: true,
|
|
251682
|
+
},
|
|
251334
251683
|
typeSafety: { scanProductionSource: true, noAnyInGenerated: true },
|
|
251335
251684
|
disallowControllerDirectDb: true,
|
|
251336
251685
|
detectOrphanModules: true,
|
|
@@ -251531,7 +251880,8 @@ export function createCompiledModules(): never {
|
|
|
251531
251880
|
throw new Error("Run bun run compile before starting the application");
|
|
251532
251881
|
}
|
|
251533
251882
|
`,
|
|
251534
|
-
"src/review/review.ts": `import
|
|
251883
|
+
"src/review/review.ts": `import * as Effect from "effect/Effect";
|
|
251884
|
+
import {
|
|
251535
251885
|
Body, Command, Controller, DB_CLIENT, Get, Inject, Param, Post,
|
|
251536
251886
|
defineFeatureSlice, defineFeatureSpec, type Aspect,
|
|
251537
251887
|
} from "@supacloud/app";
|
|
@@ -251601,12 +251951,36 @@ export class ApproveReview {
|
|
|
251601
251951
|
export class ReviewController {
|
|
251602
251952
|
constructor(@Inject(ApproveReview) private readonly approveReview: ApproveReview) {}
|
|
251603
251953
|
|
|
251604
|
-
@Get("/health", {
|
|
251605
|
-
|
|
251606
|
-
|
|
251607
|
-
|
|
251608
|
-
|
|
251609
|
-
|
|
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));
|
|
251610
251984
|
}
|
|
251611
251985
|
}
|
|
251612
251986
|
|
|
@@ -252145,7 +252519,8 @@ export class ${className}Service {
|
|
|
252145
252519
|
}
|
|
252146
252520
|
}
|
|
252147
252521
|
`,
|
|
252148
|
-
[`${name}.controller.ts`]: `import
|
|
252522
|
+
[`${name}.controller.ts`]: `import * as Effect from "effect/Effect";
|
|
252523
|
+
import { Controller, Get, Inject, Param } from "@supacloud/app";
|
|
252149
252524
|
import { ${className}Params, ${className}Response, type ${className}Result } from "./${name}.model";
|
|
252150
252525
|
import { ${className}Service } from "./${name}.service";
|
|
252151
252526
|
|
|
@@ -252153,9 +252528,13 @@ import { ${className}Service } from "./${name}.service";
|
|
|
252153
252528
|
export class ${className}Controller {
|
|
252154
252529
|
constructor(@Inject(${className}Service) readonly service: Pick<${className}Service, "find">) {}
|
|
252155
252530
|
|
|
252156
|
-
@Get("/:id", {
|
|
252157
|
-
|
|
252158
|
-
|
|
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));
|
|
252159
252538
|
}
|
|
252160
252539
|
}
|
|
252161
252540
|
`,
|
|
@@ -252190,19 +252569,20 @@ test("${name} preserves permission denial without a privileged fallback", async
|
|
|
252190
252569
|
await expect(service.find("example")).rejects.toBe(denied);
|
|
252191
252570
|
});
|
|
252192
252571
|
`,
|
|
252193
|
-
[`${name}.controller.test.ts`]: `import
|
|
252572
|
+
[`${name}.controller.test.ts`]: `import * as Effect from "effect/Effect";
|
|
252573
|
+
import { expect, test } from "bun:test";
|
|
252194
252574
|
import { ${className}Controller } from "./${name}.controller";
|
|
252195
252575
|
|
|
252196
252576
|
test("${name} controller delegates to the supplied asynchronous read port", async () => {
|
|
252197
252577
|
// Unit-test double only; the generated runtime service remains fail-closed.
|
|
252198
252578
|
const controller = new ${className}Controller({ find: async (id) => ({ id }) });
|
|
252199
|
-
expect(await controller.find("example")).toEqual({ id: "example" });
|
|
252579
|
+
expect(await Effect.runPromise(controller.find("example"))).toEqual({ id: "example" });
|
|
252200
252580
|
});
|
|
252201
252581
|
|
|
252202
252582
|
test("${name} controller preserves an asynchronous read failure", async () => {
|
|
252203
252583
|
const failure = new Error("read port unavailable");
|
|
252204
252584
|
const controller = new ${className}Controller({ find: async () => { throw failure; } });
|
|
252205
|
-
await expect(controller.find("example")).rejects.
|
|
252585
|
+
await expect(Effect.runPromise(controller.find("example"))).rejects.toThrow("read port unavailable");
|
|
252206
252586
|
});
|
|
252207
252587
|
`
|
|
252208
252588
|
};
|
|
@@ -253726,6 +254106,91 @@ function formatReconcileReport(report) {
|
|
|
253726
254106
|
return lines.join(`
|
|
253727
254107
|
`);
|
|
253728
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
|
+
}
|
|
253729
254194
|
async function runModuleCheck(module, executor, schemas = ["public"]) {
|
|
253730
254195
|
const catalog = await readCatalog(executor, schemas);
|
|
253731
254196
|
return reconcileModule(module, catalog);
|
|
@@ -253769,6 +254234,10 @@ async function runExplain2(args, root) {
|
|
|
253769
254234
|
const text = explainObject(manifest, target);
|
|
253770
254235
|
return textResult2(text, text.startsWith("未找到对象"));
|
|
253771
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
|
+
}
|
|
253772
254241
|
var defaultLiteSpawn = async (command, cwd) => {
|
|
253773
254242
|
const proc = Bun.spawn({ cmd: command, cwd, stdout: "pipe", stderr: "pipe" });
|
|
253774
254243
|
const [exitCode, stdout, stderr] = await Promise.all([
|
|
@@ -253826,12 +254295,12 @@ function registerDbGovernanceTools(server2, options = {}) {
|
|
|
253826
254295
|
const environment = options.environment || process.env;
|
|
253827
254296
|
const fallbackRoot = options.currentWorkingDirectory || process.cwd();
|
|
253828
254297
|
const liteSpawn = options.liteSpawn || defaultLiteSpawn;
|
|
253829
|
-
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", {
|
|
253830
|
-
action: withDescription(stringEnum(["lint", "explain", "module_check"]), "Database governance action"),
|
|
253831
|
-
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)"),
|
|
253832
254301
|
root: optional(String2(), "[*] Project root (default: current directory)"),
|
|
253833
254302
|
module_file: optional(String2(), "[*] File exporting defineDatabaseModule(...) (default: <root>/db/modules.ts)"),
|
|
253834
|
-
target: optional(String2(), "[explain] Policy / function / table name to explain"),
|
|
254303
|
+
target: optional(String2(), "[context/explain] Policy / function / table name to explain"),
|
|
253835
254304
|
database_url: optional(String2(), "[module_check] Postgres connection URL (default: DATABASE_URL)"),
|
|
253836
254305
|
schema: optional(String2(), "[module_check] Comma-separated schemas to inspect (default: public)"),
|
|
253837
254306
|
lite: optional(Boolean2(), "[module_check] Reconcile against a local SupaCloud Lite project (runs supacloud-lite db check)"),
|
|
@@ -253839,6 +254308,8 @@ function registerDbGovernanceTools(server2, options = {}) {
|
|
|
253839
254308
|
}, async (request) => {
|
|
253840
254309
|
const root = resolve25(request.root || fallbackRoot);
|
|
253841
254310
|
switch (request.action) {
|
|
254311
|
+
case "context":
|
|
254312
|
+
return runContext2(request, root);
|
|
253842
254313
|
case "lint":
|
|
253843
254314
|
return runLint(request, root);
|
|
253844
254315
|
case "explain":
|
|
@@ -256652,7 +257123,7 @@ function registerRemoteDevTools(server2, options = {}) {
|
|
|
256652
257123
|
// package.json
|
|
256653
257124
|
var package_default = {
|
|
256654
257125
|
name: "@supacloud/cli",
|
|
256655
|
-
version: "0.
|
|
257126
|
+
version: "0.73.0",
|
|
256656
257127
|
description: "Project-scoped CLI for SupaCloud users",
|
|
256657
257128
|
type: "module",
|
|
256658
257129
|
main: "./dist/index.js",
|
|
@@ -256686,7 +257157,7 @@ var package_default = {
|
|
|
256686
257157
|
directory: "packages/cli"
|
|
256687
257158
|
},
|
|
256688
257159
|
dependencies: {
|
|
256689
|
-
"@supacloud/compiler": "^0.
|
|
257160
|
+
"@supacloud/compiler": "^0.41.0",
|
|
256690
257161
|
"@supacloud/delivery": "0.8.0",
|
|
256691
257162
|
"@supacloud/db": "^0.14.0",
|
|
256692
257163
|
fflate: "^0.8.3",
|
|
@@ -256699,6 +257170,7 @@ var package_default = {
|
|
|
256699
257170
|
devDependencies: {
|
|
256700
257171
|
"@supacloud/elysia": "^0.24.1",
|
|
256701
257172
|
"@types/bun": "^1.4.2",
|
|
257173
|
+
effect: "3.22.1",
|
|
256702
257174
|
elysia: "2.0.0-beta.21",
|
|
256703
257175
|
typescript: "^7.0.2"
|
|
256704
257176
|
}
|
|
@@ -256981,6 +257453,8 @@ EXAMPLES
|
|
|
256981
257453
|
${preferredCommand} context --format json
|
|
256982
257454
|
${preferredCommand} context --target CaseModule --format json
|
|
256983
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
|
|
256984
257458
|
${preferredCommand} db lint --root . --module_file db/modules.ts
|
|
256985
257459
|
${preferredCommand} db explain --target public.cases --module_file db/modules.ts
|
|
256986
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",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"devDependencies": {
|
|
48
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
|
|