@supacloud/cli 0.72.0 → 0.74.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 +805 -61
- 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,24 @@ 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" },
|
|
221512
|
+
"effect-error-type": { code: "SC3041", docsUrl: "https://supacloud.dev/errors/SC3041" },
|
|
221513
|
+
"effect-error-mapping-type": { code: "SC3042", docsUrl: "https://supacloud.dev/errors/SC3042" },
|
|
221514
|
+
"effect-dependency-mismatch": { code: "SC3043", docsUrl: "https://supacloud.dev/errors/SC3043" },
|
|
221515
|
+
"effect-timeout-required": { code: "SC3044", docsUrl: "https://supacloud.dev/errors/SC3044" },
|
|
221516
|
+
"effect-direct-runtime-execution": { code: "SC3045", docsUrl: "https://supacloud.dev/errors/SC3045" },
|
|
221517
|
+
"effect-direct-throw": { code: "SC3046", docsUrl: "https://supacloud.dev/errors/SC3046" },
|
|
221518
|
+
"effect-promise-return": { code: "SC3047", docsUrl: "https://supacloud.dev/errors/SC3047" },
|
|
221501
221519
|
"invalid-route-response-map": { code: "SC3024", docsUrl: "https://supacloud.dev/errors/SC3024" },
|
|
221502
221520
|
"invalid-route-response-selector": { code: "SC3025", docsUrl: "https://supacloud.dev/errors/SC3025" },
|
|
221503
221521
|
"conflicting-route-response-schema": { code: "SC3026", docsUrl: "https://supacloud.dev/errors/SC3026" },
|
|
@@ -222026,6 +222044,9 @@ class ModuleGenerator {
|
|
|
222026
222044
|
});
|
|
222027
222045
|
fields.push(`responses: { ${responseFields.join(", ")} }`);
|
|
222028
222046
|
}
|
|
222047
|
+
if (route.effect !== undefined) {
|
|
222048
|
+
fields.push(`effect: ${JSON.stringify(route.effect)}`);
|
|
222049
|
+
}
|
|
222029
222050
|
if (route.command)
|
|
222030
222051
|
fields.push(`command: ${JSON.stringify(route.command)}`);
|
|
222031
222052
|
if (route.guards && route.guards.length > 0) {
|
|
@@ -223838,6 +223859,14 @@ var INTERFACES = `export interface CompiledRoute {
|
|
|
223838
223859
|
cookie?: unknown;
|
|
223839
223860
|
response?: unknown;
|
|
223840
223861
|
responses?: Record<string | number, unknown>;
|
|
223862
|
+
effect?: {
|
|
223863
|
+
required: true;
|
|
223864
|
+
errors?: Array<{ tag: string; status: number; code: string; message?: string }>;
|
|
223865
|
+
dependencies?: string[];
|
|
223866
|
+
retry?: "none" | "explicit";
|
|
223867
|
+
maxAttempts?: number;
|
|
223868
|
+
timeoutMs?: number;
|
|
223869
|
+
};
|
|
223841
223870
|
/** Compile-time ownership and transport classification for the route boundary. */
|
|
223842
223871
|
contract?: {
|
|
223843
223872
|
body?: "framework" | "domain";
|
|
@@ -244728,6 +244757,13 @@ function withDiagnosticMetadata(diagnostic) {
|
|
|
244728
244757
|
const metadata = COMPILER_DIAGNOSTIC_CODES[diagnostic.code];
|
|
244729
244758
|
return metadata ? { ...diagnostic, errorCode: metadata.code, docsUrl: metadata.docsUrl } : diagnostic;
|
|
244730
244759
|
}
|
|
244760
|
+
var DEFAULT_EFFECT_ANALYSIS_OPTIONS = {
|
|
244761
|
+
requireTaggedErrorTypes: true,
|
|
244762
|
+
requireExactDependencyTypes: true,
|
|
244763
|
+
requireTimeoutForDependencies: true,
|
|
244764
|
+
forbidDirectRuntimeExecution: true,
|
|
244765
|
+
forbidDirectThrows: true
|
|
244766
|
+
};
|
|
244731
244767
|
function nodeText(node) {
|
|
244732
244768
|
return node.getText(node.getSourceFile());
|
|
244733
244769
|
}
|
|
@@ -244773,7 +244809,7 @@ function descendantsOfKind(root, predicate) {
|
|
|
244773
244809
|
visit(root);
|
|
244774
244810
|
return result;
|
|
244775
244811
|
}
|
|
244776
|
-
async function analyzeProject(rootDir, include, cache, changedPaths) {
|
|
244812
|
+
async function analyzeProject(rootDir, include, cache, changedPaths, effectOptions = {}) {
|
|
244777
244813
|
const session = cache?.programSession ?? createIncrementalProgramSession(rootDir);
|
|
244778
244814
|
if (cache)
|
|
244779
244815
|
cache.programSession = session;
|
|
@@ -244792,8 +244828,21 @@ async function analyzeProject(rootDir, include, cache, changedPaths) {
|
|
|
244792
244828
|
moduleHandlerFiles: new Map,
|
|
244793
244829
|
infraResources: new Map,
|
|
244794
244830
|
infraResourceClasses: new Map,
|
|
244795
|
-
diagnostics: []
|
|
244831
|
+
diagnostics: [],
|
|
244832
|
+
effectOptions: {
|
|
244833
|
+
...DEFAULT_EFFECT_ANALYSIS_OPTIONS,
|
|
244834
|
+
requireTaggedErrorTypes: effectOptions.requireTaggedErrorTypes ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.requireTaggedErrorTypes,
|
|
244835
|
+
requireExactDependencyTypes: effectOptions.requireExactDependencyTypes ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.requireExactDependencyTypes,
|
|
244836
|
+
requireTimeoutForDependencies: effectOptions.requireTimeoutForDependencies ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.requireTimeoutForDependencies,
|
|
244837
|
+
forbidDirectRuntimeExecution: effectOptions.forbidDirectRuntimeExecution ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.forbidDirectRuntimeExecution,
|
|
244838
|
+
forbidDirectThrows: effectOptions.forbidDirectThrows ?? DEFAULT_EFFECT_ANALYSIS_OPTIONS.forbidDirectThrows
|
|
244839
|
+
}
|
|
244796
244840
|
};
|
|
244841
|
+
if (ctx.effectOptions.forbidDirectRuntimeExecution) {
|
|
244842
|
+
for (const sourceFile of sourceFiles) {
|
|
244843
|
+
scanEffectRuntimeEscapes(sourceFile, ctx);
|
|
244844
|
+
}
|
|
244845
|
+
}
|
|
244797
244846
|
const nativeTraitFiles = new Map;
|
|
244798
244847
|
for (const diagnostic of session.getDiagnostics()) {
|
|
244799
244848
|
ctx.diagnostics.push(toCompilerDiagnostic(diagnostic, rootDir));
|
|
@@ -245969,11 +246018,150 @@ function declaredClassType(declaration, ctx) {
|
|
|
245969
246018
|
return symbol ? ctx.checker.getDeclaredTypeOfSymbol(symbol) : undefined;
|
|
245970
246019
|
}
|
|
245971
246020
|
function typeArgumentsOf(type, ctx) {
|
|
245972
|
-
|
|
246021
|
+
if (!isTypeReference(type))
|
|
246022
|
+
return [];
|
|
246023
|
+
const argumentsFromChecker = ctx.checker.getTypeArguments(type);
|
|
246024
|
+
if (argumentsFromChecker.length > 0)
|
|
246025
|
+
return argumentsFromChecker;
|
|
246026
|
+
const reference = type;
|
|
246027
|
+
if (reference.resolvedTypeArguments && reference.resolvedTypeArguments.length > 0) {
|
|
246028
|
+
return reference.resolvedTypeArguments;
|
|
246029
|
+
}
|
|
246030
|
+
return reference.aliasTypeArguments ?? [];
|
|
245973
246031
|
}
|
|
245974
246032
|
function isTypeReference(type) {
|
|
245975
246033
|
return "target" in type;
|
|
245976
246034
|
}
|
|
246035
|
+
function effectTypeArgumentsOf(type, ctx) {
|
|
246036
|
+
if (!type)
|
|
246037
|
+
return;
|
|
246038
|
+
const reference = isTypeReference(type) ? type : undefined;
|
|
246039
|
+
const target = reference?.target ?? type;
|
|
246040
|
+
const symbol = target.aliasSymbol ?? target.getSymbol();
|
|
246041
|
+
const text = ctx.checker.typeToString(type, undefined, ts7.TypeFormatFlags.NoTruncation);
|
|
246042
|
+
if (symbol?.getName() !== "Effect" && !/^(?:Effect\.)?Effect</.test(text))
|
|
246043
|
+
return;
|
|
246044
|
+
const args = typeArgumentsOf(type, ctx);
|
|
246045
|
+
return args.length >= 3 ? [args[0], args[1], args[2]] : undefined;
|
|
246046
|
+
}
|
|
246047
|
+
function effectTypeName(type, location, ctx) {
|
|
246048
|
+
return ctx.checker.typeToString(type, location, ts7.TypeFormatFlags.NoTruncation);
|
|
246049
|
+
}
|
|
246050
|
+
function stringLiteralTypeValue(type) {
|
|
246051
|
+
return (type.flags & ts7.TypeFlags.StringLiteral) !== 0 ? type.value : undefined;
|
|
246052
|
+
}
|
|
246053
|
+
function effectFailureTags(type, location, ctx) {
|
|
246054
|
+
if ((type.flags & ts7.TypeFlags.Never) !== 0)
|
|
246055
|
+
return { tags: [], invalid: false };
|
|
246056
|
+
if ((type.flags & (ts7.TypeFlags.Any | ts7.TypeFlags.Unknown)) !== 0) {
|
|
246057
|
+
return { tags: [], invalid: true };
|
|
246058
|
+
}
|
|
246059
|
+
if (type.isUnion()) {
|
|
246060
|
+
return type.types.reduce((result, part) => {
|
|
246061
|
+
const next = effectFailureTags(part, location, ctx);
|
|
246062
|
+
result.tags.push(...next.tags);
|
|
246063
|
+
result.invalid ||= next.invalid;
|
|
246064
|
+
return result;
|
|
246065
|
+
}, { tags: [], invalid: false });
|
|
246066
|
+
}
|
|
246067
|
+
const apparent = ctx.checker.getApparentType(type);
|
|
246068
|
+
for (const propertyName of ["_tag", "tag"]) {
|
|
246069
|
+
const property = ctx.checker.getPropertyOfType(apparent, propertyName);
|
|
246070
|
+
if (!property)
|
|
246071
|
+
continue;
|
|
246072
|
+
const propertyType = ctx.checker.getTypeOfSymbolAtLocation(property, location);
|
|
246073
|
+
const tag = stringLiteralTypeValue(propertyType);
|
|
246074
|
+
if (tag)
|
|
246075
|
+
return { tags: [tag], invalid: false };
|
|
246076
|
+
}
|
|
246077
|
+
return { tags: [], invalid: true };
|
|
246078
|
+
}
|
|
246079
|
+
function effectEnvironmentNames(type, location, ctx) {
|
|
246080
|
+
if ((type.flags & ts7.TypeFlags.Never) !== 0)
|
|
246081
|
+
return { names: [], invalid: false };
|
|
246082
|
+
if ((type.flags & (ts7.TypeFlags.Any | ts7.TypeFlags.Unknown)) !== 0) {
|
|
246083
|
+
return { names: [], invalid: true };
|
|
246084
|
+
}
|
|
246085
|
+
if (type.isUnion() || type.isIntersection()) {
|
|
246086
|
+
return type.types.reduce((result, part) => {
|
|
246087
|
+
const next = effectEnvironmentNames(part, location, ctx);
|
|
246088
|
+
result.names.push(...next.names);
|
|
246089
|
+
result.invalid ||= next.invalid;
|
|
246090
|
+
return result;
|
|
246091
|
+
}, { names: [], invalid: false });
|
|
246092
|
+
}
|
|
246093
|
+
const literal = stringLiteralTypeValue(type);
|
|
246094
|
+
if (literal)
|
|
246095
|
+
return { names: [literal], invalid: false };
|
|
246096
|
+
const symbolName = type.getSymbol()?.getName();
|
|
246097
|
+
if (symbolName && symbolName !== "__type" && symbolName !== "__object") {
|
|
246098
|
+
return { names: [symbolName], invalid: false };
|
|
246099
|
+
}
|
|
246100
|
+
const text = effectTypeName(type, location, ctx).trim();
|
|
246101
|
+
if (text && text !== "object")
|
|
246102
|
+
return { names: [text], invalid: false };
|
|
246103
|
+
return { names: [], invalid: true };
|
|
246104
|
+
}
|
|
246105
|
+
function sortedUnique(values) {
|
|
246106
|
+
return [...new Set(values)].sort((left, right) => left.localeCompare(right, "en"));
|
|
246107
|
+
}
|
|
246108
|
+
function pushEffectDiagnostic(ctx, code, file, line, message, suggestion) {
|
|
246109
|
+
const metadata = COMPILER_DIAGNOSTIC_CODES[code];
|
|
246110
|
+
ctx.diagnostics.push({
|
|
246111
|
+
severity: "error",
|
|
246112
|
+
code,
|
|
246113
|
+
...metadata ? { errorCode: metadata.code, docsUrl: metadata.docsUrl } : {},
|
|
246114
|
+
file,
|
|
246115
|
+
line,
|
|
246116
|
+
message,
|
|
246117
|
+
suggestion
|
|
246118
|
+
});
|
|
246119
|
+
}
|
|
246120
|
+
function scanEffectRuntimeEscapes(sourceFile, ctx) {
|
|
246121
|
+
const relativeFile = sourcePath(ctx.rootDir, sourceFile.fileName);
|
|
246122
|
+
if (/\.(?:test|spec)\.[cm]?[jt]sx?$/.test(relativeFile))
|
|
246123
|
+
return;
|
|
246124
|
+
const namespaceBindings = new Set;
|
|
246125
|
+
const runtimeBindings = new Set;
|
|
246126
|
+
for (const statement of sourceFile.statements) {
|
|
246127
|
+
if (!ts7.isImportDeclaration(statement) || !ts7.isStringLiteral(statement.moduleSpecifier))
|
|
246128
|
+
continue;
|
|
246129
|
+
const moduleName = statement.moduleSpecifier.text;
|
|
246130
|
+
if (moduleName !== "effect" && moduleName !== "effect/Effect" && moduleName !== "effect/Runtime")
|
|
246131
|
+
continue;
|
|
246132
|
+
const clause = statement.importClause;
|
|
246133
|
+
if (!clause)
|
|
246134
|
+
continue;
|
|
246135
|
+
if (clause.name)
|
|
246136
|
+
namespaceBindings.add(clause.name.text);
|
|
246137
|
+
if (clause.namedBindings && ts7.isNamespaceImport(clause.namedBindings)) {
|
|
246138
|
+
namespaceBindings.add(clause.namedBindings.name.text);
|
|
246139
|
+
} else if (clause.namedBindings && ts7.isNamedImports(clause.namedBindings)) {
|
|
246140
|
+
for (const element of clause.namedBindings.elements) {
|
|
246141
|
+
const imported = element.propertyName?.text ?? element.name.text;
|
|
246142
|
+
if (moduleName === "effect" && (imported === "Effect" || imported === "Runtime")) {
|
|
246143
|
+
namespaceBindings.add(element.name.text);
|
|
246144
|
+
}
|
|
246145
|
+
if (["runPromise", "runPromiseExit", "runSync", "runFork"].includes(imported)) {
|
|
246146
|
+
runtimeBindings.add(element.name.text);
|
|
246147
|
+
}
|
|
246148
|
+
}
|
|
246149
|
+
}
|
|
246150
|
+
}
|
|
246151
|
+
if (namespaceBindings.size === 0 && runtimeBindings.size === 0)
|
|
246152
|
+
return;
|
|
246153
|
+
for (const call of descendantsOfKind(sourceFile, ts7.isCallExpression)) {
|
|
246154
|
+
let runtimeName;
|
|
246155
|
+
if (ts7.isPropertyAccessExpression(call.expression) && namespaceBindings.has(call.expression.expression.getText(sourceFile)) && ["runPromise", "runPromiseExit", "runSync", "runFork"].includes(call.expression.name.text)) {
|
|
246156
|
+
runtimeName = call.expression.name.text;
|
|
246157
|
+
} else if (ts7.isIdentifier(call.expression) && runtimeBindings.has(call.expression.text)) {
|
|
246158
|
+
runtimeName = call.expression.text;
|
|
246159
|
+
}
|
|
246160
|
+
if (!runtimeName)
|
|
246161
|
+
continue;
|
|
246162
|
+
pushEffectDiagnostic(ctx, "effect-direct-runtime-execution", sourcePath(ctx.rootDir, sourceFile.fileName), lineOf2(call), `Direct ${runtimeName} execution escapes the Effect boundary.`, "Return the Effect program to the Elysia boundary; only the framework adapter may interpret it.");
|
|
246163
|
+
}
|
|
246164
|
+
}
|
|
245977
246165
|
function isUnknownOrAny(type) {
|
|
245978
246166
|
return (type.flags & (ts7.TypeFlags.Any | ts7.TypeFlags.Unknown)) !== 0;
|
|
245979
246167
|
}
|
|
@@ -246037,7 +246225,8 @@ function parseController(input, ctx) {
|
|
|
246037
246225
|
handler: propertyName(method.name)
|
|
246038
246226
|
};
|
|
246039
246227
|
const signature = ctx.checker.getSignatureFromDeclaration(method);
|
|
246040
|
-
const
|
|
246228
|
+
const returnType = signature?.getReturnType();
|
|
246229
|
+
const resultType = returnType && ctx.checker.typeToString(returnType);
|
|
246041
246230
|
if (resultType && /\bResponse\b/.test(resultType))
|
|
246042
246231
|
route.nativeResponse = true;
|
|
246043
246232
|
const pathParams = [];
|
|
@@ -246265,6 +246454,235 @@ function parseController(input, ctx) {
|
|
|
246265
246454
|
Object.assign(route.contract, { [field]: value });
|
|
246266
246455
|
}
|
|
246267
246456
|
}
|
|
246457
|
+
const effectExpr = getProp(optionsObject, "effect");
|
|
246458
|
+
if (effectExpr) {
|
|
246459
|
+
const effectObject = resolveStaticObjectLiteral(effectExpr, ctx);
|
|
246460
|
+
if (!effectObject) {
|
|
246461
|
+
ctx.diagnostics.push({
|
|
246462
|
+
severity: "error",
|
|
246463
|
+
code: "invalid-effect-contract",
|
|
246464
|
+
errorCode: "SC3030",
|
|
246465
|
+
docsUrl: "https://supacloud.dev/errors/SC3030",
|
|
246466
|
+
file,
|
|
246467
|
+
line: lineOf2(effectExpr),
|
|
246468
|
+
message: `Route ${route.handler} effect must be a statically resolvable object literal.`,
|
|
246469
|
+
suggestion: "Use effect: { required: true, errors: [...], dependencies: [...] }."
|
|
246470
|
+
});
|
|
246471
|
+
} else {
|
|
246472
|
+
const effect = { required: true };
|
|
246473
|
+
const requiredExpr = getProp(effectObject, "required");
|
|
246474
|
+
if (requiredExpr !== undefined && booleanProp(effectObject, "required") !== true) {
|
|
246475
|
+
ctx.diagnostics.push({
|
|
246476
|
+
severity: "error",
|
|
246477
|
+
code: "invalid-effect-contract",
|
|
246478
|
+
errorCode: "SC3030",
|
|
246479
|
+
docsUrl: "https://supacloud.dev/errors/SC3030",
|
|
246480
|
+
file,
|
|
246481
|
+
line: lineOf2(requiredExpr),
|
|
246482
|
+
message: `Route ${route.handler} effect.required must be the literal true.`
|
|
246483
|
+
});
|
|
246484
|
+
}
|
|
246485
|
+
const retry = stringLiteralProp(effectObject, "retry");
|
|
246486
|
+
if (retry !== undefined) {
|
|
246487
|
+
if (retry !== "none" && retry !== "explicit") {
|
|
246488
|
+
ctx.diagnostics.push({
|
|
246489
|
+
severity: "error",
|
|
246490
|
+
code: "invalid-effect-retry",
|
|
246491
|
+
errorCode: "SC3031",
|
|
246492
|
+
docsUrl: "https://supacloud.dev/errors/SC3031",
|
|
246493
|
+
file,
|
|
246494
|
+
line: lineOf2(getProp(effectObject, "retry")),
|
|
246495
|
+
message: `Route ${route.handler} effect.retry must be "none" or "explicit".`
|
|
246496
|
+
});
|
|
246497
|
+
} else {
|
|
246498
|
+
effect.retry = retry;
|
|
246499
|
+
}
|
|
246500
|
+
}
|
|
246501
|
+
const maxAttemptsExpr = getProp(effectObject, "maxAttempts");
|
|
246502
|
+
if (maxAttemptsExpr !== undefined) {
|
|
246503
|
+
const maxAttempts = numberLiteralProp(effectObject, "maxAttempts");
|
|
246504
|
+
if (maxAttempts === undefined || maxAttempts < 1 || !Number.isInteger(maxAttempts)) {
|
|
246505
|
+
ctx.diagnostics.push({
|
|
246506
|
+
severity: "error",
|
|
246507
|
+
code: "invalid-effect-retry",
|
|
246508
|
+
errorCode: "SC3031",
|
|
246509
|
+
docsUrl: "https://supacloud.dev/errors/SC3031",
|
|
246510
|
+
file,
|
|
246511
|
+
line: lineOf2(maxAttemptsExpr),
|
|
246512
|
+
message: `Route ${route.handler} effect.maxAttempts must be a positive integer.`
|
|
246513
|
+
});
|
|
246514
|
+
} else {
|
|
246515
|
+
effect.maxAttempts = maxAttempts;
|
|
246516
|
+
}
|
|
246517
|
+
}
|
|
246518
|
+
if (effect.retry === "explicit" && effect.maxAttempts === undefined) {
|
|
246519
|
+
ctx.diagnostics.push({
|
|
246520
|
+
severity: "error",
|
|
246521
|
+
code: "invalid-effect-retry",
|
|
246522
|
+
errorCode: "SC3031",
|
|
246523
|
+
docsUrl: "https://supacloud.dev/errors/SC3031",
|
|
246524
|
+
file,
|
|
246525
|
+
line: lineOf2(effectExpr),
|
|
246526
|
+
message: `Route ${route.handler} effect.maxAttempts is required when retry is "explicit".`
|
|
246527
|
+
});
|
|
246528
|
+
}
|
|
246529
|
+
const timeoutExpr = getProp(effectObject, "timeoutMs");
|
|
246530
|
+
if (timeoutExpr !== undefined) {
|
|
246531
|
+
const timeoutMs = numberLiteralProp(effectObject, "timeoutMs");
|
|
246532
|
+
if (timeoutMs === undefined || timeoutMs <= 0 || !Number.isInteger(timeoutMs)) {
|
|
246533
|
+
ctx.diagnostics.push({
|
|
246534
|
+
severity: "error",
|
|
246535
|
+
code: "invalid-effect-timeout",
|
|
246536
|
+
errorCode: "SC3032",
|
|
246537
|
+
docsUrl: "https://supacloud.dev/errors/SC3032",
|
|
246538
|
+
file,
|
|
246539
|
+
line: lineOf2(timeoutExpr),
|
|
246540
|
+
message: `Route ${route.handler} effect.timeoutMs must be a positive integer.`
|
|
246541
|
+
});
|
|
246542
|
+
} else {
|
|
246543
|
+
effect.timeoutMs = timeoutMs;
|
|
246544
|
+
}
|
|
246545
|
+
}
|
|
246546
|
+
const dependenciesExpr = getProp(effectObject, "dependencies");
|
|
246547
|
+
if (dependenciesExpr !== undefined) {
|
|
246548
|
+
if (!ts7.isArrayLiteralExpression(dependenciesExpr) || dependenciesExpr.elements.some((element) => !ts7.isStringLiteral(element) || element.text.trim().length === 0)) {
|
|
246549
|
+
ctx.diagnostics.push({
|
|
246550
|
+
severity: "error",
|
|
246551
|
+
code: "invalid-effect-dependencies",
|
|
246552
|
+
errorCode: "SC3033",
|
|
246553
|
+
docsUrl: "https://supacloud.dev/errors/SC3033",
|
|
246554
|
+
file,
|
|
246555
|
+
line: lineOf2(dependenciesExpr),
|
|
246556
|
+
message: `Route ${route.handler} effect.dependencies must be an explicit string array.`
|
|
246557
|
+
});
|
|
246558
|
+
} else {
|
|
246559
|
+
effect.dependencies = dependenciesExpr.elements.map((element) => element.text);
|
|
246560
|
+
if (new Set(effect.dependencies).size !== effect.dependencies.length) {
|
|
246561
|
+
ctx.diagnostics.push({
|
|
246562
|
+
severity: "error",
|
|
246563
|
+
code: "invalid-effect-dependencies",
|
|
246564
|
+
errorCode: "SC3033",
|
|
246565
|
+
docsUrl: "https://supacloud.dev/errors/SC3033",
|
|
246566
|
+
file,
|
|
246567
|
+
line: lineOf2(dependenciesExpr),
|
|
246568
|
+
message: `Route ${route.handler} effect.dependencies must not contain duplicate service names.`,
|
|
246569
|
+
suggestion: "List each logical Effect dependency exactly once."
|
|
246570
|
+
});
|
|
246571
|
+
}
|
|
246572
|
+
}
|
|
246573
|
+
}
|
|
246574
|
+
const errorsExpr = getProp(effectObject, "errors");
|
|
246575
|
+
if (errorsExpr !== undefined) {
|
|
246576
|
+
if (!ts7.isArrayLiteralExpression(errorsExpr)) {
|
|
246577
|
+
ctx.diagnostics.push({
|
|
246578
|
+
severity: "error",
|
|
246579
|
+
code: "invalid-effect-errors",
|
|
246580
|
+
errorCode: "SC3034",
|
|
246581
|
+
docsUrl: "https://supacloud.dev/errors/SC3034",
|
|
246582
|
+
file,
|
|
246583
|
+
line: lineOf2(errorsExpr),
|
|
246584
|
+
message: `Route ${route.handler} effect.errors must be an explicit object array.`
|
|
246585
|
+
});
|
|
246586
|
+
} else {
|
|
246587
|
+
const errors = [];
|
|
246588
|
+
for (const errorExpr of errorsExpr.elements) {
|
|
246589
|
+
if (!ts7.isObjectLiteralExpression(errorExpr)) {
|
|
246590
|
+
ctx.diagnostics.push({
|
|
246591
|
+
severity: "error",
|
|
246592
|
+
code: "invalid-effect-errors",
|
|
246593
|
+
errorCode: "SC3034",
|
|
246594
|
+
docsUrl: "https://supacloud.dev/errors/SC3034",
|
|
246595
|
+
file,
|
|
246596
|
+
line: lineOf2(errorExpr),
|
|
246597
|
+
message: `Route ${route.handler} effect.errors entries must be object literals.`
|
|
246598
|
+
});
|
|
246599
|
+
continue;
|
|
246600
|
+
}
|
|
246601
|
+
const tag = stringLiteralProp(errorExpr, "tag");
|
|
246602
|
+
const code = stringLiteralProp(errorExpr, "code");
|
|
246603
|
+
const status = numberLiteralProp(errorExpr, "status");
|
|
246604
|
+
const message = stringLiteralProp(errorExpr, "message");
|
|
246605
|
+
if (!tag || !code || status === undefined || !Number.isInteger(status) || status < 400 || status > 599) {
|
|
246606
|
+
ctx.diagnostics.push({
|
|
246607
|
+
severity: "error",
|
|
246608
|
+
code: "invalid-effect-errors",
|
|
246609
|
+
errorCode: "SC3034",
|
|
246610
|
+
docsUrl: "https://supacloud.dev/errors/SC3034",
|
|
246611
|
+
file,
|
|
246612
|
+
line: lineOf2(errorExpr),
|
|
246613
|
+
message: `Route ${route.handler} effect.errors entries require tag, code and an HTTP status from 400 to 599.`
|
|
246614
|
+
});
|
|
246615
|
+
continue;
|
|
246616
|
+
}
|
|
246617
|
+
errors.push({ tag, code, status, ...message === undefined ? {} : { message } });
|
|
246618
|
+
}
|
|
246619
|
+
effect.errors = errors;
|
|
246620
|
+
if (new Set(errors.map((entry) => entry.tag)).size !== errors.length) {
|
|
246621
|
+
ctx.diagnostics.push({
|
|
246622
|
+
severity: "error",
|
|
246623
|
+
code: "invalid-effect-errors",
|
|
246624
|
+
errorCode: "SC3034",
|
|
246625
|
+
docsUrl: "https://supacloud.dev/errors/SC3034",
|
|
246626
|
+
file,
|
|
246627
|
+
line: lineOf2(errorsExpr),
|
|
246628
|
+
message: `Route ${route.handler} effect.errors must not contain duplicate tags.`,
|
|
246629
|
+
suggestion: "Map each tagged failure exactly once."
|
|
246630
|
+
});
|
|
246631
|
+
}
|
|
246632
|
+
}
|
|
246633
|
+
}
|
|
246634
|
+
route.effect = effect;
|
|
246635
|
+
const effectArguments = effectTypeArgumentsOf(returnType, ctx);
|
|
246636
|
+
const promiseReturn = Boolean(resultType && /\bPromise(?:<|\s)/.test(resultType));
|
|
246637
|
+
if (promiseReturn) {
|
|
246638
|
+
pushEffectDiagnostic(ctx, "effect-promise-return", file, lineOf2(method), `Route ${route.handler} returns Promise instead of an Effect program.`, "Return Effect.Effect<Success, Failure, Requirements>; the Elysia boundary owns Promise interpretation.");
|
|
246639
|
+
} else if (!effectArguments) {
|
|
246640
|
+
ctx.diagnostics.push({
|
|
246641
|
+
severity: "error",
|
|
246642
|
+
code: "effect-return-type",
|
|
246643
|
+
errorCode: "SC3035",
|
|
246644
|
+
docsUrl: "https://supacloud.dev/errors/SC3035",
|
|
246645
|
+
file,
|
|
246646
|
+
line: lineOf2(method),
|
|
246647
|
+
message: `Route ${route.handler} declares an Effect contract but returns ${resultType ?? "an unresolved type"}.`,
|
|
246648
|
+
suggestion: "Return Effect.Effect<Success, Failure, Requirements> from the handler."
|
|
246649
|
+
});
|
|
246650
|
+
}
|
|
246651
|
+
if (effectArguments) {
|
|
246652
|
+
const [, failureType, environmentType] = effectArguments;
|
|
246653
|
+
const failure = effectFailureTags(failureType, method, ctx);
|
|
246654
|
+
if (ctx.effectOptions.requireTaggedErrorTypes && failure.invalid) {
|
|
246655
|
+
pushEffectDiagnostic(ctx, "effect-error-type", file, lineOf2(method), `Route ${route.handler} must use never or a tagged failure union in Effect's error channel, not ${effectTypeName(failureType, method, ctx)}.`, "Replace unknown/any with never or a union of objects carrying a literal _tag.");
|
|
246656
|
+
} else if (ctx.effectOptions.requireTaggedErrorTypes && effect.errors !== undefined && !failure.invalid) {
|
|
246657
|
+
const declaredTags = sortedUnique(effect.errors.map((entry) => entry.tag));
|
|
246658
|
+
const typedTags = sortedUnique(failure.tags);
|
|
246659
|
+
if (declaredTags.join("\x00") !== typedTags.join("\x00")) {
|
|
246660
|
+
pushEffectDiagnostic(ctx, "effect-error-mapping-type", file, lineOf2(method), `Route ${route.handler} maps [${declaredTags.join(", ")}], but its Effect failure type declares [${typedTags.join(", ")}].`, "Make effect.errors and the Effect error type the same tagged union; do not hide unmapped failures.");
|
|
246661
|
+
}
|
|
246662
|
+
}
|
|
246663
|
+
const environment = effectEnvironmentNames(environmentType, method, ctx);
|
|
246664
|
+
if (ctx.effectOptions.requireExactDependencyTypes && effect.dependencies !== undefined) {
|
|
246665
|
+
if (environment.invalid) {
|
|
246666
|
+
pushEffectDiagnostic(ctx, "effect-dependency-mismatch", file, lineOf2(method), `Route ${route.handler} uses an Effect environment that cannot be represented by its explicit dependency list.`, "Use named service types or never in the third Effect type parameter.");
|
|
246667
|
+
} else {
|
|
246668
|
+
const declaredDependencies = sortedUnique(effect.dependencies);
|
|
246669
|
+
const typedDependencies = sortedUnique(environment.names);
|
|
246670
|
+
if (declaredDependencies.join("\x00") !== typedDependencies.join("\x00")) {
|
|
246671
|
+
pushEffectDiagnostic(ctx, "effect-dependency-mismatch", file, lineOf2(method), `Route ${route.handler} declares dependencies [${declaredDependencies.join(", ")}], but its Effect environment is [${typedDependencies.join(", ")}].`, "Make effect.dependencies exactly match the third Effect type parameter.");
|
|
246672
|
+
}
|
|
246673
|
+
}
|
|
246674
|
+
}
|
|
246675
|
+
if (ctx.effectOptions.requireTimeoutForDependencies && (effect.dependencies?.length ?? 0) > 0 && effect.timeoutMs === undefined) {
|
|
246676
|
+
pushEffectDiagnostic(ctx, "effect-timeout-required", file, lineOf2(method), `Route ${route.handler} declares external Effect dependencies without a timeout.`, "Add a positive effect.timeoutMs so external I/O cannot occupy a request indefinitely.");
|
|
246677
|
+
}
|
|
246678
|
+
}
|
|
246679
|
+
if (ctx.effectOptions.forbidDirectThrows && method.body) {
|
|
246680
|
+
for (const throwStatement of descendantsOfKind(method.body, ts7.isThrowStatement)) {
|
|
246681
|
+
pushEffectDiagnostic(ctx, "effect-direct-throw", file, lineOf2(throwStatement), `Route ${route.handler} throws directly instead of representing failure in Effect.`, "Use Effect.fail with a tagged error value and map it in effect.errors.");
|
|
246682
|
+
}
|
|
246683
|
+
}
|
|
246684
|
+
}
|
|
246685
|
+
}
|
|
246268
246686
|
for (const field of ["body", "params", "query", "headers", "cookie", "response"]) {
|
|
246269
246687
|
const schemaExpr = getProp(optionsObject, field);
|
|
246270
246688
|
if (schemaExpr && ts7.isIdentifier(schemaExpr)) {
|
|
@@ -246848,6 +247266,13 @@ function stringLiteralProp(obj, name) {
|
|
|
246848
247266
|
const expr = getProp(obj, name);
|
|
246849
247267
|
return expr && ts7.isStringLiteral(expr) ? expr.text : undefined;
|
|
246850
247268
|
}
|
|
247269
|
+
function numberLiteralProp(obj, name) {
|
|
247270
|
+
const expr = getProp(obj, name);
|
|
247271
|
+
if (!expr || !ts7.isNumericLiteral(expr))
|
|
247272
|
+
return;
|
|
247273
|
+
const value = Number(expr.text);
|
|
247274
|
+
return Number.isFinite(value) ? value : undefined;
|
|
247275
|
+
}
|
|
246851
247276
|
function arrayProp(obj, name) {
|
|
246852
247277
|
const expr = getProp(obj, name);
|
|
246853
247278
|
return expr && ts7.isArrayLiteralExpression(expr) ? [...expr.elements] : [];
|
|
@@ -247489,6 +247914,55 @@ function validateClientSchemaBoundaries(graph, options) {
|
|
|
247489
247914
|
}
|
|
247490
247915
|
return diagnostics;
|
|
247491
247916
|
}
|
|
247917
|
+
init_validate();
|
|
247918
|
+
function diagnostic(code, message, file, suggestion) {
|
|
247919
|
+
const metadata = COMPILER_DIAGNOSTIC_CODES[code];
|
|
247920
|
+
return {
|
|
247921
|
+
severity: "error",
|
|
247922
|
+
code,
|
|
247923
|
+
message,
|
|
247924
|
+
...file === undefined ? {} : { file },
|
|
247925
|
+
...suggestion === undefined ? {} : { suggestion },
|
|
247926
|
+
...metadata ? { errorCode: metadata.code, docsUrl: metadata.docsUrl } : {}
|
|
247927
|
+
};
|
|
247928
|
+
}
|
|
247929
|
+
function routeLabel(route, controllerPath) {
|
|
247930
|
+
return `${route.method} ${controllerPath}${route.path}`;
|
|
247931
|
+
}
|
|
247932
|
+
function validateEffectPolicies(graph, options = {}) {
|
|
247933
|
+
const diagnostics = [];
|
|
247934
|
+
for (const module of graph.modules) {
|
|
247935
|
+
const commands = new Map(module.commands.map((command) => [command.className, command]));
|
|
247936
|
+
for (const controller of module.controllers) {
|
|
247937
|
+
for (const route of controller.routes) {
|
|
247938
|
+
const label = routeLabel(route, controller.path);
|
|
247939
|
+
if (options.requireRouteEffects && route.effect === undefined) {
|
|
247940
|
+
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.'));
|
|
247941
|
+
continue;
|
|
247942
|
+
}
|
|
247943
|
+
const effect = route.effect;
|
|
247944
|
+
if (effect === undefined)
|
|
247945
|
+
continue;
|
|
247946
|
+
if (options.requireErrorMappings && effect.errors === undefined) {
|
|
247947
|
+
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."));
|
|
247948
|
+
}
|
|
247949
|
+
if (options.requireDependencies && effect.dependencies === undefined) {
|
|
247950
|
+
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."));
|
|
247951
|
+
}
|
|
247952
|
+
if (effect.retry === "explicit" && effect.maxAttempts === undefined) {
|
|
247953
|
+
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."));
|
|
247954
|
+
}
|
|
247955
|
+
if (route.command && effect.retry === "explicit") {
|
|
247956
|
+
const command = commands.get(route.command);
|
|
247957
|
+
if (command && command.idempotency !== "required") {
|
|
247958
|
+
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".'));
|
|
247959
|
+
}
|
|
247960
|
+
}
|
|
247961
|
+
}
|
|
247962
|
+
}
|
|
247963
|
+
}
|
|
247964
|
+
return diagnostics;
|
|
247965
|
+
}
|
|
247492
247966
|
function withDefaultGovernance(options) {
|
|
247493
247967
|
return options.commandCapabilities === undefined ? { ...options, commandCapabilities: {
|
|
247494
247968
|
requirePersistentAdapters: true,
|
|
@@ -247503,11 +247977,12 @@ async function renderOptionalGraphql(options) {
|
|
|
247503
247977
|
}
|
|
247504
247978
|
async function compileProject(options) {
|
|
247505
247979
|
options = withDefaultGovernance(options);
|
|
247506
|
-
const graph = await analyzeProject(options.rootDir, options.include, options.cache, options.changedPaths);
|
|
247980
|
+
const graph = await analyzeProject(options.rootDir, options.include, options.cache, options.changedPaths, options.effect);
|
|
247507
247981
|
const diagnostics = [
|
|
247508
247982
|
...graph.diagnostics ?? [],
|
|
247509
247983
|
...validateGraph(graph, options),
|
|
247510
|
-
...validateClientSchemaBoundaries(graph, options)
|
|
247984
|
+
...validateClientSchemaBoundaries(graph, options),
|
|
247985
|
+
...validateEffectPolicies(graph, options.effect)
|
|
247511
247986
|
];
|
|
247512
247987
|
if (diagnostics.some((item) => item.code === "runtime-injection-disallowed")) {
|
|
247513
247988
|
return { diagnostics, graph, written: [] };
|
|
@@ -247567,11 +248042,12 @@ async function compileProject(options) {
|
|
|
247567
248042
|
}
|
|
247568
248043
|
async function checkProject(options) {
|
|
247569
248044
|
options = withDefaultGovernance(options);
|
|
247570
|
-
const graph = await analyzeProject(options.rootDir, options.include, options.cache, options.changedPaths);
|
|
248045
|
+
const graph = await analyzeProject(options.rootDir, options.include, options.cache, options.changedPaths, options.effect);
|
|
247571
248046
|
const diagnostics = [
|
|
247572
248047
|
...graph.diagnostics ?? [],
|
|
247573
248048
|
...validateGraph(graph, options),
|
|
247574
|
-
...validateClientSchemaBoundaries(graph, options)
|
|
248049
|
+
...validateClientSchemaBoundaries(graph, options),
|
|
248050
|
+
...validateEffectPolicies(graph, options.effect)
|
|
247575
248051
|
];
|
|
247576
248052
|
if (diagnostics.some((item) => item.code === "runtime-injection-disallowed")) {
|
|
247577
248053
|
return { diagnostics, graph, upToDate: false, mismatches: ["Runtime DI must be migrated to constructor injection."] };
|
|
@@ -247653,7 +248129,7 @@ init_delivery_schema();
|
|
|
247653
248129
|
init_validate();
|
|
247654
248130
|
var compare2 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
247655
248131
|
var sorted = (values) => [...new Set(values)].sort(compare2);
|
|
247656
|
-
function
|
|
248132
|
+
function diagnostic2(error) {
|
|
247657
248133
|
return {
|
|
247658
248134
|
severity: error.severity,
|
|
247659
248135
|
code: error.code,
|
|
@@ -247678,8 +248154,8 @@ function createDeliveryPlan(graph, input) {
|
|
|
247678
248154
|
}] };
|
|
247679
248155
|
}
|
|
247680
248156
|
const diagnostics = [
|
|
247681
|
-
...(graph.diagnostics ?? []).map(
|
|
247682
|
-
...validateGraph(graph, { strict: true }).map(
|
|
248157
|
+
...(graph.diagnostics ?? []).map(diagnostic2),
|
|
248158
|
+
...validateGraph(graph, { strict: true }).map(diagnostic2)
|
|
247683
248159
|
];
|
|
247684
248160
|
const fail = (code, message, suggestion) => {
|
|
247685
248161
|
diagnostics.push({ severity: "error", code, message, suggestion });
|
|
@@ -248807,6 +249283,7 @@ function optionsKeyOf(options) {
|
|
|
248807
249283
|
openApi: options.openApi,
|
|
248808
249284
|
generatePermissions: options.generatePermissions,
|
|
248809
249285
|
typeSafety: options.typeSafety,
|
|
249286
|
+
effect: options.effect,
|
|
248810
249287
|
treeShakeUnusedProviders: options.treeShakeUnusedProviders,
|
|
248811
249288
|
graphql: options.graphql
|
|
248812
249289
|
});
|
|
@@ -249134,7 +249611,17 @@ var DEFAULT_SUPACLOUD_CONFIG = {
|
|
|
249134
249611
|
idempotency: true,
|
|
249135
249612
|
transaction: true
|
|
249136
249613
|
},
|
|
249137
|
-
moduleBoundaryPreset: "modular-monolith"
|
|
249614
|
+
moduleBoundaryPreset: "modular-monolith",
|
|
249615
|
+
effect: {
|
|
249616
|
+
requireRouteEffects: true,
|
|
249617
|
+
requireErrorMappings: true,
|
|
249618
|
+
requireDependencies: true,
|
|
249619
|
+
requireTaggedErrorTypes: true,
|
|
249620
|
+
requireExactDependencyTypes: true,
|
|
249621
|
+
requireTimeoutForDependencies: true,
|
|
249622
|
+
forbidDirectRuntimeExecution: true,
|
|
249623
|
+
forbidDirectThrows: true
|
|
249624
|
+
}
|
|
249138
249625
|
};
|
|
249139
249626
|
function defineSupacloudConfig(config = {}) {
|
|
249140
249627
|
if (config.delivery !== undefined)
|
|
@@ -249146,7 +249633,8 @@ function defineSupacloudConfig(config = {}) {
|
|
|
249146
249633
|
...DEFAULT_SUPACLOUD_CONFIG,
|
|
249147
249634
|
...config,
|
|
249148
249635
|
include: config.include ?? [...DEFAULT_SUPACLOUD_CONFIG.include],
|
|
249149
|
-
graphql: config.graphql ?? DEFAULT_SUPACLOUD_CONFIG.graphql
|
|
249636
|
+
graphql: config.graphql ?? DEFAULT_SUPACLOUD_CONFIG.graphql,
|
|
249637
|
+
effect: config.effect === undefined ? { ...DEFAULT_SUPACLOUD_CONFIG.effect } : { ...DEFAULT_SUPACLOUD_CONFIG.effect, ...config.effect }
|
|
249150
249638
|
};
|
|
249151
249639
|
}
|
|
249152
249640
|
function validateGovernanceConfig(config) {
|
|
@@ -249203,6 +249691,34 @@ function validateGovernanceConfig(config) {
|
|
|
249203
249691
|
if (config[key] !== undefined && typeof config[key] !== "boolean")
|
|
249204
249692
|
throw new Error(`${key} must be a boolean.`);
|
|
249205
249693
|
}
|
|
249694
|
+
if (config.effect !== undefined) {
|
|
249695
|
+
if (!isRecord(config.effect) || Object.keys(config.effect).some((key) => ![
|
|
249696
|
+
"requireRouteEffects",
|
|
249697
|
+
"requireErrorMappings",
|
|
249698
|
+
"requireDependencies",
|
|
249699
|
+
"requireTaggedErrorTypes",
|
|
249700
|
+
"requireExactDependencyTypes",
|
|
249701
|
+
"requireTimeoutForDependencies",
|
|
249702
|
+
"forbidDirectRuntimeExecution",
|
|
249703
|
+
"forbidDirectThrows"
|
|
249704
|
+
].includes(key))) {
|
|
249705
|
+
throw new Error("effect contains an unsupported governance option.");
|
|
249706
|
+
}
|
|
249707
|
+
for (const key of [
|
|
249708
|
+
"requireRouteEffects",
|
|
249709
|
+
"requireErrorMappings",
|
|
249710
|
+
"requireDependencies",
|
|
249711
|
+
"requireTaggedErrorTypes",
|
|
249712
|
+
"requireExactDependencyTypes",
|
|
249713
|
+
"requireTimeoutForDependencies",
|
|
249714
|
+
"forbidDirectRuntimeExecution",
|
|
249715
|
+
"forbidDirectThrows"
|
|
249716
|
+
]) {
|
|
249717
|
+
if (config.effect[key] !== undefined && typeof config.effect[key] !== "boolean") {
|
|
249718
|
+
throw new Error(`effect.${key} must be boolean.`);
|
|
249719
|
+
}
|
|
249720
|
+
}
|
|
249721
|
+
}
|
|
249206
249722
|
}
|
|
249207
249723
|
function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
|
|
249208
249724
|
const resolved = defineSupacloudConfig(config);
|
|
@@ -249223,6 +249739,7 @@ function resolveSupacloudConfig(config = {}, cwd = process.cwd()) {
|
|
|
249223
249739
|
...resolved.allowRouteCommandBindings === undefined ? {} : { allowRouteCommandBindings: resolved.allowRouteCommandBindings },
|
|
249224
249740
|
...resolved.disallowControllerDirectDb === undefined ? {} : { disallowControllerDirectDb: resolved.disallowControllerDirectDb },
|
|
249225
249741
|
...resolved.detectOrphanModules === undefined ? {} : { detectOrphanModules: resolved.detectOrphanModules },
|
|
249742
|
+
effect: resolved.effect === undefined ? { ...DEFAULT_SUPACLOUD_CONFIG.effect } : { ...DEFAULT_SUPACLOUD_CONFIG.effect, ...resolved.effect },
|
|
249226
249743
|
treeShakeUnusedProviders: resolved.treeShakeUnusedProviders ?? DEFAULT_SUPACLOUD_CONFIG.treeShakeUnusedProviders,
|
|
249227
249744
|
graphql: resolved.graphql ? {
|
|
249228
249745
|
...resolved.graphql,
|
|
@@ -249371,16 +249888,16 @@ var starter_metadata_default = {
|
|
|
249371
249888
|
generatedBy: "supacloud/starter-metadata-v1",
|
|
249372
249889
|
packages: {
|
|
249373
249890
|
compiler: {
|
|
249374
|
-
version: "0.
|
|
249891
|
+
version: "0.42.0"
|
|
249375
249892
|
},
|
|
249376
249893
|
app: {
|
|
249377
|
-
version: "0.
|
|
249894
|
+
version: "0.24.0",
|
|
249378
249895
|
dependencies: {
|
|
249379
249896
|
rxjs: "7.8.2"
|
|
249380
249897
|
}
|
|
249381
249898
|
},
|
|
249382
249899
|
elysia: {
|
|
249383
|
-
version: "0.
|
|
249900
|
+
version: "0.29.0"
|
|
249384
249901
|
},
|
|
249385
249902
|
commands: {
|
|
249386
249903
|
version: "0.9.1"
|
|
@@ -249734,6 +250251,7 @@ function baseFiles(name, sdkDependencies) {
|
|
|
249734
250251
|
dependencies: {
|
|
249735
250252
|
"@supacloud/app": `^${appMetadata.version}`,
|
|
249736
250253
|
"@supacloud/elysia": `^${elysiaMetadata.version}`,
|
|
250254
|
+
effect: "4.0.2",
|
|
249737
250255
|
elysia: "2.0.0-beta.21",
|
|
249738
250256
|
...sdkDependencies ?? { rxjs: appMetadata.dependencies.rxjs }
|
|
249739
250257
|
},
|
|
@@ -249778,6 +250296,16 @@ export default defineSupacloudConfig({
|
|
|
249778
250296
|
outDir: "generated",
|
|
249779
250297
|
strict: true,
|
|
249780
250298
|
moduleBoundaryPreset: "modular-monolith",
|
|
250299
|
+
effect: {
|
|
250300
|
+
requireRouteEffects: true,
|
|
250301
|
+
requireErrorMappings: true,
|
|
250302
|
+
requireDependencies: true,
|
|
250303
|
+
requireTaggedErrorTypes: true,
|
|
250304
|
+
requireExactDependencyTypes: true,
|
|
250305
|
+
requireTimeoutForDependencies: true,
|
|
250306
|
+
forbidDirectRuntimeExecution: true,
|
|
250307
|
+
forbidDirectThrows: true,
|
|
250308
|
+
},
|
|
249781
250309
|
typeSafety: { scanProductionSource: true, noAnyInGenerated: true },
|
|
249782
250310
|
disallowControllerDirectDb: true,
|
|
249783
250311
|
detectOrphanModules: true,
|
|
@@ -249806,20 +250334,34 @@ export const CreateOrderBody = t.Object({ name: t.String({ minLength: 1 }) });
|
|
|
249806
250334
|
export const OrderResult = t.Object({ id: t.String(), name: t.String() });
|
|
249807
250335
|
export const HealthResult = t.Object({ ok: t.Boolean() });
|
|
249808
250336
|
`,
|
|
249809
|
-
"src/orders/orders.ts": `import {
|
|
250337
|
+
"src/orders/orders.ts": `import { Effect } from "effect";
|
|
250338
|
+
import { Body, Controller, Get, Param, Post, defineFeatureSlice } from "@supacloud/app";
|
|
249810
250339
|
import { status } from "elysia";
|
|
249811
250340
|
import { OrderParams, CreateOrderBody, OrderResult, HealthResult } from "./contracts";
|
|
249812
250341
|
@Controller("/orders")
|
|
249813
250342
|
export class OrdersController {
|
|
249814
|
-
@Get("/health", {
|
|
249815
|
-
|
|
249816
|
-
|
|
249817
|
-
|
|
249818
|
-
|
|
249819
|
-
|
|
249820
|
-
@
|
|
250343
|
+
@Get("/health", {
|
|
250344
|
+
responses: { 200: HealthResult },
|
|
250345
|
+
effect: { required: true, dependencies: [], errors: [], retry: "none" },
|
|
250346
|
+
})
|
|
250347
|
+
health(): Effect.Effect<{ ok: boolean }, never, never> { return Effect.succeed({ ok: true }); }
|
|
250348
|
+
|
|
250349
|
+
@Get("/:id", {
|
|
250350
|
+
params: OrderParams,
|
|
250351
|
+
responses: { 200: OrderResult },
|
|
250352
|
+
effect: { required: true, dependencies: [], errors: [], retry: "none" },
|
|
250353
|
+
})
|
|
250354
|
+
get(@Param("id") id: string): Effect.Effect<{ id: string; name: string }, never, never> {
|
|
250355
|
+
return Effect.succeed({ id, name: "demo" });
|
|
250356
|
+
}
|
|
250357
|
+
|
|
250358
|
+
@Post("/", {
|
|
250359
|
+
body: CreateOrderBody,
|
|
250360
|
+
responses: { 201: OrderResult },
|
|
250361
|
+
effect: { required: true, dependencies: [], errors: [], retry: "none" },
|
|
250362
|
+
})
|
|
249821
250363
|
create(@Body() body: { name: string }) {
|
|
249822
|
-
return status(201, { id: "demo", name: body.name });
|
|
250364
|
+
return Effect.succeed(status(201, { id: "demo", name: body.name }));
|
|
249823
250365
|
}
|
|
249824
250366
|
}
|
|
249825
250367
|
|
|
@@ -249993,22 +250535,27 @@ function minimalTemplate(name, sdkDependencies) {
|
|
|
249993
250535
|
"src/features/health/contracts.ts": `import { t } from "elysia";
|
|
249994
250536
|
export const HealthResult = t.Object({ ok: t.Boolean() });
|
|
249995
250537
|
`,
|
|
249996
|
-
"src/features/health/health.ts": `import {
|
|
250538
|
+
"src/features/health/health.ts": `import { Effect } from "effect";
|
|
250539
|
+
import { Controller, Get, Module } from "@supacloud/app/core";
|
|
249997
250540
|
import { HealthResult } from "./contracts";
|
|
249998
250541
|
@Controller("/health")
|
|
249999
250542
|
export class HealthController {
|
|
250000
|
-
@Get("/", {
|
|
250001
|
-
|
|
250543
|
+
@Get("/", {
|
|
250544
|
+
responses: { 200: HealthResult },
|
|
250545
|
+
effect: { required: true, dependencies: [], errors: [], retry: "none" },
|
|
250546
|
+
})
|
|
250547
|
+
health(): Effect.Effect<{ ok: boolean }, never, never> { return Effect.succeed({ ok: true }); }
|
|
250002
250548
|
}
|
|
250003
250549
|
|
|
250004
250550
|
@Module({ name: "health", tags: ["type:feature"], controllers: [HealthController] })
|
|
250005
250551
|
export class HealthModule {}
|
|
250006
250552
|
`,
|
|
250007
|
-
"src/features/health/health.test.ts": `import {
|
|
250553
|
+
"src/features/health/health.test.ts": `import { Effect } from "effect";
|
|
250554
|
+
import { expect, test } from "bun:test";
|
|
250008
250555
|
import { HealthController } from "./health";
|
|
250009
250556
|
|
|
250010
|
-
test("health is a public, side-effect-free operation", () => {
|
|
250011
|
-
expect(new HealthController().health()).toEqual({ ok: true });
|
|
250557
|
+
test("health is a public, side-effect-free operation", async () => {
|
|
250558
|
+
expect(await Effect.runPromise(new HealthController().health())).toEqual({ ok: true });
|
|
250012
250559
|
});
|
|
250013
250560
|
`,
|
|
250014
250561
|
"src/app.module.ts": `import { Module } from "@supacloud/app/core";
|
|
@@ -250533,13 +251080,25 @@ export async function createDeliveryWorker(modules: CompiledModule[], lifecycle:
|
|
|
250533
251080
|
`;
|
|
250534
251081
|
|
|
250535
251082
|
// src/shared/tools/app-starter-upload.ts
|
|
250536
|
-
var STARTER_UPLOAD_FEATURE = `import {
|
|
251083
|
+
var STARTER_UPLOAD_FEATURE = `import { Effect } from "effect";
|
|
251084
|
+
import { Body, Controller, Inject, Injectable, Param, Post, DB_CLIENT, REQUEST_CONTEXT } from "@supacloud/app";
|
|
250537
251085
|
import { ApplicationError } from "@supacloud/elysia";
|
|
250538
251086
|
import { UploadBody, UploadLocation, UploadParams, UploadResult } from "./contracts";
|
|
250539
251087
|
|
|
250540
251088
|
export interface ReviewUploadInput { reviewId: string; artifactId: string; expectedVersion: number }
|
|
250541
251089
|
export interface ReviewUploadLocation { artifactId: string; bucketId: string; objectPath: string }
|
|
250542
251090
|
export interface ReviewUploadResult { artifactId: string; runId: string; objectPath: string; sha256: string; bytes: number }
|
|
251091
|
+
type PreviewUploadError =
|
|
251092
|
+
| { readonly _tag: "AUTHENTICATION_REQUIRED" }
|
|
251093
|
+
| { readonly _tag: "UPLOAD_INPUT_INVALID" }
|
|
251094
|
+
| { readonly _tag: "UPLOAD_DENIED" }
|
|
251095
|
+
| { readonly _tag: "UPLOADS_UNAVAILABLE" };
|
|
251096
|
+
type RegisterUploadError =
|
|
251097
|
+
| PreviewUploadError
|
|
251098
|
+
| { readonly _tag: "UPLOAD_CONTENT_INVALID" }
|
|
251099
|
+
| { readonly _tag: "COMMAND_IDEMPOTENCY_CONFLICT" }
|
|
251100
|
+
| { readonly _tag: "COMMAND_REJECTED" }
|
|
251101
|
+
| { readonly _tag: "UPLOAD_OUTCOME_UNKNOWN" };
|
|
250543
251102
|
export interface ReviewUploadPort {
|
|
250544
251103
|
previewUpload(context: unknown, input: ReviewUploadInput): Promise<ReviewUploadLocation>;
|
|
250545
251104
|
registerUpload(context: unknown, input: ReviewUploadInput): Promise<ReviewUploadResult>;
|
|
@@ -250563,13 +251122,47 @@ export class ReviewUploads {
|
|
|
250563
251122
|
export class ReviewUploadsController {
|
|
250564
251123
|
constructor(@Inject(ReviewUploads) private readonly uploads: ReviewUploads) {}
|
|
250565
251124
|
|
|
250566
|
-
@Post("/:id/attachment-upload", {
|
|
250567
|
-
|
|
250568
|
-
|
|
250569
|
-
|
|
250570
|
-
|
|
250571
|
-
|
|
250572
|
-
|
|
251125
|
+
@Post("/:id/attachment-upload", {
|
|
251126
|
+
params: UploadParams,
|
|
251127
|
+
body: UploadBody,
|
|
251128
|
+
responses: { 200: UploadLocation },
|
|
251129
|
+
effect: {
|
|
251130
|
+
required: true,
|
|
251131
|
+
dependencies: [],
|
|
251132
|
+
errors: [
|
|
251133
|
+
{ tag: "AUTHENTICATION_REQUIRED", status: 401, code: "AUTHENTICATION_REQUIRED" },
|
|
251134
|
+
{ tag: "UPLOAD_INPUT_INVALID", status: 400, code: "UPLOAD_INPUT_INVALID" },
|
|
251135
|
+
{ tag: "UPLOAD_DENIED", status: 403, code: "UPLOAD_DENIED" },
|
|
251136
|
+
{ tag: "UPLOADS_UNAVAILABLE", status: 501, code: "UPLOADS_UNAVAILABLE" },
|
|
251137
|
+
],
|
|
251138
|
+
retry: "none",
|
|
251139
|
+
},
|
|
251140
|
+
})
|
|
251141
|
+
preview(@Param("id") id: string, @Body() body: { artifactId: string; expectedVersion: number }): Effect.Effect<ReviewUploadLocation, PreviewUploadError, never> {
|
|
251142
|
+
return Effect.promise(() => this.uploads.preview({ reviewId: id, artifactId: body.artifactId, expectedVersion: body.expectedVersion }));
|
|
251143
|
+
}
|
|
251144
|
+
@Post("/:id/attachment-registration", {
|
|
251145
|
+
params: UploadParams,
|
|
251146
|
+
body: UploadBody,
|
|
251147
|
+
responses: { 200: UploadResult },
|
|
251148
|
+
effect: {
|
|
251149
|
+
required: true,
|
|
251150
|
+
dependencies: [],
|
|
251151
|
+
errors: [
|
|
251152
|
+
{ tag: "AUTHENTICATION_REQUIRED", status: 401, code: "AUTHENTICATION_REQUIRED" },
|
|
251153
|
+
{ tag: "UPLOAD_INPUT_INVALID", status: 400, code: "UPLOAD_INPUT_INVALID" },
|
|
251154
|
+
{ tag: "UPLOAD_DENIED", status: 403, code: "UPLOAD_DENIED" },
|
|
251155
|
+
{ tag: "UPLOAD_CONTENT_INVALID", status: 400, code: "UPLOAD_CONTENT_INVALID" },
|
|
251156
|
+
{ tag: "COMMAND_IDEMPOTENCY_CONFLICT", status: 409, code: "COMMAND_IDEMPOTENCY_CONFLICT" },
|
|
251157
|
+
{ tag: "COMMAND_REJECTED", status: 403, code: "COMMAND_REJECTED" },
|
|
251158
|
+
{ tag: "UPLOADS_UNAVAILABLE", status: 501, code: "UPLOADS_UNAVAILABLE" },
|
|
251159
|
+
{ tag: "UPLOAD_OUTCOME_UNKNOWN", status: 503, code: "UPLOAD_OUTCOME_UNKNOWN" },
|
|
251160
|
+
],
|
|
251161
|
+
retry: "none",
|
|
251162
|
+
},
|
|
251163
|
+
})
|
|
251164
|
+
register(@Param("id") id: string, @Body() body: { artifactId: string; expectedVersion: number }): Effect.Effect<ReviewUploadResult, RegisterUploadError, never> {
|
|
251165
|
+
return Effect.promise(() => this.uploads.register({ reviewId: id, artifactId: body.artifactId, expectedVersion: body.expectedVersion }));
|
|
250573
251166
|
}
|
|
250574
251167
|
}
|
|
250575
251168
|
`;
|
|
@@ -250853,6 +251446,10 @@ function rows(value: unknown): Record<string, unknown>[] {
|
|
|
250853
251446
|
if (!Array.isArray(value)) throw new TypeError("Invalid database result");
|
|
250854
251447
|
return value.map(record);
|
|
250855
251448
|
}
|
|
251449
|
+
function isPublicConflict(value: unknown): boolean {
|
|
251450
|
+
return value instanceof ApplicationError && value.status === 409
|
|
251451
|
+
|| isRecord(value) && value.expose === true && value.status === 409 && typeof value.code === "string";
|
|
251452
|
+
}
|
|
250856
251453
|
function input(value: unknown): Approval {
|
|
250857
251454
|
const item = record(value);
|
|
250858
251455
|
if (typeof item.id !== "string" || !item.id.length || typeof item.expectedVersion !== "number"
|
|
@@ -250967,7 +251564,7 @@ export async function createReviewPostgresAdapters(options: ReviewPostgresOption
|
|
|
250967
251564
|
execute: tx => scope.run(tx, async () => {
|
|
250968
251565
|
try { return await next(); }
|
|
250969
251566
|
catch (error) {
|
|
250970
|
-
if (error
|
|
251567
|
+
if (isPublicConflict(error)) {
|
|
250971
251568
|
throw new CommandError("COMMAND_IDEMPOTENCY_CONFLICT");
|
|
250972
251569
|
}
|
|
250973
251570
|
throw error;
|
|
@@ -251276,6 +251873,7 @@ function appStarterFiles(name) {
|
|
|
251276
251873
|
"drizzle-orm": dbMetadata.peerDependencies["drizzle-orm"],
|
|
251277
251874
|
"@supacloud/js": `^${sdkMetadata.version}`,
|
|
251278
251875
|
"@supabase/supabase-js": sdkMetadata.peerDependencies["@supabase/supabase-js"],
|
|
251876
|
+
effect: "4.0.2",
|
|
251279
251877
|
elysia: "2.0.0-beta.21",
|
|
251280
251878
|
rxjs: appMetadata2.dependencies.rxjs
|
|
251281
251879
|
},
|
|
@@ -251331,6 +251929,16 @@ export default defineSupacloudConfig({
|
|
|
251331
251929
|
outDir: "generated",
|
|
251332
251930
|
strict: true,
|
|
251333
251931
|
moduleBoundaryPreset: "modular-monolith",
|
|
251932
|
+
effect: {
|
|
251933
|
+
requireRouteEffects: true,
|
|
251934
|
+
requireErrorMappings: true,
|
|
251935
|
+
requireDependencies: true,
|
|
251936
|
+
requireTaggedErrorTypes: true,
|
|
251937
|
+
requireExactDependencyTypes: true,
|
|
251938
|
+
requireTimeoutForDependencies: true,
|
|
251939
|
+
forbidDirectRuntimeExecution: true,
|
|
251940
|
+
forbidDirectThrows: true,
|
|
251941
|
+
},
|
|
251334
251942
|
typeSafety: { scanProductionSource: true, noAnyInGenerated: true },
|
|
251335
251943
|
disallowControllerDirectDb: true,
|
|
251336
251944
|
detectOrphanModules: true,
|
|
@@ -251531,7 +252139,8 @@ export function createCompiledModules(): never {
|
|
|
251531
252139
|
throw new Error("Run bun run compile before starting the application");
|
|
251532
252140
|
}
|
|
251533
252141
|
`,
|
|
251534
|
-
"src/review/review.ts": `import {
|
|
252142
|
+
"src/review/review.ts": `import { Effect } from "effect";
|
|
252143
|
+
import {
|
|
251535
252144
|
Body, Command, Controller, DB_CLIENT, Get, Inject, Param, Post,
|
|
251536
252145
|
defineFeatureSlice, defineFeatureSpec, type Aspect,
|
|
251537
252146
|
} from "@supacloud/app";
|
|
@@ -251597,16 +252206,51 @@ export class ApproveReview {
|
|
|
251597
252206
|
}
|
|
251598
252207
|
}
|
|
251599
252208
|
|
|
252209
|
+
type ReviewError =
|
|
252210
|
+
| { readonly _tag: "AUTHENTICATION_REQUIRED" }
|
|
252211
|
+
| { readonly _tag: "REVIEW_NOT_FOUND" }
|
|
252212
|
+
| { readonly _tag: "REVIEW_PERMISSION_DENIED" }
|
|
252213
|
+
| { readonly _tag: "REVIEW_CONFLICT" }
|
|
252214
|
+
| { readonly _tag: "FEATURE_TRANSITION_CONFLICT" }
|
|
252215
|
+
| { readonly _tag: "COMMAND_IDEMPOTENCY_CONFLICT" }
|
|
252216
|
+
| { readonly _tag: "COMMAND_REJECTED" }
|
|
252217
|
+
| { readonly _tag: "REVIEW_COMMAND_UNSUPPORTED" }
|
|
252218
|
+
| { readonly _tag: "COMMAND_OUTCOME_UNKNOWN" };
|
|
252219
|
+
|
|
251600
252220
|
@Controller("/reviews")
|
|
251601
252221
|
export class ReviewController {
|
|
251602
252222
|
constructor(@Inject(ApproveReview) private readonly approveReview: ApproveReview) {}
|
|
251603
252223
|
|
|
251604
|
-
@Get("/health", {
|
|
251605
|
-
|
|
251606
|
-
|
|
251607
|
-
|
|
251608
|
-
|
|
251609
|
-
|
|
252224
|
+
@Get("/health", {
|
|
252225
|
+
responses: { 200: HealthResult },
|
|
252226
|
+
effect: { required: true, dependencies: [], errors: [], retry: "none" },
|
|
252227
|
+
})
|
|
252228
|
+
health(): Effect.Effect<{ ok: boolean }, never, never> { return Effect.succeed({ ok: true }); }
|
|
252229
|
+
|
|
252230
|
+
@Post("/:id/approve", {
|
|
252231
|
+
command: ApproveReview,
|
|
252232
|
+
params: Params,
|
|
252233
|
+
body: ApproveBody,
|
|
252234
|
+
responses: { 200: ReviewResult },
|
|
252235
|
+
effect: {
|
|
252236
|
+
required: true,
|
|
252237
|
+
dependencies: [],
|
|
252238
|
+
errors: [
|
|
252239
|
+
{ tag: "AUTHENTICATION_REQUIRED", status: 401, code: "AUTHENTICATION_REQUIRED" },
|
|
252240
|
+
{ tag: "REVIEW_NOT_FOUND", status: 404, code: "REVIEW_NOT_FOUND" },
|
|
252241
|
+
{ tag: "REVIEW_PERMISSION_DENIED", status: 403, code: "REVIEW_PERMISSION_DENIED" },
|
|
252242
|
+
{ tag: "REVIEW_CONFLICT", status: 409, code: "REVIEW_CONFLICT" },
|
|
252243
|
+
{ tag: "FEATURE_TRANSITION_CONFLICT", status: 409, code: "FEATURE_TRANSITION_CONFLICT" },
|
|
252244
|
+
{ tag: "COMMAND_IDEMPOTENCY_CONFLICT", status: 409, code: "COMMAND_IDEMPOTENCY_CONFLICT" },
|
|
252245
|
+
{ tag: "COMMAND_REJECTED", status: 403, code: "COMMAND_REJECTED" },
|
|
252246
|
+
{ tag: "REVIEW_COMMAND_UNSUPPORTED", status: 501, code: "REVIEW_COMMAND_UNSUPPORTED" },
|
|
252247
|
+
{ tag: "COMMAND_OUTCOME_UNKNOWN", status: 503, code: "COMMAND_OUTCOME_UNKNOWN" },
|
|
252248
|
+
],
|
|
252249
|
+
retry: "none",
|
|
252250
|
+
},
|
|
252251
|
+
})
|
|
252252
|
+
approve(@Param("id") id: string, @Body() body: { expectedVersion: number }): Effect.Effect<Review, ReviewError, never> {
|
|
252253
|
+
return Effect.promise(() => this.approveReview.execute(id, body.expectedVersion));
|
|
251610
252254
|
}
|
|
251611
252255
|
}
|
|
251612
252256
|
|
|
@@ -252145,7 +252789,8 @@ export class ${className}Service {
|
|
|
252145
252789
|
}
|
|
252146
252790
|
}
|
|
252147
252791
|
`,
|
|
252148
|
-
[`${name}.controller.ts`]: `import {
|
|
252792
|
+
[`${name}.controller.ts`]: `import { Effect } from "effect";
|
|
252793
|
+
import { Controller, Get, Inject, Param } from "@supacloud/app";
|
|
252149
252794
|
import { ${className}Params, ${className}Response, type ${className}Result } from "./${name}.model";
|
|
252150
252795
|
import { ${className}Service } from "./${name}.service";
|
|
252151
252796
|
|
|
@@ -252153,9 +252798,13 @@ import { ${className}Service } from "./${name}.service";
|
|
|
252153
252798
|
export class ${className}Controller {
|
|
252154
252799
|
constructor(@Inject(${className}Service) readonly service: Pick<${className}Service, "find">) {}
|
|
252155
252800
|
|
|
252156
|
-
@Get("/:id", {
|
|
252157
|
-
|
|
252158
|
-
|
|
252801
|
+
@Get("/:id", {
|
|
252802
|
+
params: ${className}Params,
|
|
252803
|
+
responses: { 200: ${className}Response },
|
|
252804
|
+
effect: { required: true, dependencies: [], errors: [], retry: "none" },
|
|
252805
|
+
})
|
|
252806
|
+
find(@Param("id") id: string): Effect.Effect<${className}Result, never, never> {
|
|
252807
|
+
return Effect.promise(() => this.service.find(id));
|
|
252159
252808
|
}
|
|
252160
252809
|
}
|
|
252161
252810
|
`,
|
|
@@ -252190,19 +252839,20 @@ test("${name} preserves permission denial without a privileged fallback", async
|
|
|
252190
252839
|
await expect(service.find("example")).rejects.toBe(denied);
|
|
252191
252840
|
});
|
|
252192
252841
|
`,
|
|
252193
|
-
[`${name}.controller.test.ts`]: `import {
|
|
252842
|
+
[`${name}.controller.test.ts`]: `import { Effect } from "effect";
|
|
252843
|
+
import { expect, test } from "bun:test";
|
|
252194
252844
|
import { ${className}Controller } from "./${name}.controller";
|
|
252195
252845
|
|
|
252196
252846
|
test("${name} controller delegates to the supplied asynchronous read port", async () => {
|
|
252197
252847
|
// Unit-test double only; the generated runtime service remains fail-closed.
|
|
252198
252848
|
const controller = new ${className}Controller({ find: async (id) => ({ id }) });
|
|
252199
|
-
expect(await controller.find("example")).toEqual({ id: "example" });
|
|
252849
|
+
expect(await Effect.runPromise(controller.find("example"))).toEqual({ id: "example" });
|
|
252200
252850
|
});
|
|
252201
252851
|
|
|
252202
252852
|
test("${name} controller preserves an asynchronous read failure", async () => {
|
|
252203
252853
|
const failure = new Error("read port unavailable");
|
|
252204
252854
|
const controller = new ${className}Controller({ find: async () => { throw failure; } });
|
|
252205
|
-
await expect(controller.find("example")).rejects.
|
|
252855
|
+
await expect(Effect.runPromise(controller.find("example"))).rejects.toThrow("read port unavailable");
|
|
252206
252856
|
});
|
|
252207
252857
|
`
|
|
252208
252858
|
};
|
|
@@ -253726,6 +254376,91 @@ function formatReconcileReport(report) {
|
|
|
253726
254376
|
return lines.join(`
|
|
253727
254377
|
`);
|
|
253728
254378
|
}
|
|
254379
|
+
function moduleObjects2(module) {
|
|
254380
|
+
return [
|
|
254381
|
+
...module.tables.map((table) => ({
|
|
254382
|
+
kind: "table",
|
|
254383
|
+
name: table,
|
|
254384
|
+
module: module.name,
|
|
254385
|
+
source: null,
|
|
254386
|
+
tests: []
|
|
254387
|
+
})),
|
|
254388
|
+
...module.policies.map((policy) => ({
|
|
254389
|
+
kind: "policy",
|
|
254390
|
+
name: policy.name,
|
|
254391
|
+
module: module.name,
|
|
254392
|
+
table: policy.table,
|
|
254393
|
+
source: policy.source,
|
|
254394
|
+
tests: policy.tests ?? []
|
|
254395
|
+
})),
|
|
254396
|
+
...module.functions.map((fn) => ({
|
|
254397
|
+
kind: "function",
|
|
254398
|
+
name: fn.name,
|
|
254399
|
+
module: module.name,
|
|
254400
|
+
source: fn.source,
|
|
254401
|
+
tests: fn.tests ?? [],
|
|
254402
|
+
security: fn.security,
|
|
254403
|
+
permission: fn.permission,
|
|
254404
|
+
transaction: fn.transaction,
|
|
254405
|
+
audit: fn.audit,
|
|
254406
|
+
idempotency: fn.idempotency
|
|
254407
|
+
})),
|
|
254408
|
+
...module.triggers.map((trigger) => ({
|
|
254409
|
+
kind: "trigger",
|
|
254410
|
+
name: trigger.name,
|
|
254411
|
+
module: module.name,
|
|
254412
|
+
table: trigger.table,
|
|
254413
|
+
source: trigger.source,
|
|
254414
|
+
tests: []
|
|
254415
|
+
})),
|
|
254416
|
+
...module.grants.map((grant) => ({
|
|
254417
|
+
kind: "grant",
|
|
254418
|
+
name: `${grant.object}:${grant.privilege}:${grant.role}`,
|
|
254419
|
+
module: module.name,
|
|
254420
|
+
source: grant.source,
|
|
254421
|
+
tests: []
|
|
254422
|
+
}))
|
|
254423
|
+
];
|
|
254424
|
+
}
|
|
254425
|
+
function objectMatchesTarget(object, target) {
|
|
254426
|
+
return object.name === target || object.table === target || object.kind === "policy" && `${object.table}.${object.name}` === target || object.kind === "grant" && object.name.startsWith(`${target}:`);
|
|
254427
|
+
}
|
|
254428
|
+
function buildDatabaseAiContext(modules, root, moduleName, target) {
|
|
254429
|
+
const selected = moduleName ? modules.filter((module) => module.name === moduleName) : [...modules];
|
|
254430
|
+
if (moduleName && selected.length === 0) {
|
|
254431
|
+
throw new Error(`未找到数据库模块: ${moduleName}(可用: ${modules.map((module) => module.name).join(", ") || "none"})`);
|
|
254432
|
+
}
|
|
254433
|
+
const allObjects = selected.flatMap(moduleObjects2);
|
|
254434
|
+
const objects = target ? allObjects.filter((object) => objectMatchesTarget(object, target)) : allObjects;
|
|
254435
|
+
if (target && objects.length === 0)
|
|
254436
|
+
throw new Error(`未找到数据库对象: ${target}`);
|
|
254437
|
+
return {
|
|
254438
|
+
version: 1,
|
|
254439
|
+
scope: "database-ai-context",
|
|
254440
|
+
root,
|
|
254441
|
+
selectedModules: selected.map((module) => module.name),
|
|
254442
|
+
target: target ?? null,
|
|
254443
|
+
objects,
|
|
254444
|
+
maintenance: {
|
|
254445
|
+
structureSource: "drizzle",
|
|
254446
|
+
behaviorSource: "sql",
|
|
254447
|
+
generatedPaths: ["generated/", "bootstrap/schema.sql", "output/database-audit/"],
|
|
254448
|
+
protectedPaths: ["migrations/", "supabase/migrations/", "generated/"],
|
|
254449
|
+
forwardMigrationPath: "migrations/<timestamp>_<intent>.sql",
|
|
254450
|
+
rules: [
|
|
254451
|
+
"Edit the selected maintained source, not a generated snapshot.",
|
|
254452
|
+
"Keep RPC, RLS, trigger and grant behavior in explicit SQL sources.",
|
|
254453
|
+
"Create a new forward migration; never rewrite an applied migration.",
|
|
254454
|
+
"Run lint, focused behavior tests and catalog reconciliation after edits."
|
|
254455
|
+
]
|
|
254456
|
+
},
|
|
254457
|
+
verification: [
|
|
254458
|
+
"supacloud-cli db lint --module_file db/modules.ts",
|
|
254459
|
+
"supacloud-cli db explain --target <object>",
|
|
254460
|
+
'supacloud-cli db module_check --module_file db/modules.ts --database_url "$DATABASE_URL"'
|
|
254461
|
+
]
|
|
254462
|
+
};
|
|
254463
|
+
}
|
|
253729
254464
|
async function runModuleCheck(module, executor, schemas = ["public"]) {
|
|
253730
254465
|
const catalog = await readCatalog(executor, schemas);
|
|
253731
254466
|
return reconcileModule(module, catalog);
|
|
@@ -253769,6 +254504,10 @@ async function runExplain2(args, root) {
|
|
|
253769
254504
|
const text = explainObject(manifest, target);
|
|
253770
254505
|
return textResult2(text, text.startsWith("未找到对象"));
|
|
253771
254506
|
}
|
|
254507
|
+
async function runContext2(args, root) {
|
|
254508
|
+
const modules = await loadDatabaseModules(root, args.module_file);
|
|
254509
|
+
return textResult2(JSON.stringify(buildDatabaseAiContext(modules, root, args.module, args.target), null, 2));
|
|
254510
|
+
}
|
|
253772
254511
|
var defaultLiteSpawn = async (command, cwd) => {
|
|
253773
254512
|
const proc = Bun.spawn({ cmd: command, cwd, stdout: "pipe", stderr: "pipe" });
|
|
253774
254513
|
const [exitCode, stdout, stderr] = await Promise.all([
|
|
@@ -253826,12 +254565,12 @@ function registerDbGovernanceTools(server2, options = {}) {
|
|
|
253826
254565
|
const environment = options.environment || process.env;
|
|
253827
254566
|
const fallbackRoot = options.currentWorkingDirectory || process.cwd();
|
|
253828
254567
|
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
|
|
254568
|
+
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", {
|
|
254569
|
+
action: withDescription(stringEnum(["context", "lint", "explain", "module_check"]), "Database governance action"),
|
|
254570
|
+
module: optional(String2(), "[context/lint] Only select this manifest module (default: all)"),
|
|
253832
254571
|
root: optional(String2(), "[*] Project root (default: current directory)"),
|
|
253833
254572
|
module_file: optional(String2(), "[*] File exporting defineDatabaseModule(...) (default: <root>/db/modules.ts)"),
|
|
253834
|
-
target: optional(String2(), "[explain] Policy / function / table name to explain"),
|
|
254573
|
+
target: optional(String2(), "[context/explain] Policy / function / table name to explain"),
|
|
253835
254574
|
database_url: optional(String2(), "[module_check] Postgres connection URL (default: DATABASE_URL)"),
|
|
253836
254575
|
schema: optional(String2(), "[module_check] Comma-separated schemas to inspect (default: public)"),
|
|
253837
254576
|
lite: optional(Boolean2(), "[module_check] Reconcile against a local SupaCloud Lite project (runs supacloud-lite db check)"),
|
|
@@ -253839,6 +254578,8 @@ function registerDbGovernanceTools(server2, options = {}) {
|
|
|
253839
254578
|
}, async (request) => {
|
|
253840
254579
|
const root = resolve25(request.root || fallbackRoot);
|
|
253841
254580
|
switch (request.action) {
|
|
254581
|
+
case "context":
|
|
254582
|
+
return runContext2(request, root);
|
|
253842
254583
|
case "lint":
|
|
253843
254584
|
return runLint(request, root);
|
|
253844
254585
|
case "explain":
|
|
@@ -256652,7 +257393,7 @@ function registerRemoteDevTools(server2, options = {}) {
|
|
|
256652
257393
|
// package.json
|
|
256653
257394
|
var package_default = {
|
|
256654
257395
|
name: "@supacloud/cli",
|
|
256655
|
-
version: "0.
|
|
257396
|
+
version: "0.74.0",
|
|
256656
257397
|
description: "Project-scoped CLI for SupaCloud users",
|
|
256657
257398
|
type: "module",
|
|
256658
257399
|
main: "./dist/index.js",
|
|
@@ -256686,7 +257427,7 @@ var package_default = {
|
|
|
256686
257427
|
directory: "packages/cli"
|
|
256687
257428
|
},
|
|
256688
257429
|
dependencies: {
|
|
256689
|
-
"@supacloud/compiler": "^0.
|
|
257430
|
+
"@supacloud/compiler": "^0.42.0",
|
|
256690
257431
|
"@supacloud/delivery": "0.8.0",
|
|
256691
257432
|
"@supacloud/db": "^0.14.0",
|
|
256692
257433
|
fflate: "^0.8.3",
|
|
@@ -256699,6 +257440,7 @@ var package_default = {
|
|
|
256699
257440
|
devDependencies: {
|
|
256700
257441
|
"@supacloud/elysia": "^0.24.1",
|
|
256701
257442
|
"@types/bun": "^1.4.2",
|
|
257443
|
+
effect: "4.0.2",
|
|
256702
257444
|
elysia: "2.0.0-beta.21",
|
|
256703
257445
|
typescript: "^7.0.2"
|
|
256704
257446
|
}
|
|
@@ -256981,6 +257723,8 @@ EXAMPLES
|
|
|
256981
257723
|
${preferredCommand} context --format json
|
|
256982
257724
|
${preferredCommand} context --target CaseModule --format json
|
|
256983
257725
|
${preferredCommand} doctor
|
|
257726
|
+
${preferredCommand} db context --root . --module_file db/modules.ts --module cases
|
|
257727
|
+
${preferredCommand} db context --root . --module_file db/modules.ts --target public.case_create
|
|
256984
257728
|
${preferredCommand} db lint --root . --module_file db/modules.ts
|
|
256985
257729
|
${preferredCommand} db explain --target public.cases --module_file db/modules.ts
|
|
256986
257730
|
${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.74.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.42.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": "4.0.2",
|
|
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
|
|