@supacloud/compiler 0.57.1 → 0.58.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/cli.js +223 -205
- package/dist/index.js +223 -205
- package/dist/migration-app-version.d.ts +1 -1
- package/dist/migration-elysia-version.d.ts +1 -1
- package/dist/types.d.ts +3 -0
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -382,6 +382,7 @@ var init_di_resolution = __esm(() => {
|
|
|
382
382
|
"conflicting-injection-flags",
|
|
383
383
|
"cross-module-scoped-dependency",
|
|
384
384
|
"mixed-multi-provider-scopes",
|
|
385
|
+
"effect-contract-required",
|
|
385
386
|
"dynamic-aspect-reference",
|
|
386
387
|
"invalid-aspect-reference",
|
|
387
388
|
"aspect-operation-mismatch"
|
|
@@ -2453,6 +2454,7 @@ function renderApplication(graph, options) {
|
|
|
2453
2454
|
executor: route.command === undefined ? "route" : "command",
|
|
2454
2455
|
...command === undefined ? {} : { commandBoundary: route.command === undefined ? "handler" : "route" },
|
|
2455
2456
|
...route.effect === undefined ? {} : { effect: route.effect },
|
|
2457
|
+
...route.effectMode === undefined ? {} : { effectMode: route.effectMode },
|
|
2456
2458
|
invoker: isPipeline ? "pipeline" : "direct",
|
|
2457
2459
|
stages: operationPlans.find((plan) => plan.module === module.name && plan.kind === "route" && plan.name === `${route.method} ${joinRoutePaths(controller.path, route.path)}`).stages,
|
|
2458
2460
|
...commandRef === undefined ? {} : { command: command?.name ?? commandRef },
|
|
@@ -2832,6 +2834,8 @@ class ModuleGenerator {
|
|
|
2832
2834
|
if (route.effect !== undefined) {
|
|
2833
2835
|
fields.push(`effect: ${JSON.stringify(route.effect)}`);
|
|
2834
2836
|
}
|
|
2837
|
+
if (route.effectMode !== undefined)
|
|
2838
|
+
fields.push(`effectMode: ${JSON.stringify(route.effectMode)}`);
|
|
2835
2839
|
if (idempotency !== undefined) {
|
|
2836
2840
|
fields.push(`idempotency: ${JSON.stringify(idempotency)}`);
|
|
2837
2841
|
}
|
|
@@ -4681,6 +4685,8 @@ var HEADER = "// GENERATED BY @supacloud/compiler — do not edit", INTERFACES =
|
|
|
4681
4685
|
maxAttempts?: number;
|
|
4682
4686
|
timeoutMs?: number;
|
|
4683
4687
|
};
|
|
4688
|
+
/** Simple route profile; no Effect interpreter or error mapping is required. */
|
|
4689
|
+
effectMode?: "simple";
|
|
4684
4690
|
/** Compile-time ownership and transport classification for the route boundary. */
|
|
4685
4691
|
contract?: {
|
|
4686
4692
|
body?: "framework" | "domain";
|
|
@@ -21488,111 +21494,102 @@ async function parseController(input, ctx) {
|
|
|
21488
21494
|
}
|
|
21489
21495
|
const effectExpr = getProp(optionsObject, "effect");
|
|
21490
21496
|
if (effectExpr) {
|
|
21491
|
-
|
|
21492
|
-
|
|
21493
|
-
ctx.diagnostics.push({
|
|
21494
|
-
severity: "error",
|
|
21495
|
-
code: "invalid-effect-contract",
|
|
21496
|
-
errorCode: "SC3030",
|
|
21497
|
-
docsUrl: "https://supacloud.dev/errors/SC3030",
|
|
21498
|
-
file,
|
|
21499
|
-
line: lineOf(effectExpr),
|
|
21500
|
-
message: `Route ${route.handler} effect must be a statically resolvable object literal.`,
|
|
21501
|
-
suggestion: 'Use effect: { required: true, errors: [...], dependencies: [...], retry: "none" }.'
|
|
21502
|
-
});
|
|
21497
|
+
if (effectExpr.kind === ts8.SyntaxKind.FalseKeyword) {
|
|
21498
|
+
route.effectMode = "simple";
|
|
21503
21499
|
} else {
|
|
21504
|
-
const
|
|
21505
|
-
|
|
21506
|
-
if (requiredExpr !== undefined && booleanProp(effectObject, "required") !== true) {
|
|
21500
|
+
const effectObject = await resolveStaticObjectLiteral(effectExpr, ctx);
|
|
21501
|
+
if (!effectObject) {
|
|
21507
21502
|
ctx.diagnostics.push({
|
|
21508
21503
|
severity: "error",
|
|
21509
21504
|
code: "invalid-effect-contract",
|
|
21510
21505
|
errorCode: "SC3030",
|
|
21511
21506
|
docsUrl: "https://supacloud.dev/errors/SC3030",
|
|
21512
21507
|
file,
|
|
21513
|
-
line: lineOf(
|
|
21514
|
-
message: `Route ${route.handler} effect
|
|
21508
|
+
line: lineOf(effectExpr),
|
|
21509
|
+
message: `Route ${route.handler} effect must be a statically resolvable object literal.`,
|
|
21510
|
+
suggestion: 'Use effect: { required: true, errors: [...], dependencies: [...], retry: "none" }.'
|
|
21515
21511
|
});
|
|
21516
|
-
}
|
|
21517
|
-
|
|
21518
|
-
|
|
21519
|
-
if (
|
|
21512
|
+
} else {
|
|
21513
|
+
const effect = { required: true };
|
|
21514
|
+
const requiredExpr = getProp(effectObject, "required");
|
|
21515
|
+
if (requiredExpr !== undefined && booleanProp(effectObject, "required") !== true) {
|
|
21520
21516
|
ctx.diagnostics.push({
|
|
21521
21517
|
severity: "error",
|
|
21522
|
-
code: "invalid-effect-
|
|
21523
|
-
errorCode: "
|
|
21524
|
-
docsUrl: "https://supacloud.dev/errors/
|
|
21518
|
+
code: "invalid-effect-contract",
|
|
21519
|
+
errorCode: "SC3030",
|
|
21520
|
+
docsUrl: "https://supacloud.dev/errors/SC3030",
|
|
21525
21521
|
file,
|
|
21526
|
-
line: lineOf(
|
|
21527
|
-
message: `Route ${route.handler} effect.
|
|
21522
|
+
line: lineOf(requiredExpr),
|
|
21523
|
+
message: `Route ${route.handler} effect.required must be the literal true.`
|
|
21528
21524
|
});
|
|
21529
|
-
} else {
|
|
21530
|
-
effect.retry = retry;
|
|
21531
21525
|
}
|
|
21532
|
-
|
|
21533
|
-
|
|
21534
|
-
|
|
21535
|
-
|
|
21536
|
-
|
|
21537
|
-
|
|
21538
|
-
|
|
21526
|
+
const retry = stringLiteralProp(effectObject, "retry");
|
|
21527
|
+
if (retry !== undefined) {
|
|
21528
|
+
if (retry !== "none" && retry !== "explicit") {
|
|
21529
|
+
ctx.diagnostics.push({
|
|
21530
|
+
severity: "error",
|
|
21531
|
+
code: "invalid-effect-retry",
|
|
21532
|
+
errorCode: "SC3031",
|
|
21533
|
+
docsUrl: "https://supacloud.dev/errors/SC3031",
|
|
21534
|
+
file,
|
|
21535
|
+
line: lineOf(getProp(effectObject, "retry")),
|
|
21536
|
+
message: `Route ${route.handler} effect.retry must be "none" or "explicit".`
|
|
21537
|
+
});
|
|
21538
|
+
} else {
|
|
21539
|
+
effect.retry = retry;
|
|
21540
|
+
}
|
|
21541
|
+
} else if (ctx.effectOptions.requireRetryPolicy === false) {
|
|
21542
|
+
effect.retry = "none";
|
|
21543
|
+
}
|
|
21544
|
+
const maxAttemptsExpr = getProp(effectObject, "maxAttempts");
|
|
21545
|
+
if (maxAttemptsExpr !== undefined) {
|
|
21546
|
+
const maxAttempts = numberLiteralProp(effectObject, "maxAttempts");
|
|
21547
|
+
if (maxAttempts === undefined || maxAttempts < 1 || !Number.isInteger(maxAttempts)) {
|
|
21548
|
+
ctx.diagnostics.push({
|
|
21549
|
+
severity: "error",
|
|
21550
|
+
code: "invalid-effect-retry",
|
|
21551
|
+
errorCode: "SC3031",
|
|
21552
|
+
docsUrl: "https://supacloud.dev/errors/SC3031",
|
|
21553
|
+
file,
|
|
21554
|
+
line: lineOf(maxAttemptsExpr),
|
|
21555
|
+
message: `Route ${route.handler} effect.maxAttempts must be a positive integer.`
|
|
21556
|
+
});
|
|
21557
|
+
} else {
|
|
21558
|
+
effect.maxAttempts = maxAttempts;
|
|
21559
|
+
}
|
|
21560
|
+
}
|
|
21561
|
+
if (effect.retry === "explicit" && effect.maxAttempts === undefined) {
|
|
21539
21562
|
ctx.diagnostics.push({
|
|
21540
21563
|
severity: "error",
|
|
21541
21564
|
code: "invalid-effect-retry",
|
|
21542
21565
|
errorCode: "SC3031",
|
|
21543
21566
|
docsUrl: "https://supacloud.dev/errors/SC3031",
|
|
21544
21567
|
file,
|
|
21545
|
-
line: lineOf(
|
|
21546
|
-
message: `Route ${route.handler} effect.maxAttempts
|
|
21568
|
+
line: lineOf(effectExpr),
|
|
21569
|
+
message: `Route ${route.handler} effect.maxAttempts is required when retry is "explicit".`
|
|
21547
21570
|
});
|
|
21548
|
-
} else {
|
|
21549
|
-
effect.maxAttempts = maxAttempts;
|
|
21550
21571
|
}
|
|
21551
|
-
|
|
21552
|
-
|
|
21553
|
-
|
|
21554
|
-
|
|
21555
|
-
|
|
21556
|
-
|
|
21557
|
-
|
|
21558
|
-
|
|
21559
|
-
|
|
21560
|
-
|
|
21561
|
-
|
|
21562
|
-
|
|
21563
|
-
|
|
21564
|
-
|
|
21565
|
-
|
|
21566
|
-
|
|
21567
|
-
ctx.diagnostics.push({
|
|
21568
|
-
severity: "error",
|
|
21569
|
-
code: "invalid-effect-timeout",
|
|
21570
|
-
errorCode: "SC3032",
|
|
21571
|
-
docsUrl: "https://supacloud.dev/errors/SC3032",
|
|
21572
|
-
file,
|
|
21573
|
-
line: lineOf(timeoutExpr),
|
|
21574
|
-
message: `Route ${route.handler} effect.timeoutMs must be a positive integer.`
|
|
21575
|
-
});
|
|
21576
|
-
} else {
|
|
21577
|
-
effect.timeoutMs = timeoutMs;
|
|
21572
|
+
const timeoutExpr = getProp(effectObject, "timeoutMs");
|
|
21573
|
+
if (timeoutExpr !== undefined) {
|
|
21574
|
+
const timeoutMs = numberLiteralProp(effectObject, "timeoutMs");
|
|
21575
|
+
if (timeoutMs === undefined || timeoutMs <= 0 || !Number.isInteger(timeoutMs)) {
|
|
21576
|
+
ctx.diagnostics.push({
|
|
21577
|
+
severity: "error",
|
|
21578
|
+
code: "invalid-effect-timeout",
|
|
21579
|
+
errorCode: "SC3032",
|
|
21580
|
+
docsUrl: "https://supacloud.dev/errors/SC3032",
|
|
21581
|
+
file,
|
|
21582
|
+
line: lineOf(timeoutExpr),
|
|
21583
|
+
message: `Route ${route.handler} effect.timeoutMs must be a positive integer.`
|
|
21584
|
+
});
|
|
21585
|
+
} else {
|
|
21586
|
+
effect.timeoutMs = timeoutMs;
|
|
21587
|
+
}
|
|
21578
21588
|
}
|
|
21579
|
-
|
|
21580
|
-
|
|
21581
|
-
|
|
21582
|
-
|
|
21583
|
-
if (!dependenciesArray || dependenciesArray.elements.some((element) => !ts8.isStringLiteral(element) || element.text.trim().length === 0)) {
|
|
21584
|
-
ctx.diagnostics.push({
|
|
21585
|
-
severity: "error",
|
|
21586
|
-
code: "invalid-effect-dependencies",
|
|
21587
|
-
errorCode: "SC3033",
|
|
21588
|
-
docsUrl: "https://supacloud.dev/errors/SC3033",
|
|
21589
|
-
file,
|
|
21590
|
-
line: lineOf(dependenciesExpr),
|
|
21591
|
-
message: `Route ${route.handler} effect.dependencies must be a string array or a const initialized with one.`
|
|
21592
|
-
});
|
|
21593
|
-
} else {
|
|
21594
|
-
effect.dependencies = dependenciesArray.elements.map((element) => element.text);
|
|
21595
|
-
if (new Set(effect.dependencies).size !== effect.dependencies.length) {
|
|
21589
|
+
const dependenciesExpr = getProp(effectObject, "dependencies");
|
|
21590
|
+
if (dependenciesExpr !== undefined) {
|
|
21591
|
+
const dependenciesArray = await resolveStaticArrayLiteral(dependenciesExpr, ctx);
|
|
21592
|
+
if (!dependenciesArray || dependenciesArray.elements.some((element) => !ts8.isStringLiteral(element) || element.text.trim().length === 0)) {
|
|
21596
21593
|
ctx.diagnostics.push({
|
|
21597
21594
|
severity: "error",
|
|
21598
21595
|
code: "invalid-effect-dependencies",
|
|
@@ -21600,148 +21597,161 @@ async function parseController(input, ctx) {
|
|
|
21600
21597
|
docsUrl: "https://supacloud.dev/errors/SC3033",
|
|
21601
21598
|
file,
|
|
21602
21599
|
line: lineOf(dependenciesExpr),
|
|
21603
|
-
message: `Route ${route.handler} effect.dependencies must
|
|
21604
|
-
suggestion: "List each logical Effect dependency exactly once."
|
|
21600
|
+
message: `Route ${route.handler} effect.dependencies must be a string array or a const initialized with one.`
|
|
21605
21601
|
});
|
|
21606
|
-
}
|
|
21607
|
-
|
|
21608
|
-
|
|
21609
|
-
const errorsExpr = getProp(effectObject, "errors");
|
|
21610
|
-
if (errorsExpr !== undefined) {
|
|
21611
|
-
const errorsArray = await resolveStaticArrayLiteral(errorsExpr, ctx);
|
|
21612
|
-
if (!errorsArray) {
|
|
21613
|
-
ctx.diagnostics.push({
|
|
21614
|
-
severity: "error",
|
|
21615
|
-
code: "invalid-effect-errors",
|
|
21616
|
-
errorCode: "SC3034",
|
|
21617
|
-
docsUrl: "https://supacloud.dev/errors/SC3034",
|
|
21618
|
-
file,
|
|
21619
|
-
line: lineOf(errorsExpr),
|
|
21620
|
-
message: `Route ${route.handler} effect.errors must be an object array or a const initialized with one.`
|
|
21621
|
-
});
|
|
21622
|
-
} else {
|
|
21623
|
-
const errors = [];
|
|
21624
|
-
for (const errorExpr of errorsArray.elements) {
|
|
21625
|
-
if (!ts8.isObjectLiteralExpression(errorExpr)) {
|
|
21602
|
+
} else {
|
|
21603
|
+
effect.dependencies = dependenciesArray.elements.map((element) => element.text);
|
|
21604
|
+
if (new Set(effect.dependencies).size !== effect.dependencies.length) {
|
|
21626
21605
|
ctx.diagnostics.push({
|
|
21627
21606
|
severity: "error",
|
|
21628
|
-
code: "invalid-effect-
|
|
21629
|
-
errorCode: "
|
|
21630
|
-
docsUrl: "https://supacloud.dev/errors/
|
|
21607
|
+
code: "invalid-effect-dependencies",
|
|
21608
|
+
errorCode: "SC3033",
|
|
21609
|
+
docsUrl: "https://supacloud.dev/errors/SC3033",
|
|
21631
21610
|
file,
|
|
21632
|
-
line: lineOf(
|
|
21633
|
-
message: `Route ${route.handler} effect.
|
|
21611
|
+
line: lineOf(dependenciesExpr),
|
|
21612
|
+
message: `Route ${route.handler} effect.dependencies must not contain duplicate service names.`,
|
|
21613
|
+
suggestion: "List each logical Effect dependency exactly once."
|
|
21634
21614
|
});
|
|
21635
|
-
continue;
|
|
21636
21615
|
}
|
|
21637
|
-
|
|
21638
|
-
|
|
21639
|
-
|
|
21640
|
-
|
|
21641
|
-
|
|
21642
|
-
|
|
21643
|
-
|
|
21644
|
-
|
|
21645
|
-
|
|
21646
|
-
|
|
21647
|
-
|
|
21648
|
-
|
|
21649
|
-
|
|
21650
|
-
|
|
21651
|
-
|
|
21652
|
-
|
|
21653
|
-
|
|
21654
|
-
|
|
21616
|
+
}
|
|
21617
|
+
}
|
|
21618
|
+
const errorsExpr = getProp(effectObject, "errors");
|
|
21619
|
+
if (errorsExpr !== undefined) {
|
|
21620
|
+
const errorsArray = await resolveStaticArrayLiteral(errorsExpr, ctx);
|
|
21621
|
+
if (!errorsArray) {
|
|
21622
|
+
ctx.diagnostics.push({
|
|
21623
|
+
severity: "error",
|
|
21624
|
+
code: "invalid-effect-errors",
|
|
21625
|
+
errorCode: "SC3034",
|
|
21626
|
+
docsUrl: "https://supacloud.dev/errors/SC3034",
|
|
21627
|
+
file,
|
|
21628
|
+
line: lineOf(errorsExpr),
|
|
21629
|
+
message: `Route ${route.handler} effect.errors must be an object array or a const initialized with one.`
|
|
21630
|
+
});
|
|
21631
|
+
} else {
|
|
21632
|
+
const errors = [];
|
|
21633
|
+
for (const errorExpr of errorsArray.elements) {
|
|
21634
|
+
if (!ts8.isObjectLiteralExpression(errorExpr)) {
|
|
21635
|
+
ctx.diagnostics.push({
|
|
21636
|
+
severity: "error",
|
|
21637
|
+
code: "invalid-effect-errors",
|
|
21638
|
+
errorCode: "SC3034",
|
|
21639
|
+
docsUrl: "https://supacloud.dev/errors/SC3034",
|
|
21640
|
+
file,
|
|
21641
|
+
line: lineOf(errorExpr),
|
|
21642
|
+
message: `Route ${route.handler} effect.errors entries must be object literals.`
|
|
21643
|
+
});
|
|
21644
|
+
continue;
|
|
21645
|
+
}
|
|
21646
|
+
const tag = stringLiteralProp(errorExpr, "tag");
|
|
21647
|
+
const code = stringLiteralProp(errorExpr, "code");
|
|
21648
|
+
const status = numberLiteralProp(errorExpr, "status");
|
|
21649
|
+
const message = stringLiteralProp(errorExpr, "message");
|
|
21650
|
+
const codeFromError = booleanLiteralProp(errorExpr, "codeFromError");
|
|
21651
|
+
const messageFromError = booleanLiteralProp(errorExpr, "messageFromError");
|
|
21652
|
+
const statusFromError = booleanLiteralProp(errorExpr, "statusFromError");
|
|
21653
|
+
const retryable = booleanLiteralProp(errorExpr, "retryable");
|
|
21654
|
+
const outcomeUnknown = booleanLiteralProp(errorExpr, "outcomeUnknown");
|
|
21655
|
+
if (invalidBooleanProp(errorExpr, "codeFromError") || invalidBooleanProp(errorExpr, "messageFromError") || invalidBooleanProp(errorExpr, "statusFromError") || invalidBooleanProp(errorExpr, "retryable") || invalidBooleanProp(errorExpr, "outcomeUnknown")) {
|
|
21656
|
+
ctx.diagnostics.push({
|
|
21657
|
+
severity: "error",
|
|
21658
|
+
code: "invalid-effect-errors",
|
|
21659
|
+
errorCode: "SC3034",
|
|
21660
|
+
docsUrl: "https://supacloud.dev/errors/SC3034",
|
|
21661
|
+
file,
|
|
21662
|
+
line: lineOf(errorExpr),
|
|
21663
|
+
message: `Route ${route.handler} effect.errors instance flags must be boolean literals.`
|
|
21664
|
+
});
|
|
21665
|
+
continue;
|
|
21666
|
+
}
|
|
21667
|
+
if (!tag || !code || status === undefined || !Number.isInteger(status) || status < 400 || status > 599) {
|
|
21668
|
+
ctx.diagnostics.push({
|
|
21669
|
+
severity: "error",
|
|
21670
|
+
code: "invalid-effect-errors",
|
|
21671
|
+
errorCode: "SC3034",
|
|
21672
|
+
docsUrl: "https://supacloud.dev/errors/SC3034",
|
|
21673
|
+
file,
|
|
21674
|
+
line: lineOf(errorExpr),
|
|
21675
|
+
message: `Route ${route.handler} effect.errors entries require tag, code and an HTTP status from 400 to 599.`
|
|
21676
|
+
});
|
|
21677
|
+
continue;
|
|
21678
|
+
}
|
|
21679
|
+
errors.push({
|
|
21680
|
+
tag,
|
|
21681
|
+
code,
|
|
21682
|
+
status,
|
|
21683
|
+
...message === undefined ? {} : { message },
|
|
21684
|
+
...codeFromError === undefined ? {} : { codeFromError },
|
|
21685
|
+
...messageFromError === undefined ? {} : { messageFromError },
|
|
21686
|
+
...statusFromError === undefined ? {} : { statusFromError },
|
|
21687
|
+
...retryable === undefined ? {} : { retryable },
|
|
21688
|
+
...outcomeUnknown === undefined ? {} : { outcomeUnknown }
|
|
21655
21689
|
});
|
|
21656
|
-
continue;
|
|
21657
21690
|
}
|
|
21658
|
-
|
|
21691
|
+
effect.errors = errors;
|
|
21692
|
+
if (new Set(errors.map((entry) => entry.tag)).size !== errors.length) {
|
|
21659
21693
|
ctx.diagnostics.push({
|
|
21660
21694
|
severity: "error",
|
|
21661
21695
|
code: "invalid-effect-errors",
|
|
21662
21696
|
errorCode: "SC3034",
|
|
21663
21697
|
docsUrl: "https://supacloud.dev/errors/SC3034",
|
|
21664
21698
|
file,
|
|
21665
|
-
line: lineOf(
|
|
21666
|
-
message: `Route ${route.handler} effect.errors
|
|
21699
|
+
line: lineOf(errorsExpr),
|
|
21700
|
+
message: `Route ${route.handler} effect.errors must not contain duplicate tags.`,
|
|
21701
|
+
suggestion: "Map each tagged failure exactly once."
|
|
21667
21702
|
});
|
|
21668
|
-
continue;
|
|
21669
21703
|
}
|
|
21670
|
-
errors.push({
|
|
21671
|
-
tag,
|
|
21672
|
-
code,
|
|
21673
|
-
status,
|
|
21674
|
-
...message === undefined ? {} : { message },
|
|
21675
|
-
...codeFromError === undefined ? {} : { codeFromError },
|
|
21676
|
-
...messageFromError === undefined ? {} : { messageFromError },
|
|
21677
|
-
...statusFromError === undefined ? {} : { statusFromError },
|
|
21678
|
-
...retryable === undefined ? {} : { retryable },
|
|
21679
|
-
...outcomeUnknown === undefined ? {} : { outcomeUnknown }
|
|
21680
|
-
});
|
|
21681
|
-
}
|
|
21682
|
-
effect.errors = errors;
|
|
21683
|
-
if (new Set(errors.map((entry) => entry.tag)).size !== errors.length) {
|
|
21684
|
-
ctx.diagnostics.push({
|
|
21685
|
-
severity: "error",
|
|
21686
|
-
code: "invalid-effect-errors",
|
|
21687
|
-
errorCode: "SC3034",
|
|
21688
|
-
docsUrl: "https://supacloud.dev/errors/SC3034",
|
|
21689
|
-
file,
|
|
21690
|
-
line: lineOf(errorsExpr),
|
|
21691
|
-
message: `Route ${route.handler} effect.errors must not contain duplicate tags.`,
|
|
21692
|
-
suggestion: "Map each tagged failure exactly once."
|
|
21693
|
-
});
|
|
21694
21704
|
}
|
|
21695
21705
|
}
|
|
21696
|
-
|
|
21697
|
-
|
|
21698
|
-
|
|
21699
|
-
|
|
21700
|
-
|
|
21701
|
-
|
|
21702
|
-
|
|
21703
|
-
|
|
21704
|
-
|
|
21705
|
-
|
|
21706
|
-
|
|
21707
|
-
|
|
21708
|
-
|
|
21709
|
-
|
|
21710
|
-
|
|
21711
|
-
|
|
21712
|
-
});
|
|
21713
|
-
}
|
|
21714
|
-
if (effectArguments) {
|
|
21715
|
-
const [, failureType, environmentType] = effectArguments;
|
|
21716
|
-
const failure = await effectFailureTags(failureType, method, ctx);
|
|
21717
|
-
if (ctx.effectOptions.requireTaggedErrorTypes && failure.invalid) {
|
|
21718
|
-
pushEffectDiagnostic(ctx, "effect-error-type", file, lineOf(method), `Route ${route.handler} must use never or a tagged failure union in Effect's error channel, not ${await effectTypeName(failureType, method, ctx)}.`, "Replace unknown/any with never or a union of objects carrying a literal _tag.");
|
|
21719
|
-
} else if (ctx.effectOptions.requireTaggedErrorTypes && effect.errors !== undefined && !failure.invalid) {
|
|
21720
|
-
const declaredTags = sortedUnique(effect.errors.map((entry) => entry.tag));
|
|
21721
|
-
const typedTags = sortedUnique(failure.tags);
|
|
21722
|
-
if (declaredTags.join("\x00") !== typedTags.join("\x00")) {
|
|
21723
|
-
pushEffectDiagnostic(ctx, "effect-error-mapping-type", file, lineOf(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.");
|
|
21724
|
-
}
|
|
21706
|
+
route.effect = effect;
|
|
21707
|
+
const effectArguments = await effectTypeArgumentsOf(returnType, ctx);
|
|
21708
|
+
const promiseReturn = Boolean(resultType && /\bPromise(?:<|\s)/.test(resultType));
|
|
21709
|
+
if (promiseReturn) {
|
|
21710
|
+
pushEffectDiagnostic(ctx, "effect-promise-return", file, lineOf(method), `Route ${route.handler} returns Promise instead of an Effect program.`, "Return Effect.Effect<Success, Failure, Requirements>; the Elysia boundary owns Promise interpretation.");
|
|
21711
|
+
} else if (!effectArguments) {
|
|
21712
|
+
ctx.diagnostics.push({
|
|
21713
|
+
severity: "error",
|
|
21714
|
+
code: "effect-return-type",
|
|
21715
|
+
errorCode: "SC3035",
|
|
21716
|
+
docsUrl: "https://supacloud.dev/errors/SC3035",
|
|
21717
|
+
file,
|
|
21718
|
+
line: lineOf(method),
|
|
21719
|
+
message: `Route ${route.handler} declares an Effect contract but returns ${resultType ?? "an unresolved type"}.`,
|
|
21720
|
+
suggestion: "Return Effect.Effect<Success, Failure, Requirements> from the handler."
|
|
21721
|
+
});
|
|
21725
21722
|
}
|
|
21726
|
-
|
|
21727
|
-
|
|
21728
|
-
|
|
21729
|
-
|
|
21730
|
-
|
|
21731
|
-
|
|
21732
|
-
const
|
|
21733
|
-
|
|
21734
|
-
|
|
21723
|
+
if (effectArguments) {
|
|
21724
|
+
const [, failureType, environmentType] = effectArguments;
|
|
21725
|
+
const failure = await effectFailureTags(failureType, method, ctx);
|
|
21726
|
+
if (ctx.effectOptions.requireTaggedErrorTypes && failure.invalid) {
|
|
21727
|
+
pushEffectDiagnostic(ctx, "effect-error-type", file, lineOf(method), `Route ${route.handler} must use never or a tagged failure union in Effect's error channel, not ${await effectTypeName(failureType, method, ctx)}.`, "Replace unknown/any with never or a union of objects carrying a literal _tag.");
|
|
21728
|
+
} else if (ctx.effectOptions.requireTaggedErrorTypes && effect.errors !== undefined && !failure.invalid) {
|
|
21729
|
+
const declaredTags = sortedUnique(effect.errors.map((entry) => entry.tag));
|
|
21730
|
+
const typedTags = sortedUnique(failure.tags);
|
|
21731
|
+
if (declaredTags.join("\x00") !== typedTags.join("\x00")) {
|
|
21732
|
+
pushEffectDiagnostic(ctx, "effect-error-mapping-type", file, lineOf(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.");
|
|
21735
21733
|
}
|
|
21736
21734
|
}
|
|
21735
|
+
const environment = await effectEnvironmentNames(environmentType, method, ctx);
|
|
21736
|
+
if (ctx.effectOptions.requireExactDependencyTypes && effect.dependencies !== undefined) {
|
|
21737
|
+
if (environment.invalid) {
|
|
21738
|
+
pushEffectDiagnostic(ctx, "effect-dependency-mismatch", file, lineOf(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.");
|
|
21739
|
+
} else {
|
|
21740
|
+
const declaredDependencies = sortedUnique(effect.dependencies);
|
|
21741
|
+
const typedDependencies = sortedUnique(environment.names);
|
|
21742
|
+
if (declaredDependencies.join("\x00") !== typedDependencies.join("\x00")) {
|
|
21743
|
+
pushEffectDiagnostic(ctx, "effect-dependency-mismatch", file, lineOf(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.");
|
|
21744
|
+
}
|
|
21745
|
+
}
|
|
21746
|
+
}
|
|
21747
|
+
if (ctx.effectOptions.requireTimeoutForDependencies && (effect.dependencies?.length ?? 0) > 0 && effect.timeoutMs === undefined) {
|
|
21748
|
+
pushEffectDiagnostic(ctx, "effect-timeout-required", file, lineOf(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.");
|
|
21749
|
+
}
|
|
21737
21750
|
}
|
|
21738
|
-
if (ctx.effectOptions.
|
|
21739
|
-
|
|
21740
|
-
|
|
21741
|
-
|
|
21742
|
-
if (ctx.effectOptions.forbidDirectThrows && method.body) {
|
|
21743
|
-
for (const throwStatement of descendantsOfKind(method.body, ts8.isThrowStatement)) {
|
|
21744
|
-
pushEffectDiagnostic(ctx, "effect-direct-throw", file, lineOf(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.");
|
|
21751
|
+
if (ctx.effectOptions.forbidDirectThrows && method.body) {
|
|
21752
|
+
for (const throwStatement of descendantsOfKind(method.body, ts8.isThrowStatement)) {
|
|
21753
|
+
pushEffectDiagnostic(ctx, "effect-direct-throw", file, lineOf(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.");
|
|
21754
|
+
}
|
|
21745
21755
|
}
|
|
21746
21756
|
}
|
|
21747
21757
|
}
|
|
@@ -23375,6 +23385,14 @@ function validateEffectPolicies(graph, options = {}) {
|
|
|
23375
23385
|
for (const controller of module.controllers) {
|
|
23376
23386
|
for (const route of controller.routes) {
|
|
23377
23387
|
const label = routeLabel(route, controller.path);
|
|
23388
|
+
if (route.effectMode === "simple") {
|
|
23389
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
23390
|
+
const commandRef = route.command ?? featureTransition?.command;
|
|
23391
|
+
if (commandRef !== undefined || !["GET", "HEAD", "OPTIONS"].includes(route.method)) {
|
|
23392
|
+
diagnostics.push(diagnostic("effect-contract-required", commandRef === undefined ? `Route ${label} cannot use effect: false for a write operation.` : `Route ${label} cannot use effect: false when it enters command ${commandRef}.`, controller.file, "Keep Effect enabled for writes and durable commands; effect: false is only for simple read routes."));
|
|
23393
|
+
}
|
|
23394
|
+
continue;
|
|
23395
|
+
}
|
|
23378
23396
|
if (options.requireRouteEffects && route.effect === undefined) {
|
|
23379
23397
|
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.'));
|
|
23380
23398
|
continue;
|
|
@@ -26432,10 +26450,10 @@ import { readFileSync as readFileSync4 } from "node:fs";
|
|
|
26432
26450
|
import { resolve as resolve19 } from "node:path";
|
|
26433
26451
|
|
|
26434
26452
|
// src/migration-app-version.ts
|
|
26435
|
-
var MIGRATION_APP_VERSION = "0.
|
|
26453
|
+
var MIGRATION_APP_VERSION = "0.39.0";
|
|
26436
26454
|
|
|
26437
26455
|
// src/migration-elysia-version.ts
|
|
26438
|
-
var MIGRATION_ELYSIA_VERSION = "0.
|
|
26456
|
+
var MIGRATION_ELYSIA_VERSION = "0.41.0";
|
|
26439
26457
|
|
|
26440
26458
|
// src/migration-policy.ts
|
|
26441
26459
|
var cachedCompilerVersion;
|
package/dist/index.js
CHANGED
|
@@ -381,6 +381,7 @@ var init_di_resolution = __esm(() => {
|
|
|
381
381
|
"conflicting-injection-flags",
|
|
382
382
|
"cross-module-scoped-dependency",
|
|
383
383
|
"mixed-multi-provider-scopes",
|
|
384
|
+
"effect-contract-required",
|
|
384
385
|
"dynamic-aspect-reference",
|
|
385
386
|
"invalid-aspect-reference",
|
|
386
387
|
"aspect-operation-mismatch"
|
|
@@ -2452,6 +2453,7 @@ function renderApplication(graph, options) {
|
|
|
2452
2453
|
executor: route.command === undefined ? "route" : "command",
|
|
2453
2454
|
...command === undefined ? {} : { commandBoundary: route.command === undefined ? "handler" : "route" },
|
|
2454
2455
|
...route.effect === undefined ? {} : { effect: route.effect },
|
|
2456
|
+
...route.effectMode === undefined ? {} : { effectMode: route.effectMode },
|
|
2455
2457
|
invoker: isPipeline ? "pipeline" : "direct",
|
|
2456
2458
|
stages: operationPlans.find((plan) => plan.module === module.name && plan.kind === "route" && plan.name === `${route.method} ${joinRoutePaths(controller.path, route.path)}`).stages,
|
|
2457
2459
|
...commandRef === undefined ? {} : { command: command?.name ?? commandRef },
|
|
@@ -2831,6 +2833,8 @@ class ModuleGenerator {
|
|
|
2831
2833
|
if (route.effect !== undefined) {
|
|
2832
2834
|
fields.push(`effect: ${JSON.stringify(route.effect)}`);
|
|
2833
2835
|
}
|
|
2836
|
+
if (route.effectMode !== undefined)
|
|
2837
|
+
fields.push(`effectMode: ${JSON.stringify(route.effectMode)}`);
|
|
2834
2838
|
if (idempotency !== undefined) {
|
|
2835
2839
|
fields.push(`idempotency: ${JSON.stringify(idempotency)}`);
|
|
2836
2840
|
}
|
|
@@ -4680,6 +4684,8 @@ var HEADER = "// GENERATED BY @supacloud/compiler — do not edit", INTERFACES =
|
|
|
4680
4684
|
maxAttempts?: number;
|
|
4681
4685
|
timeoutMs?: number;
|
|
4682
4686
|
};
|
|
4687
|
+
/** Simple route profile; no Effect interpreter or error mapping is required. */
|
|
4688
|
+
effectMode?: "simple";
|
|
4683
4689
|
/** Compile-time ownership and transport classification for the route boundary. */
|
|
4684
4690
|
contract?: {
|
|
4685
4691
|
body?: "framework" | "domain";
|
|
@@ -21447,111 +21453,102 @@ async function parseController(input, ctx) {
|
|
|
21447
21453
|
}
|
|
21448
21454
|
const effectExpr = getProp(optionsObject, "effect");
|
|
21449
21455
|
if (effectExpr) {
|
|
21450
|
-
|
|
21451
|
-
|
|
21452
|
-
ctx.diagnostics.push({
|
|
21453
|
-
severity: "error",
|
|
21454
|
-
code: "invalid-effect-contract",
|
|
21455
|
-
errorCode: "SC3030",
|
|
21456
|
-
docsUrl: "https://supacloud.dev/errors/SC3030",
|
|
21457
|
-
file,
|
|
21458
|
-
line: lineOf(effectExpr),
|
|
21459
|
-
message: `Route ${route.handler} effect must be a statically resolvable object literal.`,
|
|
21460
|
-
suggestion: 'Use effect: { required: true, errors: [...], dependencies: [...], retry: "none" }.'
|
|
21461
|
-
});
|
|
21456
|
+
if (effectExpr.kind === ts8.SyntaxKind.FalseKeyword) {
|
|
21457
|
+
route.effectMode = "simple";
|
|
21462
21458
|
} else {
|
|
21463
|
-
const
|
|
21464
|
-
|
|
21465
|
-
if (requiredExpr !== undefined && booleanProp(effectObject, "required") !== true) {
|
|
21459
|
+
const effectObject = await resolveStaticObjectLiteral(effectExpr, ctx);
|
|
21460
|
+
if (!effectObject) {
|
|
21466
21461
|
ctx.diagnostics.push({
|
|
21467
21462
|
severity: "error",
|
|
21468
21463
|
code: "invalid-effect-contract",
|
|
21469
21464
|
errorCode: "SC3030",
|
|
21470
21465
|
docsUrl: "https://supacloud.dev/errors/SC3030",
|
|
21471
21466
|
file,
|
|
21472
|
-
line: lineOf(
|
|
21473
|
-
message: `Route ${route.handler} effect
|
|
21467
|
+
line: lineOf(effectExpr),
|
|
21468
|
+
message: `Route ${route.handler} effect must be a statically resolvable object literal.`,
|
|
21469
|
+
suggestion: 'Use effect: { required: true, errors: [...], dependencies: [...], retry: "none" }.'
|
|
21474
21470
|
});
|
|
21475
|
-
}
|
|
21476
|
-
|
|
21477
|
-
|
|
21478
|
-
if (
|
|
21471
|
+
} else {
|
|
21472
|
+
const effect = { required: true };
|
|
21473
|
+
const requiredExpr = getProp(effectObject, "required");
|
|
21474
|
+
if (requiredExpr !== undefined && booleanProp(effectObject, "required") !== true) {
|
|
21479
21475
|
ctx.diagnostics.push({
|
|
21480
21476
|
severity: "error",
|
|
21481
|
-
code: "invalid-effect-
|
|
21482
|
-
errorCode: "
|
|
21483
|
-
docsUrl: "https://supacloud.dev/errors/
|
|
21477
|
+
code: "invalid-effect-contract",
|
|
21478
|
+
errorCode: "SC3030",
|
|
21479
|
+
docsUrl: "https://supacloud.dev/errors/SC3030",
|
|
21484
21480
|
file,
|
|
21485
|
-
line: lineOf(
|
|
21486
|
-
message: `Route ${route.handler} effect.
|
|
21481
|
+
line: lineOf(requiredExpr),
|
|
21482
|
+
message: `Route ${route.handler} effect.required must be the literal true.`
|
|
21487
21483
|
});
|
|
21488
|
-
} else {
|
|
21489
|
-
effect.retry = retry;
|
|
21490
21484
|
}
|
|
21491
|
-
|
|
21492
|
-
|
|
21493
|
-
|
|
21494
|
-
|
|
21495
|
-
|
|
21496
|
-
|
|
21497
|
-
|
|
21485
|
+
const retry = stringLiteralProp(effectObject, "retry");
|
|
21486
|
+
if (retry !== undefined) {
|
|
21487
|
+
if (retry !== "none" && retry !== "explicit") {
|
|
21488
|
+
ctx.diagnostics.push({
|
|
21489
|
+
severity: "error",
|
|
21490
|
+
code: "invalid-effect-retry",
|
|
21491
|
+
errorCode: "SC3031",
|
|
21492
|
+
docsUrl: "https://supacloud.dev/errors/SC3031",
|
|
21493
|
+
file,
|
|
21494
|
+
line: lineOf(getProp(effectObject, "retry")),
|
|
21495
|
+
message: `Route ${route.handler} effect.retry must be "none" or "explicit".`
|
|
21496
|
+
});
|
|
21497
|
+
} else {
|
|
21498
|
+
effect.retry = retry;
|
|
21499
|
+
}
|
|
21500
|
+
} else if (ctx.effectOptions.requireRetryPolicy === false) {
|
|
21501
|
+
effect.retry = "none";
|
|
21502
|
+
}
|
|
21503
|
+
const maxAttemptsExpr = getProp(effectObject, "maxAttempts");
|
|
21504
|
+
if (maxAttemptsExpr !== undefined) {
|
|
21505
|
+
const maxAttempts = numberLiteralProp(effectObject, "maxAttempts");
|
|
21506
|
+
if (maxAttempts === undefined || maxAttempts < 1 || !Number.isInteger(maxAttempts)) {
|
|
21507
|
+
ctx.diagnostics.push({
|
|
21508
|
+
severity: "error",
|
|
21509
|
+
code: "invalid-effect-retry",
|
|
21510
|
+
errorCode: "SC3031",
|
|
21511
|
+
docsUrl: "https://supacloud.dev/errors/SC3031",
|
|
21512
|
+
file,
|
|
21513
|
+
line: lineOf(maxAttemptsExpr),
|
|
21514
|
+
message: `Route ${route.handler} effect.maxAttempts must be a positive integer.`
|
|
21515
|
+
});
|
|
21516
|
+
} else {
|
|
21517
|
+
effect.maxAttempts = maxAttempts;
|
|
21518
|
+
}
|
|
21519
|
+
}
|
|
21520
|
+
if (effect.retry === "explicit" && effect.maxAttempts === undefined) {
|
|
21498
21521
|
ctx.diagnostics.push({
|
|
21499
21522
|
severity: "error",
|
|
21500
21523
|
code: "invalid-effect-retry",
|
|
21501
21524
|
errorCode: "SC3031",
|
|
21502
21525
|
docsUrl: "https://supacloud.dev/errors/SC3031",
|
|
21503
21526
|
file,
|
|
21504
|
-
line: lineOf(
|
|
21505
|
-
message: `Route ${route.handler} effect.maxAttempts
|
|
21527
|
+
line: lineOf(effectExpr),
|
|
21528
|
+
message: `Route ${route.handler} effect.maxAttempts is required when retry is "explicit".`
|
|
21506
21529
|
});
|
|
21507
|
-
} else {
|
|
21508
|
-
effect.maxAttempts = maxAttempts;
|
|
21509
21530
|
}
|
|
21510
|
-
|
|
21511
|
-
|
|
21512
|
-
|
|
21513
|
-
|
|
21514
|
-
|
|
21515
|
-
|
|
21516
|
-
|
|
21517
|
-
|
|
21518
|
-
|
|
21519
|
-
|
|
21520
|
-
|
|
21521
|
-
|
|
21522
|
-
|
|
21523
|
-
|
|
21524
|
-
|
|
21525
|
-
|
|
21526
|
-
ctx.diagnostics.push({
|
|
21527
|
-
severity: "error",
|
|
21528
|
-
code: "invalid-effect-timeout",
|
|
21529
|
-
errorCode: "SC3032",
|
|
21530
|
-
docsUrl: "https://supacloud.dev/errors/SC3032",
|
|
21531
|
-
file,
|
|
21532
|
-
line: lineOf(timeoutExpr),
|
|
21533
|
-
message: `Route ${route.handler} effect.timeoutMs must be a positive integer.`
|
|
21534
|
-
});
|
|
21535
|
-
} else {
|
|
21536
|
-
effect.timeoutMs = timeoutMs;
|
|
21531
|
+
const timeoutExpr = getProp(effectObject, "timeoutMs");
|
|
21532
|
+
if (timeoutExpr !== undefined) {
|
|
21533
|
+
const timeoutMs = numberLiteralProp(effectObject, "timeoutMs");
|
|
21534
|
+
if (timeoutMs === undefined || timeoutMs <= 0 || !Number.isInteger(timeoutMs)) {
|
|
21535
|
+
ctx.diagnostics.push({
|
|
21536
|
+
severity: "error",
|
|
21537
|
+
code: "invalid-effect-timeout",
|
|
21538
|
+
errorCode: "SC3032",
|
|
21539
|
+
docsUrl: "https://supacloud.dev/errors/SC3032",
|
|
21540
|
+
file,
|
|
21541
|
+
line: lineOf(timeoutExpr),
|
|
21542
|
+
message: `Route ${route.handler} effect.timeoutMs must be a positive integer.`
|
|
21543
|
+
});
|
|
21544
|
+
} else {
|
|
21545
|
+
effect.timeoutMs = timeoutMs;
|
|
21546
|
+
}
|
|
21537
21547
|
}
|
|
21538
|
-
|
|
21539
|
-
|
|
21540
|
-
|
|
21541
|
-
|
|
21542
|
-
if (!dependenciesArray || dependenciesArray.elements.some((element) => !ts8.isStringLiteral(element) || element.text.trim().length === 0)) {
|
|
21543
|
-
ctx.diagnostics.push({
|
|
21544
|
-
severity: "error",
|
|
21545
|
-
code: "invalid-effect-dependencies",
|
|
21546
|
-
errorCode: "SC3033",
|
|
21547
|
-
docsUrl: "https://supacloud.dev/errors/SC3033",
|
|
21548
|
-
file,
|
|
21549
|
-
line: lineOf(dependenciesExpr),
|
|
21550
|
-
message: `Route ${route.handler} effect.dependencies must be a string array or a const initialized with one.`
|
|
21551
|
-
});
|
|
21552
|
-
} else {
|
|
21553
|
-
effect.dependencies = dependenciesArray.elements.map((element) => element.text);
|
|
21554
|
-
if (new Set(effect.dependencies).size !== effect.dependencies.length) {
|
|
21548
|
+
const dependenciesExpr = getProp(effectObject, "dependencies");
|
|
21549
|
+
if (dependenciesExpr !== undefined) {
|
|
21550
|
+
const dependenciesArray = await resolveStaticArrayLiteral(dependenciesExpr, ctx);
|
|
21551
|
+
if (!dependenciesArray || dependenciesArray.elements.some((element) => !ts8.isStringLiteral(element) || element.text.trim().length === 0)) {
|
|
21555
21552
|
ctx.diagnostics.push({
|
|
21556
21553
|
severity: "error",
|
|
21557
21554
|
code: "invalid-effect-dependencies",
|
|
@@ -21559,148 +21556,161 @@ async function parseController(input, ctx) {
|
|
|
21559
21556
|
docsUrl: "https://supacloud.dev/errors/SC3033",
|
|
21560
21557
|
file,
|
|
21561
21558
|
line: lineOf(dependenciesExpr),
|
|
21562
|
-
message: `Route ${route.handler} effect.dependencies must
|
|
21563
|
-
suggestion: "List each logical Effect dependency exactly once."
|
|
21559
|
+
message: `Route ${route.handler} effect.dependencies must be a string array or a const initialized with one.`
|
|
21564
21560
|
});
|
|
21565
|
-
}
|
|
21566
|
-
|
|
21567
|
-
|
|
21568
|
-
const errorsExpr = getProp(effectObject, "errors");
|
|
21569
|
-
if (errorsExpr !== undefined) {
|
|
21570
|
-
const errorsArray = await resolveStaticArrayLiteral(errorsExpr, ctx);
|
|
21571
|
-
if (!errorsArray) {
|
|
21572
|
-
ctx.diagnostics.push({
|
|
21573
|
-
severity: "error",
|
|
21574
|
-
code: "invalid-effect-errors",
|
|
21575
|
-
errorCode: "SC3034",
|
|
21576
|
-
docsUrl: "https://supacloud.dev/errors/SC3034",
|
|
21577
|
-
file,
|
|
21578
|
-
line: lineOf(errorsExpr),
|
|
21579
|
-
message: `Route ${route.handler} effect.errors must be an object array or a const initialized with one.`
|
|
21580
|
-
});
|
|
21581
|
-
} else {
|
|
21582
|
-
const errors = [];
|
|
21583
|
-
for (const errorExpr of errorsArray.elements) {
|
|
21584
|
-
if (!ts8.isObjectLiteralExpression(errorExpr)) {
|
|
21561
|
+
} else {
|
|
21562
|
+
effect.dependencies = dependenciesArray.elements.map((element) => element.text);
|
|
21563
|
+
if (new Set(effect.dependencies).size !== effect.dependencies.length) {
|
|
21585
21564
|
ctx.diagnostics.push({
|
|
21586
21565
|
severity: "error",
|
|
21587
|
-
code: "invalid-effect-
|
|
21588
|
-
errorCode: "
|
|
21589
|
-
docsUrl: "https://supacloud.dev/errors/
|
|
21566
|
+
code: "invalid-effect-dependencies",
|
|
21567
|
+
errorCode: "SC3033",
|
|
21568
|
+
docsUrl: "https://supacloud.dev/errors/SC3033",
|
|
21590
21569
|
file,
|
|
21591
|
-
line: lineOf(
|
|
21592
|
-
message: `Route ${route.handler} effect.
|
|
21570
|
+
line: lineOf(dependenciesExpr),
|
|
21571
|
+
message: `Route ${route.handler} effect.dependencies must not contain duplicate service names.`,
|
|
21572
|
+
suggestion: "List each logical Effect dependency exactly once."
|
|
21593
21573
|
});
|
|
21594
|
-
continue;
|
|
21595
21574
|
}
|
|
21596
|
-
|
|
21597
|
-
|
|
21598
|
-
|
|
21599
|
-
|
|
21600
|
-
|
|
21601
|
-
|
|
21602
|
-
|
|
21603
|
-
|
|
21604
|
-
|
|
21605
|
-
|
|
21606
|
-
|
|
21607
|
-
|
|
21608
|
-
|
|
21609
|
-
|
|
21610
|
-
|
|
21611
|
-
|
|
21612
|
-
|
|
21613
|
-
|
|
21575
|
+
}
|
|
21576
|
+
}
|
|
21577
|
+
const errorsExpr = getProp(effectObject, "errors");
|
|
21578
|
+
if (errorsExpr !== undefined) {
|
|
21579
|
+
const errorsArray = await resolveStaticArrayLiteral(errorsExpr, ctx);
|
|
21580
|
+
if (!errorsArray) {
|
|
21581
|
+
ctx.diagnostics.push({
|
|
21582
|
+
severity: "error",
|
|
21583
|
+
code: "invalid-effect-errors",
|
|
21584
|
+
errorCode: "SC3034",
|
|
21585
|
+
docsUrl: "https://supacloud.dev/errors/SC3034",
|
|
21586
|
+
file,
|
|
21587
|
+
line: lineOf(errorsExpr),
|
|
21588
|
+
message: `Route ${route.handler} effect.errors must be an object array or a const initialized with one.`
|
|
21589
|
+
});
|
|
21590
|
+
} else {
|
|
21591
|
+
const errors = [];
|
|
21592
|
+
for (const errorExpr of errorsArray.elements) {
|
|
21593
|
+
if (!ts8.isObjectLiteralExpression(errorExpr)) {
|
|
21594
|
+
ctx.diagnostics.push({
|
|
21595
|
+
severity: "error",
|
|
21596
|
+
code: "invalid-effect-errors",
|
|
21597
|
+
errorCode: "SC3034",
|
|
21598
|
+
docsUrl: "https://supacloud.dev/errors/SC3034",
|
|
21599
|
+
file,
|
|
21600
|
+
line: lineOf(errorExpr),
|
|
21601
|
+
message: `Route ${route.handler} effect.errors entries must be object literals.`
|
|
21602
|
+
});
|
|
21603
|
+
continue;
|
|
21604
|
+
}
|
|
21605
|
+
const tag = stringLiteralProp(errorExpr, "tag");
|
|
21606
|
+
const code = stringLiteralProp(errorExpr, "code");
|
|
21607
|
+
const status = numberLiteralProp(errorExpr, "status");
|
|
21608
|
+
const message = stringLiteralProp(errorExpr, "message");
|
|
21609
|
+
const codeFromError = booleanLiteralProp(errorExpr, "codeFromError");
|
|
21610
|
+
const messageFromError = booleanLiteralProp(errorExpr, "messageFromError");
|
|
21611
|
+
const statusFromError = booleanLiteralProp(errorExpr, "statusFromError");
|
|
21612
|
+
const retryable = booleanLiteralProp(errorExpr, "retryable");
|
|
21613
|
+
const outcomeUnknown = booleanLiteralProp(errorExpr, "outcomeUnknown");
|
|
21614
|
+
if (invalidBooleanProp(errorExpr, "codeFromError") || invalidBooleanProp(errorExpr, "messageFromError") || invalidBooleanProp(errorExpr, "statusFromError") || invalidBooleanProp(errorExpr, "retryable") || invalidBooleanProp(errorExpr, "outcomeUnknown")) {
|
|
21615
|
+
ctx.diagnostics.push({
|
|
21616
|
+
severity: "error",
|
|
21617
|
+
code: "invalid-effect-errors",
|
|
21618
|
+
errorCode: "SC3034",
|
|
21619
|
+
docsUrl: "https://supacloud.dev/errors/SC3034",
|
|
21620
|
+
file,
|
|
21621
|
+
line: lineOf(errorExpr),
|
|
21622
|
+
message: `Route ${route.handler} effect.errors instance flags must be boolean literals.`
|
|
21623
|
+
});
|
|
21624
|
+
continue;
|
|
21625
|
+
}
|
|
21626
|
+
if (!tag || !code || status === undefined || !Number.isInteger(status) || status < 400 || status > 599) {
|
|
21627
|
+
ctx.diagnostics.push({
|
|
21628
|
+
severity: "error",
|
|
21629
|
+
code: "invalid-effect-errors",
|
|
21630
|
+
errorCode: "SC3034",
|
|
21631
|
+
docsUrl: "https://supacloud.dev/errors/SC3034",
|
|
21632
|
+
file,
|
|
21633
|
+
line: lineOf(errorExpr),
|
|
21634
|
+
message: `Route ${route.handler} effect.errors entries require tag, code and an HTTP status from 400 to 599.`
|
|
21635
|
+
});
|
|
21636
|
+
continue;
|
|
21637
|
+
}
|
|
21638
|
+
errors.push({
|
|
21639
|
+
tag,
|
|
21640
|
+
code,
|
|
21641
|
+
status,
|
|
21642
|
+
...message === undefined ? {} : { message },
|
|
21643
|
+
...codeFromError === undefined ? {} : { codeFromError },
|
|
21644
|
+
...messageFromError === undefined ? {} : { messageFromError },
|
|
21645
|
+
...statusFromError === undefined ? {} : { statusFromError },
|
|
21646
|
+
...retryable === undefined ? {} : { retryable },
|
|
21647
|
+
...outcomeUnknown === undefined ? {} : { outcomeUnknown }
|
|
21614
21648
|
});
|
|
21615
|
-
continue;
|
|
21616
21649
|
}
|
|
21617
|
-
|
|
21650
|
+
effect.errors = errors;
|
|
21651
|
+
if (new Set(errors.map((entry) => entry.tag)).size !== errors.length) {
|
|
21618
21652
|
ctx.diagnostics.push({
|
|
21619
21653
|
severity: "error",
|
|
21620
21654
|
code: "invalid-effect-errors",
|
|
21621
21655
|
errorCode: "SC3034",
|
|
21622
21656
|
docsUrl: "https://supacloud.dev/errors/SC3034",
|
|
21623
21657
|
file,
|
|
21624
|
-
line: lineOf(
|
|
21625
|
-
message: `Route ${route.handler} effect.errors
|
|
21658
|
+
line: lineOf(errorsExpr),
|
|
21659
|
+
message: `Route ${route.handler} effect.errors must not contain duplicate tags.`,
|
|
21660
|
+
suggestion: "Map each tagged failure exactly once."
|
|
21626
21661
|
});
|
|
21627
|
-
continue;
|
|
21628
21662
|
}
|
|
21629
|
-
errors.push({
|
|
21630
|
-
tag,
|
|
21631
|
-
code,
|
|
21632
|
-
status,
|
|
21633
|
-
...message === undefined ? {} : { message },
|
|
21634
|
-
...codeFromError === undefined ? {} : { codeFromError },
|
|
21635
|
-
...messageFromError === undefined ? {} : { messageFromError },
|
|
21636
|
-
...statusFromError === undefined ? {} : { statusFromError },
|
|
21637
|
-
...retryable === undefined ? {} : { retryable },
|
|
21638
|
-
...outcomeUnknown === undefined ? {} : { outcomeUnknown }
|
|
21639
|
-
});
|
|
21640
|
-
}
|
|
21641
|
-
effect.errors = errors;
|
|
21642
|
-
if (new Set(errors.map((entry) => entry.tag)).size !== errors.length) {
|
|
21643
|
-
ctx.diagnostics.push({
|
|
21644
|
-
severity: "error",
|
|
21645
|
-
code: "invalid-effect-errors",
|
|
21646
|
-
errorCode: "SC3034",
|
|
21647
|
-
docsUrl: "https://supacloud.dev/errors/SC3034",
|
|
21648
|
-
file,
|
|
21649
|
-
line: lineOf(errorsExpr),
|
|
21650
|
-
message: `Route ${route.handler} effect.errors must not contain duplicate tags.`,
|
|
21651
|
-
suggestion: "Map each tagged failure exactly once."
|
|
21652
|
-
});
|
|
21653
21663
|
}
|
|
21654
21664
|
}
|
|
21655
|
-
|
|
21656
|
-
|
|
21657
|
-
|
|
21658
|
-
|
|
21659
|
-
|
|
21660
|
-
|
|
21661
|
-
|
|
21662
|
-
|
|
21663
|
-
|
|
21664
|
-
|
|
21665
|
-
|
|
21666
|
-
|
|
21667
|
-
|
|
21668
|
-
|
|
21669
|
-
|
|
21670
|
-
|
|
21671
|
-
});
|
|
21672
|
-
}
|
|
21673
|
-
if (effectArguments) {
|
|
21674
|
-
const [, failureType, environmentType] = effectArguments;
|
|
21675
|
-
const failure = await effectFailureTags(failureType, method, ctx);
|
|
21676
|
-
if (ctx.effectOptions.requireTaggedErrorTypes && failure.invalid) {
|
|
21677
|
-
pushEffectDiagnostic(ctx, "effect-error-type", file, lineOf(method), `Route ${route.handler} must use never or a tagged failure union in Effect's error channel, not ${await effectTypeName(failureType, method, ctx)}.`, "Replace unknown/any with never or a union of objects carrying a literal _tag.");
|
|
21678
|
-
} else if (ctx.effectOptions.requireTaggedErrorTypes && effect.errors !== undefined && !failure.invalid) {
|
|
21679
|
-
const declaredTags = sortedUnique(effect.errors.map((entry) => entry.tag));
|
|
21680
|
-
const typedTags = sortedUnique(failure.tags);
|
|
21681
|
-
if (declaredTags.join("\x00") !== typedTags.join("\x00")) {
|
|
21682
|
-
pushEffectDiagnostic(ctx, "effect-error-mapping-type", file, lineOf(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.");
|
|
21683
|
-
}
|
|
21665
|
+
route.effect = effect;
|
|
21666
|
+
const effectArguments = await effectTypeArgumentsOf(returnType, ctx);
|
|
21667
|
+
const promiseReturn = Boolean(resultType && /\bPromise(?:<|\s)/.test(resultType));
|
|
21668
|
+
if (promiseReturn) {
|
|
21669
|
+
pushEffectDiagnostic(ctx, "effect-promise-return", file, lineOf(method), `Route ${route.handler} returns Promise instead of an Effect program.`, "Return Effect.Effect<Success, Failure, Requirements>; the Elysia boundary owns Promise interpretation.");
|
|
21670
|
+
} else if (!effectArguments) {
|
|
21671
|
+
ctx.diagnostics.push({
|
|
21672
|
+
severity: "error",
|
|
21673
|
+
code: "effect-return-type",
|
|
21674
|
+
errorCode: "SC3035",
|
|
21675
|
+
docsUrl: "https://supacloud.dev/errors/SC3035",
|
|
21676
|
+
file,
|
|
21677
|
+
line: lineOf(method),
|
|
21678
|
+
message: `Route ${route.handler} declares an Effect contract but returns ${resultType ?? "an unresolved type"}.`,
|
|
21679
|
+
suggestion: "Return Effect.Effect<Success, Failure, Requirements> from the handler."
|
|
21680
|
+
});
|
|
21684
21681
|
}
|
|
21685
|
-
|
|
21686
|
-
|
|
21687
|
-
|
|
21688
|
-
|
|
21689
|
-
|
|
21690
|
-
|
|
21691
|
-
const
|
|
21692
|
-
|
|
21693
|
-
|
|
21682
|
+
if (effectArguments) {
|
|
21683
|
+
const [, failureType, environmentType] = effectArguments;
|
|
21684
|
+
const failure = await effectFailureTags(failureType, method, ctx);
|
|
21685
|
+
if (ctx.effectOptions.requireTaggedErrorTypes && failure.invalid) {
|
|
21686
|
+
pushEffectDiagnostic(ctx, "effect-error-type", file, lineOf(method), `Route ${route.handler} must use never or a tagged failure union in Effect's error channel, not ${await effectTypeName(failureType, method, ctx)}.`, "Replace unknown/any with never or a union of objects carrying a literal _tag.");
|
|
21687
|
+
} else if (ctx.effectOptions.requireTaggedErrorTypes && effect.errors !== undefined && !failure.invalid) {
|
|
21688
|
+
const declaredTags = sortedUnique(effect.errors.map((entry) => entry.tag));
|
|
21689
|
+
const typedTags = sortedUnique(failure.tags);
|
|
21690
|
+
if (declaredTags.join("\x00") !== typedTags.join("\x00")) {
|
|
21691
|
+
pushEffectDiagnostic(ctx, "effect-error-mapping-type", file, lineOf(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.");
|
|
21694
21692
|
}
|
|
21695
21693
|
}
|
|
21694
|
+
const environment = await effectEnvironmentNames(environmentType, method, ctx);
|
|
21695
|
+
if (ctx.effectOptions.requireExactDependencyTypes && effect.dependencies !== undefined) {
|
|
21696
|
+
if (environment.invalid) {
|
|
21697
|
+
pushEffectDiagnostic(ctx, "effect-dependency-mismatch", file, lineOf(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.");
|
|
21698
|
+
} else {
|
|
21699
|
+
const declaredDependencies = sortedUnique(effect.dependencies);
|
|
21700
|
+
const typedDependencies = sortedUnique(environment.names);
|
|
21701
|
+
if (declaredDependencies.join("\x00") !== typedDependencies.join("\x00")) {
|
|
21702
|
+
pushEffectDiagnostic(ctx, "effect-dependency-mismatch", file, lineOf(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.");
|
|
21703
|
+
}
|
|
21704
|
+
}
|
|
21705
|
+
}
|
|
21706
|
+
if (ctx.effectOptions.requireTimeoutForDependencies && (effect.dependencies?.length ?? 0) > 0 && effect.timeoutMs === undefined) {
|
|
21707
|
+
pushEffectDiagnostic(ctx, "effect-timeout-required", file, lineOf(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.");
|
|
21708
|
+
}
|
|
21696
21709
|
}
|
|
21697
|
-
if (ctx.effectOptions.
|
|
21698
|
-
|
|
21699
|
-
|
|
21700
|
-
|
|
21701
|
-
if (ctx.effectOptions.forbidDirectThrows && method.body) {
|
|
21702
|
-
for (const throwStatement of descendantsOfKind(method.body, ts8.isThrowStatement)) {
|
|
21703
|
-
pushEffectDiagnostic(ctx, "effect-direct-throw", file, lineOf(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.");
|
|
21710
|
+
if (ctx.effectOptions.forbidDirectThrows && method.body) {
|
|
21711
|
+
for (const throwStatement of descendantsOfKind(method.body, ts8.isThrowStatement)) {
|
|
21712
|
+
pushEffectDiagnostic(ctx, "effect-direct-throw", file, lineOf(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.");
|
|
21713
|
+
}
|
|
21704
21714
|
}
|
|
21705
21715
|
}
|
|
21706
21716
|
}
|
|
@@ -23336,6 +23346,14 @@ function validateEffectPolicies(graph, options = {}) {
|
|
|
23336
23346
|
for (const controller of module.controllers) {
|
|
23337
23347
|
for (const route of controller.routes) {
|
|
23338
23348
|
const label = routeLabel(route, controller.path);
|
|
23349
|
+
if (route.effectMode === "simple") {
|
|
23350
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
23351
|
+
const commandRef = route.command ?? featureTransition?.command;
|
|
23352
|
+
if (commandRef !== undefined || !["GET", "HEAD", "OPTIONS"].includes(route.method)) {
|
|
23353
|
+
diagnostics.push(diagnostic("effect-contract-required", commandRef === undefined ? `Route ${label} cannot use effect: false for a write operation.` : `Route ${label} cannot use effect: false when it enters command ${commandRef}.`, controller.file, "Keep Effect enabled for writes and durable commands; effect: false is only for simple read routes."));
|
|
23354
|
+
}
|
|
23355
|
+
continue;
|
|
23356
|
+
}
|
|
23339
23357
|
if (options.requireRouteEffects && route.effect === undefined) {
|
|
23340
23358
|
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.'));
|
|
23341
23359
|
continue;
|
|
@@ -25684,10 +25702,10 @@ import { readFileSync as readFileSync4 } from "node:fs";
|
|
|
25684
25702
|
import { resolve as resolve17 } from "node:path";
|
|
25685
25703
|
|
|
25686
25704
|
// src/migration-app-version.ts
|
|
25687
|
-
var MIGRATION_APP_VERSION = "0.
|
|
25705
|
+
var MIGRATION_APP_VERSION = "0.39.0";
|
|
25688
25706
|
|
|
25689
25707
|
// src/migration-elysia-version.ts
|
|
25690
|
-
var MIGRATION_ELYSIA_VERSION = "0.
|
|
25708
|
+
var MIGRATION_ELYSIA_VERSION = "0.41.0";
|
|
25691
25709
|
|
|
25692
25710
|
// src/migration-policy.ts
|
|
25693
25711
|
var cachedCompilerVersion;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const MIGRATION_APP_VERSION = "0.
|
|
1
|
+
export declare const MIGRATION_APP_VERSION = "0.39.0";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const MIGRATION_ELYSIA_VERSION = "0.
|
|
1
|
+
export declare const MIGRATION_ELYSIA_VERSION = "0.41.0";
|
package/dist/types.d.ts
CHANGED
|
@@ -253,6 +253,8 @@ export interface RouteNode {
|
|
|
253
253
|
responses?: Record<string, string>;
|
|
254
254
|
/** Static Effect execution contract for this route. */
|
|
255
255
|
effect?: EffectRouteNode;
|
|
256
|
+
/** Simple route profile: handler may return a Promise/value without Effect governance. */
|
|
257
|
+
effectMode?: "simple";
|
|
256
258
|
/** @Command-decorated class explicitly bound by the route. */
|
|
257
259
|
command?: string;
|
|
258
260
|
/** Route guards executed before handler (Angular CanActivateFn style). */
|
|
@@ -404,6 +406,7 @@ export interface ExecutionPlanRoute {
|
|
|
404
406
|
invoker: "direct" | "pipeline";
|
|
405
407
|
commandBoundary?: "route" | "handler";
|
|
406
408
|
effect?: EffectRouteNode;
|
|
409
|
+
effectMode?: "simple";
|
|
407
410
|
stages: string[];
|
|
408
411
|
command?: string;
|
|
409
412
|
guards: string[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supacloud/compiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.58.0",
|
|
4
4
|
"description": "Static compiler for @supacloud/app metadata: builds the application graph from AST, validates it, and generates reflection-free factory code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"directory": "packages/compiler"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@supacloud/db": "0.
|
|
60
|
+
"@supacloud/db": "0.22.0",
|
|
61
61
|
"@supacloud/delivery": "0.25.0",
|
|
62
62
|
"typebox": "1.4.3",
|
|
63
63
|
"@graphql-codegen/core": "^6.2.2",
|