@supacloud/compiler 0.48.0 → 0.49.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/application-development.d.ts +1 -0
- package/dist/cli.js +242 -47
- package/dist/contract-manifest.d.ts +3 -0
- package/dist/delivery-bundle-worker.js +3 -1
- package/dist/feature.d.ts +2 -1
- package/dist/index.js +246 -51
- package/dist/migration-app-version.d.ts +1 -1
- package/dist/migration-elysia-version.d.ts +1 -1
- package/dist/types.d.ts +6 -0
- package/package.json +4 -4
package/dist/cli.js
CHANGED
|
@@ -249,6 +249,10 @@ function findClosestMatch(target, candidates) {
|
|
|
249
249
|
var REQUEST_CONTEXT_TOKEN_NAME = "supacloud.request-context", JOB_CONTEXT_TOKEN_NAME = "supacloud.job-context";
|
|
250
250
|
|
|
251
251
|
// src/feature.ts
|
|
252
|
+
function featureTransitionForRoute(module, controller, route) {
|
|
253
|
+
const routePath = `${route.method} ${joinRoutePaths(controller.path, route.path)}`;
|
|
254
|
+
return module.featureSpec?.transitions.find((transition) => transition.route === routePath);
|
|
255
|
+
}
|
|
252
256
|
function validateFeatureSpec(spec, module) {
|
|
253
257
|
const diagnostics = [];
|
|
254
258
|
const error = (code, message) => {
|
|
@@ -278,6 +282,11 @@ function validateFeatureSpec(spec, module) {
|
|
|
278
282
|
if (transition.permission !== undefined && !transition.permission.trim()) {
|
|
279
283
|
error("feature-governance-drift", `Transition ${transition.name} declares an empty permission.`);
|
|
280
284
|
}
|
|
285
|
+
if (transition.invalidateTags !== undefined) {
|
|
286
|
+
if (transition.invalidateTags.length === 0 || transition.invalidateTags.some((tag) => !tag.trim()) || new Set(transition.invalidateTags).size !== transition.invalidateTags.length) {
|
|
287
|
+
error("feature-invalidation-tags-invalid", `Transition ${transition.name} must declare unique, non-empty invalidateTags.`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
281
290
|
if (transition.persistence) {
|
|
282
291
|
const persistence = transition.persistence;
|
|
283
292
|
if (persistence.boundary === "database" && (persistence.optimisticLock !== "required" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(persistence.versionField ?? "") || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(persistence.tenantField ?? "") || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(persistence.stateField ?? "") || new Set([persistence.tenantField, persistence.versionField, persistence.stateField]).size !== 3 || persistence.receipt !== "required" || persistence.reconciliation !== "none")) {
|
|
@@ -370,7 +379,7 @@ function validateFeatureSpec(spec, module) {
|
|
|
370
379
|
const routes = module.controllers.flatMap((controller) => controller.routes.filter((route) => `${route.method} ${joinRoutePaths(controller.path, route.path)}` === transition.route));
|
|
371
380
|
if (routes.length !== 1) {
|
|
372
381
|
error("feature-route-unresolved", `Transition ${transition.name} must reference exactly one route '${transition.route}' in module ${module.name}.`);
|
|
373
|
-
} else if (command && transition.routeBinding !== "controller" && routes[0]?.command !== command.className) {
|
|
382
|
+
} else if (command && transition.routeBinding !== "controller" && routes[0]?.command !== command.className && routes[0]?.command !== command.name) {
|
|
374
383
|
error("feature-route-drift", `Route ${transition.route} is not bound to ${command.className}.`);
|
|
375
384
|
} else if (transition.routeBinding === "controller" && routes[0]?.command !== undefined) {
|
|
376
385
|
error("feature-route-drift", `Controller-bound route ${transition.route} must not also declare a framework command binding.`);
|
|
@@ -615,13 +624,17 @@ function validateGraph(graph, options = false) {
|
|
|
615
624
|
handler: route.handler,
|
|
616
625
|
...route.redirectTo === undefined ? {} : { redirectTo: route.redirectTo }
|
|
617
626
|
});
|
|
618
|
-
|
|
619
|
-
|
|
627
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
628
|
+
const effectiveCommandRef = route.command ?? featureTransition?.command;
|
|
629
|
+
const effectiveCommand = effectiveCommandRef === undefined ? undefined : module.commands.find((command) => command.className === effectiveCommandRef || command.name === effectiveCommandRef);
|
|
630
|
+
const effectiveIdempotency = route.idempotency ?? featureTransition?.idempotency;
|
|
631
|
+
if (route.command && !effectiveCommand) {
|
|
632
|
+
error("route-command-unresolved", `路由 ${key} 绑定的 command ${route.command} 未在模块 ${module.name} 声明`, controller.file);
|
|
620
633
|
}
|
|
621
|
-
if ((route.method === "GET" || route.method === "HEAD") &&
|
|
622
|
-
const boundCommand =
|
|
634
|
+
if ((route.method === "GET" || route.method === "HEAD") && effectiveCommand) {
|
|
635
|
+
const boundCommand = effectiveCommand;
|
|
623
636
|
if (boundCommand && boundCommand.transaction === "required") {
|
|
624
|
-
error("command-transaction-readonly", `GET route '${route.path}' in '${controller.className}.${route.handler}' binds mutating command '${
|
|
637
|
+
error("command-transaction-readonly", `GET route '${route.path}' in '${controller.className}.${route.handler}' binds mutating command '${boundCommand.name}' with transaction: 'required'. Mutating transactions are not permitted on read-only HTTP GET requests.`, controller.file, undefined, `Use POST, PUT, or PATCH for mutating command routes, or set transaction: 'none'.`);
|
|
625
638
|
}
|
|
626
639
|
}
|
|
627
640
|
if (typeof options === "object" && options.allowRouteCommandBindings === false && route.command) {
|
|
@@ -692,6 +705,9 @@ function validateGraph(graph, options = false) {
|
|
|
692
705
|
if (route.allowDeleteBody && (route.method !== "DELETE" || !route.body)) {
|
|
693
706
|
error("invalid-delete-body-opt-in", `Route ${controller.className}.${route.handler} allowDeleteBody requires DELETE and an explicit body schema.`, controller.file);
|
|
694
707
|
}
|
|
708
|
+
if (effectiveIdempotency === "required" && (route.method === "GET" || route.method === "HEAD" || route.method === "OPTIONS")) {
|
|
709
|
+
error("invalid-route-idempotency", `Route ${controller.className}.${route.handler} cannot require idempotency on HTTP ${route.method}.`, controller.file, undefined, `Use idempotency: "required" only on a mutation route.`);
|
|
710
|
+
}
|
|
695
711
|
if (route.parse === "none" && (route.body || route.hasBodyBinding || route.contract?.body !== "domain" || !route.contract.evidence?.trim())) {
|
|
696
712
|
error("invalid-raw-body-contract", `Route ${controller.className}.${route.handler} parse: "none" requires domain body ownership and evidence, without a parsed body schema or @Body binding.`, controller.file);
|
|
697
713
|
}
|
|
@@ -1366,6 +1382,7 @@ var init_validate = __esm(() => {
|
|
|
1366
1382
|
"command-transaction-unsupported": { code: "SC4026", docsUrl: "https://supacloud.dev/errors/SC4026" },
|
|
1367
1383
|
"command-reconcile-required": { code: "SC4028", docsUrl: "https://supacloud.dev/errors/SC4028" },
|
|
1368
1384
|
"invalid-route-contract": { code: "SC3020", docsUrl: "https://supacloud.dev/errors/SC3020" },
|
|
1385
|
+
"invalid-route-idempotency": { code: "SC3028", docsUrl: "https://supacloud.dev/errors/SC3028" },
|
|
1369
1386
|
"route-contract-required": { code: "SC3021", docsUrl: "https://supacloud.dev/errors/SC3021" },
|
|
1370
1387
|
"route-contract-unverified": { code: "SC3022", docsUrl: "https://supacloud.dev/errors/SC3022" },
|
|
1371
1388
|
"route-contract-evidence-required": { code: "SC3023", docsUrl: "https://supacloud.dev/errors/SC3023" },
|
|
@@ -1512,6 +1529,7 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1512
1529
|
...transition.idempotency === undefined ? {} : { idempotency: transition.idempotency },
|
|
1513
1530
|
...transition.audit === undefined ? {} : { audit: transition.audit },
|
|
1514
1531
|
...transition.job === undefined ? {} : { job: transition.job },
|
|
1532
|
+
...transition.invalidateTags === undefined ? {} : { invalidateTags: [...transition.invalidateTags] },
|
|
1515
1533
|
...job === undefined ? {} : {
|
|
1516
1534
|
jobDetails: {
|
|
1517
1535
|
name: job.name,
|
|
@@ -1532,9 +1550,14 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1532
1550
|
}
|
|
1533
1551
|
for (const controller of module.controllers) {
|
|
1534
1552
|
for (const route of controller.routes) {
|
|
1535
|
-
const command = route.command ? module.commands.find((candidate) => candidate.className === route.command) : undefined;
|
|
1536
|
-
|
|
1537
|
-
|
|
1553
|
+
const command = route.command ? module.commands.find((candidate) => candidate.className === route.command || candidate.name === route.command) : undefined;
|
|
1554
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
1555
|
+
const featureCommand = featureTransition?.command === undefined ? undefined : module.commands.find((candidate) => candidate.className === featureTransition.command || candidate.name === featureTransition.command);
|
|
1556
|
+
const governedCommand = command ?? featureCommand;
|
|
1557
|
+
const effectiveCommand = route.command ?? featureTransition?.command;
|
|
1558
|
+
const idempotency = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency;
|
|
1559
|
+
if (governedCommand?.permission)
|
|
1560
|
+
permissions.add(governedCommand.permission);
|
|
1538
1561
|
if (route.contract?.evidence)
|
|
1539
1562
|
fixtures.add(route.contract.evidence);
|
|
1540
1563
|
routes.push({
|
|
@@ -1543,8 +1566,10 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1543
1566
|
module: module.name,
|
|
1544
1567
|
controller: controller.className,
|
|
1545
1568
|
handler: route.handler,
|
|
1546
|
-
...
|
|
1547
|
-
...
|
|
1569
|
+
...effectiveCommand === undefined ? {} : { command: effectiveCommand },
|
|
1570
|
+
...governedCommand?.permission === undefined ? {} : { permission: governedCommand.permission },
|
|
1571
|
+
...idempotency === undefined ? {} : { idempotency },
|
|
1572
|
+
...route.tags === undefined ? {} : { tags: [...route.tags] },
|
|
1548
1573
|
requestSchemas: Object.fromEntries(["body", "params", "query", "headers", "cookie"].flatMap((key) => route[key] === undefined ? [] : [[key, route[key]]])),
|
|
1549
1574
|
...route.response === undefined ? {} : { responseSchema: route.response },
|
|
1550
1575
|
...route.responses === undefined ? {} : {
|
|
@@ -1583,7 +1608,9 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1583
1608
|
}
|
|
1584
1609
|
};
|
|
1585
1610
|
}
|
|
1586
|
-
var init_contract_manifest = () => {
|
|
1611
|
+
var init_contract_manifest = __esm(() => {
|
|
1612
|
+
init_feature();
|
|
1613
|
+
});
|
|
1587
1614
|
|
|
1588
1615
|
// src/generate.ts
|
|
1589
1616
|
import { createHash as createHash4 } from "node:crypto";
|
|
@@ -1995,6 +2022,9 @@ class ModuleGenerator {
|
|
|
1995
2022
|
return "[]";
|
|
1996
2023
|
const items = this.module.controllers.map((controller) => {
|
|
1997
2024
|
const routes = controller.routes.map((route) => {
|
|
2025
|
+
const command = route.command === undefined ? undefined : this.module.commands.find((candidate) => candidate.className === route.command || candidate.name === route.command);
|
|
2026
|
+
const featureTransition = featureTransitionForRoute(this.module, controller, route);
|
|
2027
|
+
const idempotency = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency;
|
|
1998
2028
|
const responseEntries = normalizedResponseEntries(route.responses);
|
|
1999
2029
|
const hasResponseMap = responseEntries.length > 0;
|
|
2000
2030
|
const fields = [
|
|
@@ -2021,6 +2051,9 @@ class ModuleGenerator {
|
|
|
2021
2051
|
if (route.effect !== undefined) {
|
|
2022
2052
|
fields.push(`effect: ${JSON.stringify(route.effect)}`);
|
|
2023
2053
|
}
|
|
2054
|
+
if (idempotency !== undefined) {
|
|
2055
|
+
fields.push(`idempotency: ${JSON.stringify(idempotency)}`);
|
|
2056
|
+
}
|
|
2024
2057
|
if (route.command)
|
|
2025
2058
|
fields.push(`command: ${JSON.stringify(route.command)}`);
|
|
2026
2059
|
if (route.guards && route.guards.length > 0) {
|
|
@@ -2503,7 +2536,18 @@ function renderClient(graph, options) {
|
|
|
2503
2536
|
const usedTypeNames = new Set;
|
|
2504
2537
|
const allRoutes = [];
|
|
2505
2538
|
const allProcedures = [];
|
|
2506
|
-
const commandByClassName = new Map(graph.modules.flatMap((module) => module.commands.
|
|
2539
|
+
const commandByClassName = new Map(graph.modules.flatMap((module) => module.commands.flatMap((command) => [
|
|
2540
|
+
[command.className, command],
|
|
2541
|
+
[command.name, command]
|
|
2542
|
+
])));
|
|
2543
|
+
const featureInvalidationTags = new Map;
|
|
2544
|
+
for (const module of graph.modules) {
|
|
2545
|
+
for (const transition of module.featureSpec?.transitions ?? []) {
|
|
2546
|
+
if (transition.route && transition.invalidateTags) {
|
|
2547
|
+
featureInvalidationTags.set(`${module.name}:${transition.route}`, [...transition.invalidateTags]);
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2507
2551
|
let usesStatic = false;
|
|
2508
2552
|
let usesValue = false;
|
|
2509
2553
|
const uniqueTypeName = (candidate) => {
|
|
@@ -2581,6 +2625,13 @@ function renderClient(graph, options) {
|
|
|
2581
2625
|
const requestType = `ClientRequestOptions<${paramsType}, ${queryType}, ${bodyType}, ${headersType}, ${cookieType}>${requiredRequestFields.length > 0 ? ` & { ${requiredRequestFields.join("; ")} }` : ""}`;
|
|
2582
2626
|
routeTypes.push(`export type ${requestTypeName} = ${requestType};`);
|
|
2583
2627
|
routeTypes.push(`export type ${responseTypeName} = ${responseType === "never" ? "unknown" : responseType};`);
|
|
2628
|
+
const command = route.command === undefined ? undefined : commandByClassName.get(route.command);
|
|
2629
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
2630
|
+
const featureCommand = featureTransition?.command === undefined ? undefined : commandByClassName.get(featureTransition.command);
|
|
2631
|
+
const governedCommand = command ?? featureCommand;
|
|
2632
|
+
const idempotencyMode = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency ?? "none";
|
|
2633
|
+
const transitionRoute = `${route.method} ${fullPath}`;
|
|
2634
|
+
const invalidationTags = featureInvalidationTags.get(`${module.name}:${transitionRoute}`);
|
|
2584
2635
|
const routeFields = [
|
|
2585
2636
|
`method: ${JSON.stringify(route.method)}`,
|
|
2586
2637
|
`path: ${JSON.stringify(fullPath)}`,
|
|
@@ -2588,7 +2639,7 @@ function renderClient(graph, options) {
|
|
|
2588
2639
|
`handler: ${JSON.stringify(route.handler)}`
|
|
2589
2640
|
];
|
|
2590
2641
|
for (const [key, value] of [
|
|
2591
|
-
["command", route.command],
|
|
2642
|
+
["command", route.command ?? featureTransition?.command],
|
|
2592
2643
|
["guards", route.guards],
|
|
2593
2644
|
["canMatch", route.canMatch],
|
|
2594
2645
|
["canDeactivate", route.canDeactivate],
|
|
@@ -2602,7 +2653,10 @@ function renderClient(graph, options) {
|
|
|
2602
2653
|
["title", route.title],
|
|
2603
2654
|
["data", route.data],
|
|
2604
2655
|
["parse", route.parse],
|
|
2605
|
-
["allowDeleteBody", route.allowDeleteBody]
|
|
2656
|
+
["allowDeleteBody", route.allowDeleteBody],
|
|
2657
|
+
["idempotency", idempotencyMode === "none" ? undefined : idempotencyMode],
|
|
2658
|
+
["tags", route.tags],
|
|
2659
|
+
["invalidateTags", invalidationTags]
|
|
2606
2660
|
]) {
|
|
2607
2661
|
if (value !== undefined)
|
|
2608
2662
|
routeFields.push(`${key}: ${JSON.stringify(value)}`);
|
|
@@ -2635,9 +2689,7 @@ function renderClient(graph, options) {
|
|
|
2635
2689
|
const responseSchemaEntries = hasResponseMap ? Object.entries(responseSchemas).map(([status, ref]) => [status, ref.local]) : responseSchema ? [["200", responseSchema.local]] : [];
|
|
2636
2690
|
const responseSchemaArgument = responseSchemaEntries.length > 0 ? `{ ${responseSchemaEntries.map(([status, local]) => `${JSON.stringify(status)}: ${local}`).join(", ")} }` : "undefined";
|
|
2637
2691
|
const responseKindArgument = responseKind === undefined ? "undefined" : JSON.stringify(responseKind);
|
|
2638
|
-
const command = route.command === undefined ? undefined : commandByClassName.get(route.command);
|
|
2639
2692
|
const procedureKind = route.method === "GET" || route.method === "HEAD" ? "query" : "mutation";
|
|
2640
|
-
const idempotencyMode = command?.idempotency ?? "none";
|
|
2641
2693
|
const procedureMetadata = {
|
|
2642
2694
|
key: `${controller.className}.${route.handler}`,
|
|
2643
2695
|
method: route.method,
|
|
@@ -2645,7 +2697,9 @@ function renderClient(graph, options) {
|
|
|
2645
2697
|
controller: controller.className,
|
|
2646
2698
|
handler: route.handler,
|
|
2647
2699
|
kind: procedureKind,
|
|
2648
|
-
idempotency: idempotencyMode
|
|
2700
|
+
idempotency: idempotencyMode,
|
|
2701
|
+
...route.tags === undefined ? {} : { tags: route.tags },
|
|
2702
|
+
...invalidationTags === undefined ? {} : { invalidateTags: invalidationTags }
|
|
2649
2703
|
};
|
|
2650
2704
|
allProcedures.push({
|
|
2651
2705
|
method: route.method,
|
|
@@ -2653,9 +2707,11 @@ function renderClient(graph, options) {
|
|
|
2653
2707
|
controller: controller.className,
|
|
2654
2708
|
handler: route.handler,
|
|
2655
2709
|
kind: procedureKind,
|
|
2656
|
-
...
|
|
2657
|
-
...
|
|
2658
|
-
idempotency: idempotencyMode
|
|
2710
|
+
...governedCommand?.name === undefined ? {} : { command: governedCommand.name },
|
|
2711
|
+
...governedCommand?.permission === undefined ? {} : { permission: governedCommand.permission },
|
|
2712
|
+
idempotency: idempotencyMode,
|
|
2713
|
+
...route.tags === undefined ? {} : { tags: [...route.tags] },
|
|
2714
|
+
...invalidationTags === undefined ? {} : { invalidateTags: [...invalidationTags] }
|
|
2659
2715
|
});
|
|
2660
2716
|
routeMethods.push(`
|
|
2661
2717
|
${route.handler}: makeRoute<${requestTypeName}, ${responseType === "never" ? "never" : responseTypeName}, ${JSON.stringify(procedureKind)}, ${JSON.stringify(idempotencyMode)}>(${JSON.stringify(route.method)}, ${JSON.stringify(fullPath)}, ${responseSchemaArgument}, ${responseKindArgument}, ${route.parse === "none"}, ${JSON.stringify(procedureKind)}, ${JSON.stringify(idempotencyMode)}, ${JSON.stringify(procedureMetadata)}),`);
|
|
@@ -2709,6 +2765,8 @@ ${procedureMethods.join(`
|
|
|
2709
2765
|
" handler: string;",
|
|
2710
2766
|
" kind: Kind;",
|
|
2711
2767
|
" idempotency: Idempotency;",
|
|
2768
|
+
" tags?: readonly string[];",
|
|
2769
|
+
" invalidateTags?: readonly string[];",
|
|
2712
2770
|
"}",
|
|
2713
2771
|
"",
|
|
2714
2772
|
"export interface ProcedureExecutionOptions {",
|
|
@@ -2814,6 +2872,8 @@ ${procedureMethods.join(`
|
|
|
2814
2872
|
" command?: string;",
|
|
2815
2873
|
" permission?: string;",
|
|
2816
2874
|
" idempotency: IdempotencyMode;",
|
|
2875
|
+
" tags?: readonly string[];",
|
|
2876
|
+
" invalidateTags?: readonly string[];",
|
|
2817
2877
|
"}",
|
|
2818
2878
|
"",
|
|
2819
2879
|
...routeTypes,
|
|
@@ -3313,7 +3373,10 @@ function renderOpenApi(graph, options) {
|
|
|
3313
3373
|
}
|
|
3314
3374
|
return schemaComponents.get(key);
|
|
3315
3375
|
};
|
|
3316
|
-
const commandByClass = new Map(graph.modules.flatMap((module) => module.commands.
|
|
3376
|
+
const commandByClass = new Map(graph.modules.flatMap((module) => module.commands.flatMap((command) => [
|
|
3377
|
+
[command.className, command],
|
|
3378
|
+
[command.name, command]
|
|
3379
|
+
])));
|
|
3317
3380
|
for (const module of graph.modules) {
|
|
3318
3381
|
for (const controller of module.controllers) {
|
|
3319
3382
|
for (const route of controller.routes) {
|
|
@@ -3335,7 +3398,12 @@ function renderOpenApi(graph, options) {
|
|
|
3335
3398
|
const operationBase = pascalName(`${module.name}_${controller.className}_${route.handler}`);
|
|
3336
3399
|
const operationId = uniqueName(operationBase, operationIds);
|
|
3337
3400
|
const command = route.command ? commandByClass.get(route.command) : undefined;
|
|
3338
|
-
const
|
|
3401
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
3402
|
+
const featureCommand = featureTransition?.command === undefined ? undefined : commandByClass.get(featureTransition.command);
|
|
3403
|
+
const governedCommand = command ?? featureCommand;
|
|
3404
|
+
const idempotencyMode = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency ?? "none";
|
|
3405
|
+
const permission = governedCommand?.permission ?? featureTransition?.permission;
|
|
3406
|
+
const requiresAuth = (route.guards?.length ?? 0) > 0 || permission !== undefined || governedCommand !== undefined;
|
|
3339
3407
|
const responseKind = route.contract?.response;
|
|
3340
3408
|
const responseContentType = responseKind === "binary" || responseKind === "stream" ? "application/octet-stream" : "application/json";
|
|
3341
3409
|
const definition = {
|
|
@@ -3360,11 +3428,12 @@ function renderOpenApi(graph, options) {
|
|
|
3360
3428
|
responseKind: responseKind ?? (route.nativeResponse ? "native-response" : undefined),
|
|
3361
3429
|
...route.parse === undefined ? {} : { parse: route.parse },
|
|
3362
3430
|
...route.allowDeleteBody === undefined ? {} : { allowDeleteBody: route.allowDeleteBody },
|
|
3431
|
+
...idempotencyMode === "none" ? {} : { idempotency: idempotencyMode },
|
|
3363
3432
|
module: module.name,
|
|
3364
3433
|
controller: controller.className,
|
|
3365
3434
|
handler: route.handler,
|
|
3366
|
-
...route.command === undefined ? {} : { command: route.command },
|
|
3367
|
-
...
|
|
3435
|
+
...route.command === undefined && featureTransition?.command === undefined ? {} : { command: route.command ?? featureTransition?.command },
|
|
3436
|
+
...permission === undefined ? {} : { permission },
|
|
3368
3437
|
...route.guards && route.guards.length > 0 ? { guards: route.guards } : {},
|
|
3369
3438
|
...route.canMatch && route.canMatch.length > 0 ? { canMatch: route.canMatch } : {},
|
|
3370
3439
|
...route.canDeactivate && route.canDeactivate.length > 0 ? { canDeactivate: route.canDeactivate } : {},
|
|
@@ -3439,6 +3508,7 @@ function renderOpenApi(graph, options) {
|
|
|
3439
3508
|
" handler: string;",
|
|
3440
3509
|
" command?: string;",
|
|
3441
3510
|
" permission?: string;",
|
|
3511
|
+
' idempotency?: "required" | "none";',
|
|
3442
3512
|
" guards?: readonly string[];",
|
|
3443
3513
|
" canMatch?: readonly string[];",
|
|
3444
3514
|
" canDeactivate?: readonly string[];",
|
|
@@ -3691,6 +3761,18 @@ function renderOpenApi(graph, options) {
|
|
|
3691
3761
|
" for (const name of headersSchema ? schemaPropertyNames(headersSchema) : []) parameters.push({",
|
|
3692
3762
|
' name, in: "header", required: schemaRequired(headersSchema, name), schema: schemaProperty(headersSchema, name),',
|
|
3693
3763
|
" });",
|
|
3764
|
+
' if (definition.idempotency === "required") {',
|
|
3765
|
+
' const keySchema = { type: "string", minLength: 1, maxLength: 512, pattern: "^[A-Za-z0-9._:-]{1,512}$" };',
|
|
3766
|
+
' const declaredHeaders = parameters.filter((parameter) => parameter["in"] === "header" && typeof parameter["name"] === "string" && parameter["name"].toLowerCase() === "idempotency-key");',
|
|
3767
|
+
" const header = declaredHeaders[0];",
|
|
3768
|
+
" if (header) {",
|
|
3769
|
+
' header["required"] = true;',
|
|
3770
|
+
' header["schema"] = { allOf: [...declaredHeaders.map((parameter) => parameter["schema"]), keySchema] };',
|
|
3771
|
+
" for (const duplicate of declaredHeaders.slice(1)) parameters.splice(parameters.indexOf(duplicate), 1);",
|
|
3772
|
+
" } else {",
|
|
3773
|
+
' parameters.push({ name: "Idempotency-Key", in: "header", required: true, schema: keySchema });',
|
|
3774
|
+
" }",
|
|
3775
|
+
" }",
|
|
3694
3776
|
" const cookieSchema = definition.cookieSchema ? OPENAPI_SCHEMA_REGISTRY[definition.cookieSchema as keyof typeof OPENAPI_SCHEMA_REGISTRY] : undefined;",
|
|
3695
3777
|
" for (const name of cookieSchema ? schemaPropertyNames(cookieSchema) : []) parameters.push({",
|
|
3696
3778
|
' name, in: "cookie", required: schemaRequired(cookieSchema, name), schema: schemaProperty(cookieSchema, name),',
|
|
@@ -3713,6 +3795,7 @@ function renderOpenApi(graph, options) {
|
|
|
3713
3795
|
' responses["500"] = { description: "Internal server error", content: { "application/json": { schema: { $ref: "#/components/schemas/SupaCloudError" } } } };',
|
|
3714
3796
|
' responses["200"] = { description: "Successful response" };',
|
|
3715
3797
|
" }",
|
|
3798
|
+
' if (definition.idempotency === "required" && (!definition.responseSchemas || !responseMapCoversStatus(definition.responseSchemas, 400))) responses["400"] = { description: "Idempotency-Key is missing or invalid", content: { "application/json": { schema: { $ref: "#/components/schemas/SupaCloudError" } } } };',
|
|
3716
3799
|
" const operation: Record<string, unknown> = {",
|
|
3717
3800
|
" operationId: definition.operationId, tags: [...definition.tags], summary: definition.summary, responses,",
|
|
3718
3801
|
" ...(parameters.length === 0 ? {} : { parameters }),",
|
|
@@ -3723,6 +3806,7 @@ function renderOpenApi(graph, options) {
|
|
|
3723
3806
|
" module: definition.module, controller: definition.controller, handler: definition.handler,",
|
|
3724
3807
|
" ...(definition.command ? { command: definition.command } : {}),",
|
|
3725
3808
|
" ...(definition.permission ? { permission: definition.permission } : {}),",
|
|
3809
|
+
" ...(definition.idempotency ? { idempotency: definition.idempotency } : {}),",
|
|
3726
3810
|
" ...(definition.guards ? { guards: [...definition.guards] } : {}),",
|
|
3727
3811
|
" ...(definition.canMatch ? { canMatch: [...definition.canMatch] } : {}),",
|
|
3728
3812
|
" ...(definition.canDeactivate ? { canDeactivate: [...definition.canDeactivate] } : {}),",
|
|
@@ -3773,11 +3857,13 @@ function renderPermissions(graph) {
|
|
|
3773
3857
|
}
|
|
3774
3858
|
for (const controller of module.controllers) {
|
|
3775
3859
|
for (const route of controller.routes) {
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3860
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
3861
|
+
const command = route.command === undefined ? undefined : module.commands.find((candidate) => candidate.className === route.command || candidate.name === route.command);
|
|
3862
|
+
const featureCommand = featureTransition?.command === undefined ? undefined : module.commands.find((candidate) => candidate.className === featureTransition.command || candidate.name === featureTransition.command);
|
|
3863
|
+
const governedCommand = command ?? featureCommand;
|
|
3864
|
+
const effectiveCommand = route.command ?? featureTransition?.command;
|
|
3865
|
+
const perm = governedCommand?.permission ?? featureTransition?.permission;
|
|
3866
|
+
const idempotency = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency;
|
|
3781
3867
|
if (perm)
|
|
3782
3868
|
permissions.add(perm);
|
|
3783
3869
|
bindings.push({
|
|
@@ -3785,8 +3871,9 @@ function renderPermissions(graph) {
|
|
|
3785
3871
|
path: joinRoutePaths(controller.path, route.path),
|
|
3786
3872
|
controller: controller.className,
|
|
3787
3873
|
handler: route.handler,
|
|
3788
|
-
...
|
|
3789
|
-
...perm === undefined ? {} : { permission: perm }
|
|
3874
|
+
...effectiveCommand === undefined ? {} : { command: effectiveCommand },
|
|
3875
|
+
...perm === undefined ? {} : { permission: perm },
|
|
3876
|
+
...idempotency === undefined ? {} : { idempotency }
|
|
3790
3877
|
});
|
|
3791
3878
|
}
|
|
3792
3879
|
}
|
|
@@ -3812,6 +3899,7 @@ function renderPermissions(graph) {
|
|
|
3812
3899
|
" handler: string;",
|
|
3813
3900
|
" command?: string;",
|
|
3814
3901
|
" permission?: string;",
|
|
3902
|
+
' idempotency?: "required" | "none";',
|
|
3815
3903
|
"}",
|
|
3816
3904
|
"",
|
|
3817
3905
|
"export const RoutePermissions: RoutePermissionBinding[] = " + JSON.stringify(bindings, null, 2) + ";",
|
|
@@ -3855,6 +3943,8 @@ var HEADER = "// GENERATED BY @supacloud/compiler — do not edit", INTERFACES =
|
|
|
3855
3943
|
/** True when the handler returns a native Response rather than a framework value. */
|
|
3856
3944
|
nativeResponse?: boolean;
|
|
3857
3945
|
command?: string;
|
|
3946
|
+
/** Transport-level idempotency contract, independent of route command binding. */
|
|
3947
|
+
idempotency?: "required" | "none";
|
|
3858
3948
|
guards?: string[];
|
|
3859
3949
|
canMatch?: string[];
|
|
3860
3950
|
canDeactivate?: string[];
|
|
@@ -4092,6 +4182,7 @@ async function destroyServiceInstances(
|
|
|
4092
4182
|
`;
|
|
4093
4183
|
var init_generate = __esm(() => {
|
|
4094
4184
|
init_contract_manifest();
|
|
4185
|
+
init_feature();
|
|
4095
4186
|
});
|
|
4096
4187
|
|
|
4097
4188
|
// src/route-contracts.ts
|
|
@@ -4945,7 +5036,9 @@ function createExecutionPlans(graph) {
|
|
|
4945
5036
|
const aspects = (boundary, refs = []) => refs.map((ref, index) => `${boundary}.aspect[${index}]:${ref.name}`);
|
|
4946
5037
|
return graph.modules.flatMap((module) => [
|
|
4947
5038
|
...module.controllers.flatMap((controller) => controller.routes.map((route) => {
|
|
4948
|
-
const
|
|
5039
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
5040
|
+
const commandRef = route.command ?? featureTransition?.command;
|
|
5041
|
+
const command = module.commands.find((item) => item.className === commandRef || item.name === commandRef);
|
|
4949
5042
|
const path = `${controller.path}/${route.path}`.replace(/\/+/g, "/");
|
|
4950
5043
|
return {
|
|
4951
5044
|
module: module.name,
|
|
@@ -5095,6 +5188,7 @@ function exportGraphDot(graph) {
|
|
|
5095
5188
|
}
|
|
5096
5189
|
var init_inspect = __esm(() => {
|
|
5097
5190
|
init_route_contracts();
|
|
5191
|
+
init_feature();
|
|
5098
5192
|
});
|
|
5099
5193
|
|
|
5100
5194
|
// node_modules/typebox/build/system/memory/metrics.mjs
|
|
@@ -15662,6 +15756,7 @@ var init_application_development = __esm(() => {
|
|
|
15662
15756
|
controller: text2,
|
|
15663
15757
|
handler: text2,
|
|
15664
15758
|
command: Optional(text2),
|
|
15759
|
+
idempotency: Optional(mode),
|
|
15665
15760
|
aspects: texts,
|
|
15666
15761
|
schemaKinds: Optional(_Object_({
|
|
15667
15762
|
body: schemaKind,
|
|
@@ -15784,7 +15879,11 @@ function createApplicationDevelopmentContext(graph, options = {}) {
|
|
|
15784
15879
|
for (const controller of module.controllers) {
|
|
15785
15880
|
for (const route of controller.routes) {
|
|
15786
15881
|
const candidates = module.commands.filter((command) => command.className === route.command || command.name === route.command);
|
|
15787
|
-
const
|
|
15882
|
+
const commandNode = candidates.length === 1 ? candidates[0] : undefined;
|
|
15883
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
15884
|
+
const featureCommandNode = featureTransition?.command === undefined ? undefined : module.commands.find((candidate) => candidate.className === featureTransition.command || candidate.name === featureTransition.command);
|
|
15885
|
+
const command = commandNode?.name ?? featureCommandNode?.name;
|
|
15886
|
+
const idempotency = route.idempotency ?? commandNode?.idempotency ?? featureTransition?.idempotency;
|
|
15788
15887
|
const kinds = schemaKinds(route);
|
|
15789
15888
|
routes.push({
|
|
15790
15889
|
module: module.name,
|
|
@@ -15793,6 +15892,7 @@ function createApplicationDevelopmentContext(graph, options = {}) {
|
|
|
15793
15892
|
controller: controller.className,
|
|
15794
15893
|
handler: route.handler,
|
|
15795
15894
|
...command === undefined ? {} : { command },
|
|
15895
|
+
...idempotency === undefined ? {} : { idempotency },
|
|
15796
15896
|
aspects: (route.aspects ?? []).map((aspect) => aspect.name),
|
|
15797
15897
|
...kinds === undefined ? {} : { schemaKinds: kinds }
|
|
15798
15898
|
});
|
|
@@ -15913,6 +16013,7 @@ var init_application_development2 = __esm(() => {
|
|
|
15913
16013
|
init_application_development();
|
|
15914
16014
|
init_inspect();
|
|
15915
16015
|
init_execution_snapshot();
|
|
16016
|
+
init_feature();
|
|
15916
16017
|
APPLICATION_DEVELOPMENT_LIMITS2 = APPLICATION_DEVELOPMENT_LIMITS;
|
|
15917
16018
|
ApplicationDevelopmentError = class ApplicationDevelopmentError extends Error {
|
|
15918
16019
|
code;
|
|
@@ -16014,7 +16115,7 @@ function parseDeliveryPlanResult(value) {
|
|
|
16014
16115
|
throw new Error("Invalid delivery plan result.");
|
|
16015
16116
|
return value;
|
|
16016
16117
|
}
|
|
16017
|
-
var objectOptions, reference2, references, kind2, isolation, DeliveryFilePathSchema, functionTarget, DeliveryOptionsSchema, DeliveryDiagnosticSchema, routeSchema, jobSchema, DeliveryTargetSchema, DeliveryPlanSchema, DeliveryPlanResultSchema, DeliveryConfigurationError;
|
|
16118
|
+
var objectOptions, reference2, references, kind2, isolation, mode2, DeliveryFilePathSchema, functionTarget, DeliveryOptionsSchema, DeliveryDiagnosticSchema, routeSchema, jobSchema, DeliveryTargetSchema, DeliveryPlanSchema, DeliveryPlanResultSchema, DeliveryConfigurationError;
|
|
16018
16119
|
var init_delivery_schema = __esm(() => {
|
|
16019
16120
|
init_build();
|
|
16020
16121
|
init_value2();
|
|
@@ -16024,6 +16125,7 @@ var init_delivery_schema = __esm(() => {
|
|
|
16024
16125
|
references = _Array_(reference2, { uniqueItems: true });
|
|
16025
16126
|
kind2 = Union([Literal("api"), Literal("jobs"), Literal("webhook"), Literal("function")]);
|
|
16026
16127
|
isolation = Union([Literal("shared"), Literal("process")]);
|
|
16128
|
+
mode2 = Union([Literal("required"), Literal("none")]);
|
|
16027
16129
|
DeliveryFilePathSchema = String2({
|
|
16028
16130
|
maxLength: 512,
|
|
16029
16131
|
pattern: "^(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*/)*[A-Za-z0-9_-][A-Za-z0-9_.-]*$"
|
|
@@ -16097,6 +16199,7 @@ var init_delivery_schema = __esm(() => {
|
|
|
16097
16199
|
]),
|
|
16098
16200
|
path: String2(),
|
|
16099
16201
|
command: Optional(String2()),
|
|
16202
|
+
idempotency: Optional(mode2),
|
|
16100
16203
|
reason: Union([Literal("default-http"), Literal("explicit-module")])
|
|
16101
16204
|
}, objectOptions);
|
|
16102
16205
|
jobSchema = _Object_({
|
|
@@ -16355,7 +16458,12 @@ function parseDeliveryBuildManifest(value) {
|
|
|
16355
16458
|
throw new Error("Invalid delivery object inventory.");
|
|
16356
16459
|
}
|
|
16357
16460
|
}
|
|
16358
|
-
const routes = value.plan.targets.flatMap((target) => target.routes.map((route) => ({
|
|
16461
|
+
const routes = value.plan.targets.flatMap((target) => target.routes.map((route) => ({
|
|
16462
|
+
method: route.method,
|
|
16463
|
+
path: route.path,
|
|
16464
|
+
target: target.name,
|
|
16465
|
+
...route.idempotency === undefined ? {} : { idempotency: route.idempotency }
|
|
16466
|
+
})));
|
|
16359
16467
|
const jobs = value.plan.targets.flatMap((target) => target.jobs.map((job) => ({ name: job.name, target: target.name })));
|
|
16360
16468
|
if (canonical2(value.routes) !== canonical2(routes) || canonical2(value.jobs) !== canonical2(jobs)) {
|
|
16361
16469
|
throw new Error("Delivery mappings do not match planned ownership.");
|
|
@@ -16406,7 +16514,8 @@ var init_delivery_build_schema = __esm(() => {
|
|
|
16406
16514
|
routes: _Array_(_Object_({
|
|
16407
16515
|
method: String2(),
|
|
16408
16516
|
path: String2(),
|
|
16409
|
-
target: String2()
|
|
16517
|
+
target: String2(),
|
|
16518
|
+
idempotency: Optional(Union([Literal("required"), Literal("none")]))
|
|
16410
16519
|
}, closed4)),
|
|
16411
16520
|
jobs: _Array_(_Object_({ name: String2(), target: String2() }, closed4))
|
|
16412
16521
|
}, closed4);
|
|
@@ -19536,6 +19645,10 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
|
|
|
19536
19645
|
const idempotency = commandModeProp(options, "idempotency");
|
|
19537
19646
|
const audit = stringLiteralProp(options, "audit");
|
|
19538
19647
|
const job = stringLiteralProp(options, "job");
|
|
19648
|
+
const invalidateTagsExpr = getProp(options, "invalidateTags");
|
|
19649
|
+
const invalidateTags = invalidateTagsExpr === undefined ? undefined : ts7.isArrayLiteralExpression(invalidateTagsExpr) && invalidateTagsExpr.elements.every((element) => ts7.isStringLiteral(element) && element.text.trim().length > 0) ? invalidateTagsExpr.elements.map((element) => element.text) : undefined;
|
|
19650
|
+
if (invalidateTagsExpr !== undefined && invalidateTags === undefined)
|
|
19651
|
+
return invalid();
|
|
19539
19652
|
const persistenceExpr = getProp(options, "persistence");
|
|
19540
19653
|
const persistence = persistenceExpr && ts7.isObjectLiteralExpression(persistenceExpr) ? parseFeaturePersistence(persistenceExpr) : undefined;
|
|
19541
19654
|
if (persistenceExpr && !persistence)
|
|
@@ -19552,6 +19665,7 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
|
|
|
19552
19665
|
...idempotency === undefined ? {} : { idempotency },
|
|
19553
19666
|
...audit === undefined ? {} : { audit },
|
|
19554
19667
|
...job === undefined ? {} : { job },
|
|
19668
|
+
...invalidateTags === undefined ? {} : { invalidateTags },
|
|
19555
19669
|
...persistence === undefined ? {} : { persistence }
|
|
19556
19670
|
});
|
|
19557
19671
|
}
|
|
@@ -19654,6 +19768,54 @@ function checkedCommandMode(object, property, ctx, command) {
|
|
|
19654
19768
|
}
|
|
19655
19769
|
return value;
|
|
19656
19770
|
}
|
|
19771
|
+
function checkedRouteIdempotency(object, ctx, route) {
|
|
19772
|
+
const expression = getProp(object, "idempotency");
|
|
19773
|
+
const value = stringLiteralProp(object, "idempotency");
|
|
19774
|
+
if (expression && value !== "required" && value !== "none") {
|
|
19775
|
+
const file = sourcePath(ctx.rootDir, expression.getSourceFile().fileName);
|
|
19776
|
+
ctx.diagnostics.push({
|
|
19777
|
+
severity: "error",
|
|
19778
|
+
code: "invalid-route-idempotency",
|
|
19779
|
+
errorCode: "SC3028",
|
|
19780
|
+
docsUrl: "https://supacloud.dev/errors/SC3028",
|
|
19781
|
+
message: `${route}.idempotency must be the explicit literal "required" or "none".`,
|
|
19782
|
+
file,
|
|
19783
|
+
line: lineOf(expression),
|
|
19784
|
+
suggestion: `Choose the intended idempotency policy explicitly; use "required" for replayable mutations or "none" for routes without that contract.`
|
|
19785
|
+
});
|
|
19786
|
+
}
|
|
19787
|
+
return value === "required" || value === "none" ? value : undefined;
|
|
19788
|
+
}
|
|
19789
|
+
function checkedRouteTags(expression, ctx, route) {
|
|
19790
|
+
if (expression === undefined)
|
|
19791
|
+
return;
|
|
19792
|
+
if (!ts7.isArrayLiteralExpression(expression) || expression.elements.some((element) => !ts7.isStringLiteral(element) || element.text.trim().length === 0)) {
|
|
19793
|
+
ctx.diagnostics.push({
|
|
19794
|
+
severity: "error",
|
|
19795
|
+
code: "invalid-route-tags",
|
|
19796
|
+
errorCode: "SC3029",
|
|
19797
|
+
docsUrl: "https://supacloud.dev/errors/SC3029",
|
|
19798
|
+
message: `${route}.tags must be a static array of non-empty string literals.`,
|
|
19799
|
+
file: sourcePath(ctx.rootDir, expression.getSourceFile().fileName),
|
|
19800
|
+
line: lineOf(expression),
|
|
19801
|
+
suggestion: 'Declare stable cache tags explicitly, for example tags: ["cases.list"].'
|
|
19802
|
+
});
|
|
19803
|
+
return;
|
|
19804
|
+
}
|
|
19805
|
+
const tags = expression.elements.map((element) => element.text);
|
|
19806
|
+
if (new Set(tags).size !== tags.length) {
|
|
19807
|
+
ctx.diagnostics.push({
|
|
19808
|
+
severity: "error",
|
|
19809
|
+
code: "duplicate-route-tags",
|
|
19810
|
+
errorCode: "SC3030",
|
|
19811
|
+
docsUrl: "https://supacloud.dev/errors/SC3030",
|
|
19812
|
+
message: `${route}.tags must not contain duplicate values.`,
|
|
19813
|
+
file: sourcePath(ctx.rootDir, expression.getSourceFile().fileName),
|
|
19814
|
+
line: lineOf(expression)
|
|
19815
|
+
});
|
|
19816
|
+
}
|
|
19817
|
+
return tags;
|
|
19818
|
+
}
|
|
19657
19819
|
function parseProvider(el, exportsSet, ctx) {
|
|
19658
19820
|
const file = sourcePath(ctx.rootDir, el.getSourceFile().fileName);
|
|
19659
19821
|
const line = lineOf(el);
|
|
@@ -20433,7 +20595,11 @@ function parseController(input, ctx) {
|
|
|
20433
20595
|
if (optionsObject) {
|
|
20434
20596
|
const parsePolicy = routePolicyProperty(optionsObject, "parse", ctx);
|
|
20435
20597
|
const deletePolicy = routePolicyProperty(optionsObject, "allowDeleteBody", ctx);
|
|
20436
|
-
|
|
20598
|
+
const idempotency = checkedRouteIdempotency(optionsObject, ctx, route.handler);
|
|
20599
|
+
if (idempotency !== undefined)
|
|
20600
|
+
route.idempotency = idempotency;
|
|
20601
|
+
const tagsPolicy = routePolicyProperty(optionsObject, "tags", ctx);
|
|
20602
|
+
if (parsePolicy.unresolved || deletePolicy.unresolved || tagsPolicy.unresolved) {
|
|
20437
20603
|
ctx.diagnostics.push({
|
|
20438
20604
|
severity: "error",
|
|
20439
20605
|
code: "unresolved-route-body-policy",
|
|
@@ -20441,6 +20607,9 @@ function parseController(input, ctx) {
|
|
|
20441
20607
|
message: `Route ${route.handler} body policy must be statically resolvable; dynamic spreads, computed keys and shorthand policies are unsupported.`
|
|
20442
20608
|
});
|
|
20443
20609
|
}
|
|
20610
|
+
const tags = checkedRouteTags(tagsPolicy.value, ctx, route.handler);
|
|
20611
|
+
if (tags !== undefined)
|
|
20612
|
+
route.tags = tags;
|
|
20444
20613
|
const parse = parsePolicy.value;
|
|
20445
20614
|
if (parse) {
|
|
20446
20615
|
if (ts7.isStringLiteral(parse) && parse.text === "none")
|
|
@@ -22289,6 +22458,7 @@ function validateClientSchemaBoundaries(graph, options) {
|
|
|
22289
22458
|
|
|
22290
22459
|
// src/effect-governance.ts
|
|
22291
22460
|
init_validate();
|
|
22461
|
+
init_feature();
|
|
22292
22462
|
function diagnostic(code, message, file, suggestion) {
|
|
22293
22463
|
const metadata = COMPILER_DIAGNOSTIC_CODES[code];
|
|
22294
22464
|
return {
|
|
@@ -22326,8 +22496,10 @@ function validateEffectPolicies(graph, options = {}) {
|
|
|
22326
22496
|
if (effect.retry === "explicit" && effect.maxAttempts === undefined) {
|
|
22327
22497
|
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."));
|
|
22328
22498
|
}
|
|
22329
|
-
if (
|
|
22330
|
-
const
|
|
22499
|
+
if (effect.retry === "explicit") {
|
|
22500
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
22501
|
+
const commandRef = route.command ?? featureTransition?.command;
|
|
22502
|
+
const command = commandRef === undefined ? undefined : commands.get(commandRef) ?? [...commands.values()].find((candidate) => candidate.name === commandRef);
|
|
22331
22503
|
if (command && command.idempotency !== "required") {
|
|
22332
22504
|
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".'));
|
|
22333
22505
|
}
|
|
@@ -23544,6 +23716,7 @@ init_graphql_options();
|
|
|
23544
23716
|
|
|
23545
23717
|
// src/delivery-plan.ts
|
|
23546
23718
|
import { createHash as createHash10 } from "node:crypto";
|
|
23719
|
+
init_feature();
|
|
23547
23720
|
init_validate();
|
|
23548
23721
|
var compare2 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
23549
23722
|
var sorted = (values) => [...new Set(values)].sort(compare2);
|
|
@@ -23663,6 +23836,10 @@ function createDeliveryPlan(graph, input, validation) {
|
|
|
23663
23836
|
fail("delivery-route-conflict", `Public route "${key}" is owned by both "${existing}" and "${target.name}".`, "Give public routes unambiguous method/path contracts before splitting targets.");
|
|
23664
23837
|
}
|
|
23665
23838
|
publicRoutes.set(key, target.name);
|
|
23839
|
+
const command = route.command === undefined ? undefined : module.commands.find((candidate) => candidate.className === route.command || candidate.name === route.command);
|
|
23840
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
23841
|
+
const effectiveCommand = route.command ?? featureTransition?.command;
|
|
23842
|
+
const idempotency = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency;
|
|
23666
23843
|
target.roots.push(module.name);
|
|
23667
23844
|
target.routes.push({
|
|
23668
23845
|
module: module.name,
|
|
@@ -23670,7 +23847,8 @@ function createDeliveryPlan(graph, input, validation) {
|
|
|
23670
23847
|
handler: route.handler,
|
|
23671
23848
|
method: route.method,
|
|
23672
23849
|
path,
|
|
23673
|
-
...
|
|
23850
|
+
...effectiveCommand === undefined ? {} : { command: effectiveCommand },
|
|
23851
|
+
...idempotency === undefined ? {} : { idempotency },
|
|
23674
23852
|
reason: owner === undefined ? "default-http" : "explicit-module"
|
|
23675
23853
|
});
|
|
23676
23854
|
}
|
|
@@ -24702,7 +24880,12 @@ async function buildDeliveryProject(options, delivery) {
|
|
|
24702
24880
|
deploymentReady: false,
|
|
24703
24881
|
plan: planned.plan,
|
|
24704
24882
|
objects,
|
|
24705
|
-
routes: planned.plan.targets.flatMap((target) => target.routes.map((route) => ({
|
|
24883
|
+
routes: planned.plan.targets.flatMap((target) => target.routes.map((route) => ({
|
|
24884
|
+
method: route.method,
|
|
24885
|
+
path: route.path,
|
|
24886
|
+
target: target.name,
|
|
24887
|
+
...route.idempotency === undefined ? {} : { idempotency: route.idempotency }
|
|
24888
|
+
}))),
|
|
24706
24889
|
jobs: planned.plan.targets.flatMap((target) => target.jobs.map((job) => ({ name: job.name, target: target.name })))
|
|
24707
24890
|
});
|
|
24708
24891
|
phase = "artifact-integrity";
|
|
@@ -24768,10 +24951,10 @@ import { readFileSync as readFileSync4 } from "node:fs";
|
|
|
24768
24951
|
import { resolve as resolve18 } from "node:path";
|
|
24769
24952
|
|
|
24770
24953
|
// src/migration-app-version.ts
|
|
24771
|
-
var MIGRATION_APP_VERSION = "0.
|
|
24954
|
+
var MIGRATION_APP_VERSION = "0.30.0";
|
|
24772
24955
|
|
|
24773
24956
|
// src/migration-elysia-version.ts
|
|
24774
|
-
var MIGRATION_ELYSIA_VERSION = "0.
|
|
24957
|
+
var MIGRATION_ELYSIA_VERSION = "0.35.0";
|
|
24775
24958
|
|
|
24776
24959
|
// src/migration-policy.ts
|
|
24777
24960
|
var cachedCompilerVersion;
|
|
@@ -25807,8 +25990,8 @@ function validFeatureInventory(value, jobs, commands, routes, rpc) {
|
|
|
25807
25990
|
return false;
|
|
25808
25991
|
}
|
|
25809
25992
|
const commandName = transition["command"];
|
|
25993
|
+
const command = commandName === undefined ? undefined : commands.find((candidate) => isRecord2(candidate) && (candidate["name"] === commandName || candidate["className"] === commandName));
|
|
25810
25994
|
if (commandName !== undefined) {
|
|
25811
|
-
const command = commands.find((candidate) => isRecord2(candidate) && (candidate["name"] === commandName || candidate["className"] === commandName));
|
|
25812
25995
|
if (!isRecord2(command))
|
|
25813
25996
|
return false;
|
|
25814
25997
|
if (transition["permission"] !== undefined && command["permission"] !== transition["permission"])
|
|
@@ -25839,9 +26022,21 @@ function validFeatureInventory(value, jobs, commands, routes, rpc) {
|
|
|
25839
26022
|
if (routeBinding === "controller") {
|
|
25840
26023
|
if (route["command"] !== undefined)
|
|
25841
26024
|
return false;
|
|
25842
|
-
} else
|
|
25843
|
-
|
|
26025
|
+
} else {
|
|
26026
|
+
if (typeof route["command"] !== "string")
|
|
26027
|
+
return false;
|
|
26028
|
+
const routeCommand = commands.find((candidate) => isRecord2(candidate) && (candidate["name"] === route["command"] || candidate["className"] === route["command"]));
|
|
26029
|
+
if (!isRecord2(routeCommand) || !isRecord2(command) || routeCommand["name"] !== command["name"])
|
|
26030
|
+
return false;
|
|
25844
26031
|
}
|
|
26032
|
+
if (transition["permission"] !== undefined && route["permission"] !== transition["permission"])
|
|
26033
|
+
return false;
|
|
26034
|
+
if (transition["idempotency"] !== undefined && route["idempotency"] !== transition["idempotency"])
|
|
26035
|
+
return false;
|
|
26036
|
+
if (persistence !== undefined && route["idempotency"] !== "required")
|
|
26037
|
+
return false;
|
|
26038
|
+
if (route["idempotency"] === "required" && (route["method"] === "GET" || route["method"] === "HEAD" || route["method"] === "OPTIONS"))
|
|
26039
|
+
return false;
|
|
25845
26040
|
}
|
|
25846
26041
|
const job = transition["job"];
|
|
25847
26042
|
if (job === undefined)
|