@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/index.js
CHANGED
|
@@ -248,6 +248,10 @@ function findClosestMatch(target, candidates) {
|
|
|
248
248
|
var REQUEST_CONTEXT_TOKEN_NAME = "supacloud.request-context", JOB_CONTEXT_TOKEN_NAME = "supacloud.job-context";
|
|
249
249
|
|
|
250
250
|
// src/feature.ts
|
|
251
|
+
function featureTransitionForRoute(module, controller, route) {
|
|
252
|
+
const routePath = `${route.method} ${joinRoutePaths(controller.path, route.path)}`;
|
|
253
|
+
return module.featureSpec?.transitions.find((transition) => transition.route === routePath);
|
|
254
|
+
}
|
|
251
255
|
function validateFeatureSpec(spec, module) {
|
|
252
256
|
const diagnostics = [];
|
|
253
257
|
const error = (code, message) => {
|
|
@@ -277,6 +281,11 @@ function validateFeatureSpec(spec, module) {
|
|
|
277
281
|
if (transition.permission !== undefined && !transition.permission.trim()) {
|
|
278
282
|
error("feature-governance-drift", `Transition ${transition.name} declares an empty permission.`);
|
|
279
283
|
}
|
|
284
|
+
if (transition.invalidateTags !== undefined) {
|
|
285
|
+
if (transition.invalidateTags.length === 0 || transition.invalidateTags.some((tag) => !tag.trim()) || new Set(transition.invalidateTags).size !== transition.invalidateTags.length) {
|
|
286
|
+
error("feature-invalidation-tags-invalid", `Transition ${transition.name} must declare unique, non-empty invalidateTags.`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
280
289
|
if (transition.persistence) {
|
|
281
290
|
const persistence = transition.persistence;
|
|
282
291
|
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")) {
|
|
@@ -369,7 +378,7 @@ function validateFeatureSpec(spec, module) {
|
|
|
369
378
|
const routes = module.controllers.flatMap((controller) => controller.routes.filter((route) => `${route.method} ${joinRoutePaths(controller.path, route.path)}` === transition.route));
|
|
370
379
|
if (routes.length !== 1) {
|
|
371
380
|
error("feature-route-unresolved", `Transition ${transition.name} must reference exactly one route '${transition.route}' in module ${module.name}.`);
|
|
372
|
-
} else if (command && transition.routeBinding !== "controller" && routes[0]?.command !== command.className) {
|
|
381
|
+
} else if (command && transition.routeBinding !== "controller" && routes[0]?.command !== command.className && routes[0]?.command !== command.name) {
|
|
373
382
|
error("feature-route-drift", `Route ${transition.route} is not bound to ${command.className}.`);
|
|
374
383
|
} else if (transition.routeBinding === "controller" && routes[0]?.command !== undefined) {
|
|
375
384
|
error("feature-route-drift", `Controller-bound route ${transition.route} must not also declare a framework command binding.`);
|
|
@@ -614,13 +623,17 @@ function validateGraph(graph, options = false) {
|
|
|
614
623
|
handler: route.handler,
|
|
615
624
|
...route.redirectTo === undefined ? {} : { redirectTo: route.redirectTo }
|
|
616
625
|
});
|
|
617
|
-
|
|
618
|
-
|
|
626
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
627
|
+
const effectiveCommandRef = route.command ?? featureTransition?.command;
|
|
628
|
+
const effectiveCommand = effectiveCommandRef === undefined ? undefined : module.commands.find((command) => command.className === effectiveCommandRef || command.name === effectiveCommandRef);
|
|
629
|
+
const effectiveIdempotency = route.idempotency ?? featureTransition?.idempotency;
|
|
630
|
+
if (route.command && !effectiveCommand) {
|
|
631
|
+
error("route-command-unresolved", `路由 ${key} 绑定的 command ${route.command} 未在模块 ${module.name} 声明`, controller.file);
|
|
619
632
|
}
|
|
620
|
-
if ((route.method === "GET" || route.method === "HEAD") &&
|
|
621
|
-
const boundCommand =
|
|
633
|
+
if ((route.method === "GET" || route.method === "HEAD") && effectiveCommand) {
|
|
634
|
+
const boundCommand = effectiveCommand;
|
|
622
635
|
if (boundCommand && boundCommand.transaction === "required") {
|
|
623
|
-
error("command-transaction-readonly", `GET route '${route.path}' in '${controller.className}.${route.handler}' binds mutating command '${
|
|
636
|
+
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'.`);
|
|
624
637
|
}
|
|
625
638
|
}
|
|
626
639
|
if (typeof options === "object" && options.allowRouteCommandBindings === false && route.command) {
|
|
@@ -691,6 +704,9 @@ function validateGraph(graph, options = false) {
|
|
|
691
704
|
if (route.allowDeleteBody && (route.method !== "DELETE" || !route.body)) {
|
|
692
705
|
error("invalid-delete-body-opt-in", `Route ${controller.className}.${route.handler} allowDeleteBody requires DELETE and an explicit body schema.`, controller.file);
|
|
693
706
|
}
|
|
707
|
+
if (effectiveIdempotency === "required" && (route.method === "GET" || route.method === "HEAD" || route.method === "OPTIONS")) {
|
|
708
|
+
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.`);
|
|
709
|
+
}
|
|
694
710
|
if (route.parse === "none" && (route.body || route.hasBodyBinding || route.contract?.body !== "domain" || !route.contract.evidence?.trim())) {
|
|
695
711
|
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);
|
|
696
712
|
}
|
|
@@ -1365,6 +1381,7 @@ var init_validate = __esm(() => {
|
|
|
1365
1381
|
"command-transaction-unsupported": { code: "SC4026", docsUrl: "https://supacloud.dev/errors/SC4026" },
|
|
1366
1382
|
"command-reconcile-required": { code: "SC4028", docsUrl: "https://supacloud.dev/errors/SC4028" },
|
|
1367
1383
|
"invalid-route-contract": { code: "SC3020", docsUrl: "https://supacloud.dev/errors/SC3020" },
|
|
1384
|
+
"invalid-route-idempotency": { code: "SC3028", docsUrl: "https://supacloud.dev/errors/SC3028" },
|
|
1368
1385
|
"route-contract-required": { code: "SC3021", docsUrl: "https://supacloud.dev/errors/SC3021" },
|
|
1369
1386
|
"route-contract-unverified": { code: "SC3022", docsUrl: "https://supacloud.dev/errors/SC3022" },
|
|
1370
1387
|
"route-contract-evidence-required": { code: "SC3023", docsUrl: "https://supacloud.dev/errors/SC3023" },
|
|
@@ -1511,6 +1528,7 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1511
1528
|
...transition.idempotency === undefined ? {} : { idempotency: transition.idempotency },
|
|
1512
1529
|
...transition.audit === undefined ? {} : { audit: transition.audit },
|
|
1513
1530
|
...transition.job === undefined ? {} : { job: transition.job },
|
|
1531
|
+
...transition.invalidateTags === undefined ? {} : { invalidateTags: [...transition.invalidateTags] },
|
|
1514
1532
|
...job === undefined ? {} : {
|
|
1515
1533
|
jobDetails: {
|
|
1516
1534
|
name: job.name,
|
|
@@ -1531,9 +1549,14 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1531
1549
|
}
|
|
1532
1550
|
for (const controller of module.controllers) {
|
|
1533
1551
|
for (const route of controller.routes) {
|
|
1534
|
-
const command = route.command ? module.commands.find((candidate) => candidate.className === route.command) : undefined;
|
|
1535
|
-
|
|
1536
|
-
|
|
1552
|
+
const command = route.command ? module.commands.find((candidate) => candidate.className === route.command || candidate.name === route.command) : undefined;
|
|
1553
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
1554
|
+
const featureCommand = featureTransition?.command === undefined ? undefined : module.commands.find((candidate) => candidate.className === featureTransition.command || candidate.name === featureTransition.command);
|
|
1555
|
+
const governedCommand = command ?? featureCommand;
|
|
1556
|
+
const effectiveCommand = route.command ?? featureTransition?.command;
|
|
1557
|
+
const idempotency = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency;
|
|
1558
|
+
if (governedCommand?.permission)
|
|
1559
|
+
permissions.add(governedCommand.permission);
|
|
1537
1560
|
if (route.contract?.evidence)
|
|
1538
1561
|
fixtures.add(route.contract.evidence);
|
|
1539
1562
|
routes.push({
|
|
@@ -1542,8 +1565,10 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1542
1565
|
module: module.name,
|
|
1543
1566
|
controller: controller.className,
|
|
1544
1567
|
handler: route.handler,
|
|
1545
|
-
...
|
|
1546
|
-
...
|
|
1568
|
+
...effectiveCommand === undefined ? {} : { command: effectiveCommand },
|
|
1569
|
+
...governedCommand?.permission === undefined ? {} : { permission: governedCommand.permission },
|
|
1570
|
+
...idempotency === undefined ? {} : { idempotency },
|
|
1571
|
+
...route.tags === undefined ? {} : { tags: [...route.tags] },
|
|
1547
1572
|
requestSchemas: Object.fromEntries(["body", "params", "query", "headers", "cookie"].flatMap((key) => route[key] === undefined ? [] : [[key, route[key]]])),
|
|
1548
1573
|
...route.response === undefined ? {} : { responseSchema: route.response },
|
|
1549
1574
|
...route.responses === undefined ? {} : {
|
|
@@ -1582,7 +1607,9 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1582
1607
|
}
|
|
1583
1608
|
};
|
|
1584
1609
|
}
|
|
1585
|
-
var init_contract_manifest = () => {
|
|
1610
|
+
var init_contract_manifest = __esm(() => {
|
|
1611
|
+
init_feature();
|
|
1612
|
+
});
|
|
1586
1613
|
|
|
1587
1614
|
// src/generate.ts
|
|
1588
1615
|
import { createHash as createHash4 } from "node:crypto";
|
|
@@ -1994,6 +2021,9 @@ class ModuleGenerator {
|
|
|
1994
2021
|
return "[]";
|
|
1995
2022
|
const items = this.module.controllers.map((controller) => {
|
|
1996
2023
|
const routes = controller.routes.map((route) => {
|
|
2024
|
+
const command = route.command === undefined ? undefined : this.module.commands.find((candidate) => candidate.className === route.command || candidate.name === route.command);
|
|
2025
|
+
const featureTransition = featureTransitionForRoute(this.module, controller, route);
|
|
2026
|
+
const idempotency = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency;
|
|
1997
2027
|
const responseEntries = normalizedResponseEntries(route.responses);
|
|
1998
2028
|
const hasResponseMap = responseEntries.length > 0;
|
|
1999
2029
|
const fields = [
|
|
@@ -2020,6 +2050,9 @@ class ModuleGenerator {
|
|
|
2020
2050
|
if (route.effect !== undefined) {
|
|
2021
2051
|
fields.push(`effect: ${JSON.stringify(route.effect)}`);
|
|
2022
2052
|
}
|
|
2053
|
+
if (idempotency !== undefined) {
|
|
2054
|
+
fields.push(`idempotency: ${JSON.stringify(idempotency)}`);
|
|
2055
|
+
}
|
|
2023
2056
|
if (route.command)
|
|
2024
2057
|
fields.push(`command: ${JSON.stringify(route.command)}`);
|
|
2025
2058
|
if (route.guards && route.guards.length > 0) {
|
|
@@ -2502,7 +2535,18 @@ function renderClient(graph, options) {
|
|
|
2502
2535
|
const usedTypeNames = new Set;
|
|
2503
2536
|
const allRoutes = [];
|
|
2504
2537
|
const allProcedures = [];
|
|
2505
|
-
const commandByClassName = new Map(graph.modules.flatMap((module) => module.commands.
|
|
2538
|
+
const commandByClassName = new Map(graph.modules.flatMap((module) => module.commands.flatMap((command) => [
|
|
2539
|
+
[command.className, command],
|
|
2540
|
+
[command.name, command]
|
|
2541
|
+
])));
|
|
2542
|
+
const featureInvalidationTags = new Map;
|
|
2543
|
+
for (const module of graph.modules) {
|
|
2544
|
+
for (const transition of module.featureSpec?.transitions ?? []) {
|
|
2545
|
+
if (transition.route && transition.invalidateTags) {
|
|
2546
|
+
featureInvalidationTags.set(`${module.name}:${transition.route}`, [...transition.invalidateTags]);
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2506
2550
|
let usesStatic = false;
|
|
2507
2551
|
let usesValue = false;
|
|
2508
2552
|
const uniqueTypeName = (candidate) => {
|
|
@@ -2580,6 +2624,13 @@ function renderClient(graph, options) {
|
|
|
2580
2624
|
const requestType = `ClientRequestOptions<${paramsType}, ${queryType}, ${bodyType}, ${headersType}, ${cookieType}>${requiredRequestFields.length > 0 ? ` & { ${requiredRequestFields.join("; ")} }` : ""}`;
|
|
2581
2625
|
routeTypes.push(`export type ${requestTypeName} = ${requestType};`);
|
|
2582
2626
|
routeTypes.push(`export type ${responseTypeName} = ${responseType === "never" ? "unknown" : responseType};`);
|
|
2627
|
+
const command = route.command === undefined ? undefined : commandByClassName.get(route.command);
|
|
2628
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
2629
|
+
const featureCommand = featureTransition?.command === undefined ? undefined : commandByClassName.get(featureTransition.command);
|
|
2630
|
+
const governedCommand = command ?? featureCommand;
|
|
2631
|
+
const idempotencyMode = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency ?? "none";
|
|
2632
|
+
const transitionRoute = `${route.method} ${fullPath}`;
|
|
2633
|
+
const invalidationTags = featureInvalidationTags.get(`${module.name}:${transitionRoute}`);
|
|
2583
2634
|
const routeFields = [
|
|
2584
2635
|
`method: ${JSON.stringify(route.method)}`,
|
|
2585
2636
|
`path: ${JSON.stringify(fullPath)}`,
|
|
@@ -2587,7 +2638,7 @@ function renderClient(graph, options) {
|
|
|
2587
2638
|
`handler: ${JSON.stringify(route.handler)}`
|
|
2588
2639
|
];
|
|
2589
2640
|
for (const [key, value] of [
|
|
2590
|
-
["command", route.command],
|
|
2641
|
+
["command", route.command ?? featureTransition?.command],
|
|
2591
2642
|
["guards", route.guards],
|
|
2592
2643
|
["canMatch", route.canMatch],
|
|
2593
2644
|
["canDeactivate", route.canDeactivate],
|
|
@@ -2601,7 +2652,10 @@ function renderClient(graph, options) {
|
|
|
2601
2652
|
["title", route.title],
|
|
2602
2653
|
["data", route.data],
|
|
2603
2654
|
["parse", route.parse],
|
|
2604
|
-
["allowDeleteBody", route.allowDeleteBody]
|
|
2655
|
+
["allowDeleteBody", route.allowDeleteBody],
|
|
2656
|
+
["idempotency", idempotencyMode === "none" ? undefined : idempotencyMode],
|
|
2657
|
+
["tags", route.tags],
|
|
2658
|
+
["invalidateTags", invalidationTags]
|
|
2605
2659
|
]) {
|
|
2606
2660
|
if (value !== undefined)
|
|
2607
2661
|
routeFields.push(`${key}: ${JSON.stringify(value)}`);
|
|
@@ -2634,9 +2688,7 @@ function renderClient(graph, options) {
|
|
|
2634
2688
|
const responseSchemaEntries = hasResponseMap ? Object.entries(responseSchemas).map(([status, ref]) => [status, ref.local]) : responseSchema ? [["200", responseSchema.local]] : [];
|
|
2635
2689
|
const responseSchemaArgument = responseSchemaEntries.length > 0 ? `{ ${responseSchemaEntries.map(([status, local]) => `${JSON.stringify(status)}: ${local}`).join(", ")} }` : "undefined";
|
|
2636
2690
|
const responseKindArgument = responseKind === undefined ? "undefined" : JSON.stringify(responseKind);
|
|
2637
|
-
const command = route.command === undefined ? undefined : commandByClassName.get(route.command);
|
|
2638
2691
|
const procedureKind = route.method === "GET" || route.method === "HEAD" ? "query" : "mutation";
|
|
2639
|
-
const idempotencyMode = command?.idempotency ?? "none";
|
|
2640
2692
|
const procedureMetadata = {
|
|
2641
2693
|
key: `${controller.className}.${route.handler}`,
|
|
2642
2694
|
method: route.method,
|
|
@@ -2644,7 +2696,9 @@ function renderClient(graph, options) {
|
|
|
2644
2696
|
controller: controller.className,
|
|
2645
2697
|
handler: route.handler,
|
|
2646
2698
|
kind: procedureKind,
|
|
2647
|
-
idempotency: idempotencyMode
|
|
2699
|
+
idempotency: idempotencyMode,
|
|
2700
|
+
...route.tags === undefined ? {} : { tags: route.tags },
|
|
2701
|
+
...invalidationTags === undefined ? {} : { invalidateTags: invalidationTags }
|
|
2648
2702
|
};
|
|
2649
2703
|
allProcedures.push({
|
|
2650
2704
|
method: route.method,
|
|
@@ -2652,9 +2706,11 @@ function renderClient(graph, options) {
|
|
|
2652
2706
|
controller: controller.className,
|
|
2653
2707
|
handler: route.handler,
|
|
2654
2708
|
kind: procedureKind,
|
|
2655
|
-
...
|
|
2656
|
-
...
|
|
2657
|
-
idempotency: idempotencyMode
|
|
2709
|
+
...governedCommand?.name === undefined ? {} : { command: governedCommand.name },
|
|
2710
|
+
...governedCommand?.permission === undefined ? {} : { permission: governedCommand.permission },
|
|
2711
|
+
idempotency: idempotencyMode,
|
|
2712
|
+
...route.tags === undefined ? {} : { tags: [...route.tags] },
|
|
2713
|
+
...invalidationTags === undefined ? {} : { invalidateTags: [...invalidationTags] }
|
|
2658
2714
|
});
|
|
2659
2715
|
routeMethods.push(`
|
|
2660
2716
|
${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)}),`);
|
|
@@ -2708,6 +2764,8 @@ ${procedureMethods.join(`
|
|
|
2708
2764
|
" handler: string;",
|
|
2709
2765
|
" kind: Kind;",
|
|
2710
2766
|
" idempotency: Idempotency;",
|
|
2767
|
+
" tags?: readonly string[];",
|
|
2768
|
+
" invalidateTags?: readonly string[];",
|
|
2711
2769
|
"}",
|
|
2712
2770
|
"",
|
|
2713
2771
|
"export interface ProcedureExecutionOptions {",
|
|
@@ -2813,6 +2871,8 @@ ${procedureMethods.join(`
|
|
|
2813
2871
|
" command?: string;",
|
|
2814
2872
|
" permission?: string;",
|
|
2815
2873
|
" idempotency: IdempotencyMode;",
|
|
2874
|
+
" tags?: readonly string[];",
|
|
2875
|
+
" invalidateTags?: readonly string[];",
|
|
2816
2876
|
"}",
|
|
2817
2877
|
"",
|
|
2818
2878
|
...routeTypes,
|
|
@@ -3312,7 +3372,10 @@ function renderOpenApi(graph, options) {
|
|
|
3312
3372
|
}
|
|
3313
3373
|
return schemaComponents.get(key);
|
|
3314
3374
|
};
|
|
3315
|
-
const commandByClass = new Map(graph.modules.flatMap((module) => module.commands.
|
|
3375
|
+
const commandByClass = new Map(graph.modules.flatMap((module) => module.commands.flatMap((command) => [
|
|
3376
|
+
[command.className, command],
|
|
3377
|
+
[command.name, command]
|
|
3378
|
+
])));
|
|
3316
3379
|
for (const module of graph.modules) {
|
|
3317
3380
|
for (const controller of module.controllers) {
|
|
3318
3381
|
for (const route of controller.routes) {
|
|
@@ -3334,7 +3397,12 @@ function renderOpenApi(graph, options) {
|
|
|
3334
3397
|
const operationBase = pascalName(`${module.name}_${controller.className}_${route.handler}`);
|
|
3335
3398
|
const operationId = uniqueName(operationBase, operationIds);
|
|
3336
3399
|
const command = route.command ? commandByClass.get(route.command) : undefined;
|
|
3337
|
-
const
|
|
3400
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
3401
|
+
const featureCommand = featureTransition?.command === undefined ? undefined : commandByClass.get(featureTransition.command);
|
|
3402
|
+
const governedCommand = command ?? featureCommand;
|
|
3403
|
+
const idempotencyMode = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency ?? "none";
|
|
3404
|
+
const permission = governedCommand?.permission ?? featureTransition?.permission;
|
|
3405
|
+
const requiresAuth = (route.guards?.length ?? 0) > 0 || permission !== undefined || governedCommand !== undefined;
|
|
3338
3406
|
const responseKind = route.contract?.response;
|
|
3339
3407
|
const responseContentType = responseKind === "binary" || responseKind === "stream" ? "application/octet-stream" : "application/json";
|
|
3340
3408
|
const definition = {
|
|
@@ -3359,11 +3427,12 @@ function renderOpenApi(graph, options) {
|
|
|
3359
3427
|
responseKind: responseKind ?? (route.nativeResponse ? "native-response" : undefined),
|
|
3360
3428
|
...route.parse === undefined ? {} : { parse: route.parse },
|
|
3361
3429
|
...route.allowDeleteBody === undefined ? {} : { allowDeleteBody: route.allowDeleteBody },
|
|
3430
|
+
...idempotencyMode === "none" ? {} : { idempotency: idempotencyMode },
|
|
3362
3431
|
module: module.name,
|
|
3363
3432
|
controller: controller.className,
|
|
3364
3433
|
handler: route.handler,
|
|
3365
|
-
...route.command === undefined ? {} : { command: route.command },
|
|
3366
|
-
...
|
|
3434
|
+
...route.command === undefined && featureTransition?.command === undefined ? {} : { command: route.command ?? featureTransition?.command },
|
|
3435
|
+
...permission === undefined ? {} : { permission },
|
|
3367
3436
|
...route.guards && route.guards.length > 0 ? { guards: route.guards } : {},
|
|
3368
3437
|
...route.canMatch && route.canMatch.length > 0 ? { canMatch: route.canMatch } : {},
|
|
3369
3438
|
...route.canDeactivate && route.canDeactivate.length > 0 ? { canDeactivate: route.canDeactivate } : {},
|
|
@@ -3438,6 +3507,7 @@ function renderOpenApi(graph, options) {
|
|
|
3438
3507
|
" handler: string;",
|
|
3439
3508
|
" command?: string;",
|
|
3440
3509
|
" permission?: string;",
|
|
3510
|
+
' idempotency?: "required" | "none";',
|
|
3441
3511
|
" guards?: readonly string[];",
|
|
3442
3512
|
" canMatch?: readonly string[];",
|
|
3443
3513
|
" canDeactivate?: readonly string[];",
|
|
@@ -3690,6 +3760,18 @@ function renderOpenApi(graph, options) {
|
|
|
3690
3760
|
" for (const name of headersSchema ? schemaPropertyNames(headersSchema) : []) parameters.push({",
|
|
3691
3761
|
' name, in: "header", required: schemaRequired(headersSchema, name), schema: schemaProperty(headersSchema, name),',
|
|
3692
3762
|
" });",
|
|
3763
|
+
' if (definition.idempotency === "required") {',
|
|
3764
|
+
' const keySchema = { type: "string", minLength: 1, maxLength: 512, pattern: "^[A-Za-z0-9._:-]{1,512}$" };',
|
|
3765
|
+
' const declaredHeaders = parameters.filter((parameter) => parameter["in"] === "header" && typeof parameter["name"] === "string" && parameter["name"].toLowerCase() === "idempotency-key");',
|
|
3766
|
+
" const header = declaredHeaders[0];",
|
|
3767
|
+
" if (header) {",
|
|
3768
|
+
' header["required"] = true;',
|
|
3769
|
+
' header["schema"] = { allOf: [...declaredHeaders.map((parameter) => parameter["schema"]), keySchema] };',
|
|
3770
|
+
" for (const duplicate of declaredHeaders.slice(1)) parameters.splice(parameters.indexOf(duplicate), 1);",
|
|
3771
|
+
" } else {",
|
|
3772
|
+
' parameters.push({ name: "Idempotency-Key", in: "header", required: true, schema: keySchema });',
|
|
3773
|
+
" }",
|
|
3774
|
+
" }",
|
|
3693
3775
|
" const cookieSchema = definition.cookieSchema ? OPENAPI_SCHEMA_REGISTRY[definition.cookieSchema as keyof typeof OPENAPI_SCHEMA_REGISTRY] : undefined;",
|
|
3694
3776
|
" for (const name of cookieSchema ? schemaPropertyNames(cookieSchema) : []) parameters.push({",
|
|
3695
3777
|
' name, in: "cookie", required: schemaRequired(cookieSchema, name), schema: schemaProperty(cookieSchema, name),',
|
|
@@ -3712,6 +3794,7 @@ function renderOpenApi(graph, options) {
|
|
|
3712
3794
|
' responses["500"] = { description: "Internal server error", content: { "application/json": { schema: { $ref: "#/components/schemas/SupaCloudError" } } } };',
|
|
3713
3795
|
' responses["200"] = { description: "Successful response" };',
|
|
3714
3796
|
" }",
|
|
3797
|
+
' 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" } } } };',
|
|
3715
3798
|
" const operation: Record<string, unknown> = {",
|
|
3716
3799
|
" operationId: definition.operationId, tags: [...definition.tags], summary: definition.summary, responses,",
|
|
3717
3800
|
" ...(parameters.length === 0 ? {} : { parameters }),",
|
|
@@ -3722,6 +3805,7 @@ function renderOpenApi(graph, options) {
|
|
|
3722
3805
|
" module: definition.module, controller: definition.controller, handler: definition.handler,",
|
|
3723
3806
|
" ...(definition.command ? { command: definition.command } : {}),",
|
|
3724
3807
|
" ...(definition.permission ? { permission: definition.permission } : {}),",
|
|
3808
|
+
" ...(definition.idempotency ? { idempotency: definition.idempotency } : {}),",
|
|
3725
3809
|
" ...(definition.guards ? { guards: [...definition.guards] } : {}),",
|
|
3726
3810
|
" ...(definition.canMatch ? { canMatch: [...definition.canMatch] } : {}),",
|
|
3727
3811
|
" ...(definition.canDeactivate ? { canDeactivate: [...definition.canDeactivate] } : {}),",
|
|
@@ -3772,11 +3856,13 @@ function renderPermissions(graph) {
|
|
|
3772
3856
|
}
|
|
3773
3857
|
for (const controller of module.controllers) {
|
|
3774
3858
|
for (const route of controller.routes) {
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3859
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
3860
|
+
const command = route.command === undefined ? undefined : module.commands.find((candidate) => candidate.className === route.command || candidate.name === route.command);
|
|
3861
|
+
const featureCommand = featureTransition?.command === undefined ? undefined : module.commands.find((candidate) => candidate.className === featureTransition.command || candidate.name === featureTransition.command);
|
|
3862
|
+
const governedCommand = command ?? featureCommand;
|
|
3863
|
+
const effectiveCommand = route.command ?? featureTransition?.command;
|
|
3864
|
+
const perm = governedCommand?.permission ?? featureTransition?.permission;
|
|
3865
|
+
const idempotency = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency;
|
|
3780
3866
|
if (perm)
|
|
3781
3867
|
permissions.add(perm);
|
|
3782
3868
|
bindings.push({
|
|
@@ -3784,8 +3870,9 @@ function renderPermissions(graph) {
|
|
|
3784
3870
|
path: joinRoutePaths(controller.path, route.path),
|
|
3785
3871
|
controller: controller.className,
|
|
3786
3872
|
handler: route.handler,
|
|
3787
|
-
...
|
|
3788
|
-
...perm === undefined ? {} : { permission: perm }
|
|
3873
|
+
...effectiveCommand === undefined ? {} : { command: effectiveCommand },
|
|
3874
|
+
...perm === undefined ? {} : { permission: perm },
|
|
3875
|
+
...idempotency === undefined ? {} : { idempotency }
|
|
3789
3876
|
});
|
|
3790
3877
|
}
|
|
3791
3878
|
}
|
|
@@ -3811,6 +3898,7 @@ function renderPermissions(graph) {
|
|
|
3811
3898
|
" handler: string;",
|
|
3812
3899
|
" command?: string;",
|
|
3813
3900
|
" permission?: string;",
|
|
3901
|
+
' idempotency?: "required" | "none";',
|
|
3814
3902
|
"}",
|
|
3815
3903
|
"",
|
|
3816
3904
|
"export const RoutePermissions: RoutePermissionBinding[] = " + JSON.stringify(bindings, null, 2) + ";",
|
|
@@ -3854,6 +3942,8 @@ var HEADER = "// GENERATED BY @supacloud/compiler — do not edit", INTERFACES =
|
|
|
3854
3942
|
/** True when the handler returns a native Response rather than a framework value. */
|
|
3855
3943
|
nativeResponse?: boolean;
|
|
3856
3944
|
command?: string;
|
|
3945
|
+
/** Transport-level idempotency contract, independent of route command binding. */
|
|
3946
|
+
idempotency?: "required" | "none";
|
|
3857
3947
|
guards?: string[];
|
|
3858
3948
|
canMatch?: string[];
|
|
3859
3949
|
canDeactivate?: string[];
|
|
@@ -4091,6 +4181,7 @@ async function destroyServiceInstances(
|
|
|
4091
4181
|
`;
|
|
4092
4182
|
var init_generate = __esm(() => {
|
|
4093
4183
|
init_contract_manifest();
|
|
4184
|
+
init_feature();
|
|
4094
4185
|
});
|
|
4095
4186
|
|
|
4096
4187
|
// src/route-contracts.ts
|
|
@@ -14813,7 +14904,7 @@ function parseDeliveryPlanResult(value) {
|
|
|
14813
14904
|
throw new Error("Invalid delivery plan result.");
|
|
14814
14905
|
return value;
|
|
14815
14906
|
}
|
|
14816
|
-
var objectOptions, reference, references, kind, isolation, DeliveryFilePathSchema, functionTarget, DeliveryOptionsSchema, DeliveryDiagnosticSchema, routeSchema, jobSchema, DeliveryTargetSchema, DeliveryPlanSchema, DeliveryPlanResultSchema, DeliveryConfigurationError;
|
|
14907
|
+
var objectOptions, reference, references, kind, isolation, mode, DeliveryFilePathSchema, functionTarget, DeliveryOptionsSchema, DeliveryDiagnosticSchema, routeSchema, jobSchema, DeliveryTargetSchema, DeliveryPlanSchema, DeliveryPlanResultSchema, DeliveryConfigurationError;
|
|
14817
14908
|
var init_delivery_schema = __esm(() => {
|
|
14818
14909
|
init_build();
|
|
14819
14910
|
init_value2();
|
|
@@ -14823,6 +14914,7 @@ var init_delivery_schema = __esm(() => {
|
|
|
14823
14914
|
references = _Array_(reference, { uniqueItems: true });
|
|
14824
14915
|
kind = Union([Literal("api"), Literal("jobs"), Literal("webhook"), Literal("function")]);
|
|
14825
14916
|
isolation = Union([Literal("shared"), Literal("process")]);
|
|
14917
|
+
mode = Union([Literal("required"), Literal("none")]);
|
|
14826
14918
|
DeliveryFilePathSchema = String2({
|
|
14827
14919
|
maxLength: 512,
|
|
14828
14920
|
pattern: "^(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*/)*[A-Za-z0-9_-][A-Za-z0-9_.-]*$"
|
|
@@ -14896,6 +14988,7 @@ var init_delivery_schema = __esm(() => {
|
|
|
14896
14988
|
]),
|
|
14897
14989
|
path: String2(),
|
|
14898
14990
|
command: Optional(String2()),
|
|
14991
|
+
idempotency: Optional(mode),
|
|
14899
14992
|
reason: Union([Literal("default-http"), Literal("explicit-module")])
|
|
14900
14993
|
}, objectOptions);
|
|
14901
14994
|
jobSchema = _Object_({
|
|
@@ -15154,7 +15247,12 @@ function parseDeliveryBuildManifest(value) {
|
|
|
15154
15247
|
throw new Error("Invalid delivery object inventory.");
|
|
15155
15248
|
}
|
|
15156
15249
|
}
|
|
15157
|
-
const routes = value.plan.targets.flatMap((target) => target.routes.map((route) => ({
|
|
15250
|
+
const routes = value.plan.targets.flatMap((target) => target.routes.map((route) => ({
|
|
15251
|
+
method: route.method,
|
|
15252
|
+
path: route.path,
|
|
15253
|
+
target: target.name,
|
|
15254
|
+
...route.idempotency === undefined ? {} : { idempotency: route.idempotency }
|
|
15255
|
+
})));
|
|
15158
15256
|
const jobs = value.plan.targets.flatMap((target) => target.jobs.map((job) => ({ name: job.name, target: target.name })));
|
|
15159
15257
|
if (canonical2(value.routes) !== canonical2(routes) || canonical2(value.jobs) !== canonical2(jobs)) {
|
|
15160
15258
|
throw new Error("Delivery mappings do not match planned ownership.");
|
|
@@ -15205,7 +15303,8 @@ var init_delivery_build_schema = __esm(() => {
|
|
|
15205
15303
|
routes: _Array_(_Object_({
|
|
15206
15304
|
method: String2(),
|
|
15207
15305
|
path: String2(),
|
|
15208
|
-
target: String2()
|
|
15306
|
+
target: String2(),
|
|
15307
|
+
idempotency: Optional(Union([Literal("required"), Literal("none")]))
|
|
15209
15308
|
}, closed2)),
|
|
15210
15309
|
jobs: _Array_(_Object_({ name: String2(), target: String2() }, closed2))
|
|
15211
15310
|
}, closed2);
|
|
@@ -15424,7 +15523,9 @@ function createExecutionPlans(graph) {
|
|
|
15424
15523
|
const aspects = (boundary, refs = []) => refs.map((ref, index) => `${boundary}.aspect[${index}]:${ref.name}`);
|
|
15425
15524
|
return graph.modules.flatMap((module) => [
|
|
15426
15525
|
...module.controllers.flatMap((controller) => controller.routes.map((route) => {
|
|
15427
|
-
const
|
|
15526
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
15527
|
+
const commandRef = route.command ?? featureTransition?.command;
|
|
15528
|
+
const command = module.commands.find((item) => item.className === commandRef || item.name === commandRef);
|
|
15428
15529
|
const path = `${controller.path}/${route.path}`.replace(/\/+/g, "/");
|
|
15429
15530
|
return {
|
|
15430
15531
|
module: module.name,
|
|
@@ -15574,6 +15675,7 @@ function exportGraphDot(graph) {
|
|
|
15574
15675
|
}
|
|
15575
15676
|
var init_inspect = __esm(() => {
|
|
15576
15677
|
init_route_contracts();
|
|
15678
|
+
init_feature();
|
|
15577
15679
|
});
|
|
15578
15680
|
|
|
15579
15681
|
// src/repair-plan.ts
|
|
@@ -15827,7 +15929,7 @@ function parseDevelopmentContext(input) {
|
|
|
15827
15929
|
throw invalid();
|
|
15828
15930
|
}
|
|
15829
15931
|
}
|
|
15830
|
-
var APPLICATION_DEVELOPMENT_SCHEMA = "supacloud.application-development.v1", APPLICATION_DEVELOPMENT_ARCHIVE_MAX_BYTES = 524288, APPLICATION_DEVELOPMENT_LIMITS, DevelopmentContractError, closed4, text2, texts, count,
|
|
15932
|
+
var APPLICATION_DEVELOPMENT_SCHEMA = "supacloud.application-development.v1", APPLICATION_DEVELOPMENT_ARCHIVE_MAX_BYTES = 524288, APPLICATION_DEVELOPMENT_LIMITS, DevelopmentContractError, closed4, text2, texts, count, mode2, operations2, resourceUse, schemaKind, stage, ContextSchema;
|
|
15831
15933
|
var init_application_development = __esm(() => {
|
|
15832
15934
|
init_build();
|
|
15833
15935
|
init_value2();
|
|
@@ -15855,7 +15957,7 @@ var init_application_development = __esm(() => {
|
|
|
15855
15957
|
text2 = String2({ minLength: 1, maxLength: 512, pattern: "^[^\\u0000-\\u001f\\u007f]+$" });
|
|
15856
15958
|
texts = _Array_(text2);
|
|
15857
15959
|
count = Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER });
|
|
15858
|
-
|
|
15960
|
+
mode2 = Union([Literal("required"), Literal("none")]);
|
|
15859
15961
|
operations2 = _Array_(Union([Literal("read"), Literal("write"), Literal("publish"), Literal("consume")]));
|
|
15860
15962
|
resourceUse = _Object_({ resource: text2, operations: operations2 }, closed4);
|
|
15861
15963
|
schemaKind = Optional(Union([Literal("opaque"), Literal("declared")]));
|
|
@@ -15890,6 +15992,7 @@ var init_application_development = __esm(() => {
|
|
|
15890
15992
|
controller: text2,
|
|
15891
15993
|
handler: text2,
|
|
15892
15994
|
command: Optional(text2),
|
|
15995
|
+
idempotency: Optional(mode2),
|
|
15893
15996
|
aspects: texts,
|
|
15894
15997
|
schemaKinds: Optional(_Object_({
|
|
15895
15998
|
body: schemaKind,
|
|
@@ -15904,8 +16007,8 @@ var init_application_development = __esm(() => {
|
|
|
15904
16007
|
module: text2,
|
|
15905
16008
|
name: text2,
|
|
15906
16009
|
permission: Optional(text2),
|
|
15907
|
-
transaction:
|
|
15908
|
-
idempotency:
|
|
16010
|
+
transaction: mode2,
|
|
16011
|
+
idempotency: mode2,
|
|
15909
16012
|
audit: Optional(text2),
|
|
15910
16013
|
resources: _Array_(resourceUse)
|
|
15911
16014
|
}, closed4), { maxItems: APPLICATION_DEVELOPMENT_LIMITS.commands }),
|
|
@@ -16012,7 +16115,11 @@ function createApplicationDevelopmentContext(graph, options = {}) {
|
|
|
16012
16115
|
for (const controller of module.controllers) {
|
|
16013
16116
|
for (const route of controller.routes) {
|
|
16014
16117
|
const candidates = module.commands.filter((command) => command.className === route.command || command.name === route.command);
|
|
16015
|
-
const
|
|
16118
|
+
const commandNode = candidates.length === 1 ? candidates[0] : undefined;
|
|
16119
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
16120
|
+
const featureCommandNode = featureTransition?.command === undefined ? undefined : module.commands.find((candidate) => candidate.className === featureTransition.command || candidate.name === featureTransition.command);
|
|
16121
|
+
const command = commandNode?.name ?? featureCommandNode?.name;
|
|
16122
|
+
const idempotency = route.idempotency ?? commandNode?.idempotency ?? featureTransition?.idempotency;
|
|
16016
16123
|
const kinds = schemaKinds(route);
|
|
16017
16124
|
routes.push({
|
|
16018
16125
|
module: module.name,
|
|
@@ -16021,6 +16128,7 @@ function createApplicationDevelopmentContext(graph, options = {}) {
|
|
|
16021
16128
|
controller: controller.className,
|
|
16022
16129
|
handler: route.handler,
|
|
16023
16130
|
...command === undefined ? {} : { command },
|
|
16131
|
+
...idempotency === undefined ? {} : { idempotency },
|
|
16024
16132
|
aspects: (route.aspects ?? []).map((aspect) => aspect.name),
|
|
16025
16133
|
...kinds === undefined ? {} : { schemaKinds: kinds }
|
|
16026
16134
|
});
|
|
@@ -16141,6 +16249,7 @@ var init_application_development2 = __esm(() => {
|
|
|
16141
16249
|
init_application_development();
|
|
16142
16250
|
init_inspect();
|
|
16143
16251
|
init_execution_snapshot();
|
|
16252
|
+
init_feature();
|
|
16144
16253
|
APPLICATION_DEVELOPMENT_LIMITS2 = APPLICATION_DEVELOPMENT_LIMITS;
|
|
16145
16254
|
ApplicationDevelopmentError = class ApplicationDevelopmentError extends Error {
|
|
16146
16255
|
code;
|
|
@@ -19495,6 +19604,10 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
|
|
|
19495
19604
|
const idempotency = commandModeProp(options, "idempotency");
|
|
19496
19605
|
const audit = stringLiteralProp(options, "audit");
|
|
19497
19606
|
const job = stringLiteralProp(options, "job");
|
|
19607
|
+
const invalidateTagsExpr = getProp(options, "invalidateTags");
|
|
19608
|
+
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;
|
|
19609
|
+
if (invalidateTagsExpr !== undefined && invalidateTags === undefined)
|
|
19610
|
+
return invalid();
|
|
19498
19611
|
const persistenceExpr = getProp(options, "persistence");
|
|
19499
19612
|
const persistence = persistenceExpr && ts7.isObjectLiteralExpression(persistenceExpr) ? parseFeaturePersistence(persistenceExpr) : undefined;
|
|
19500
19613
|
if (persistenceExpr && !persistence)
|
|
@@ -19511,6 +19624,7 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
|
|
|
19511
19624
|
...idempotency === undefined ? {} : { idempotency },
|
|
19512
19625
|
...audit === undefined ? {} : { audit },
|
|
19513
19626
|
...job === undefined ? {} : { job },
|
|
19627
|
+
...invalidateTags === undefined ? {} : { invalidateTags },
|
|
19514
19628
|
...persistence === undefined ? {} : { persistence }
|
|
19515
19629
|
});
|
|
19516
19630
|
}
|
|
@@ -19613,6 +19727,54 @@ function checkedCommandMode(object, property, ctx, command) {
|
|
|
19613
19727
|
}
|
|
19614
19728
|
return value;
|
|
19615
19729
|
}
|
|
19730
|
+
function checkedRouteIdempotency(object, ctx, route) {
|
|
19731
|
+
const expression = getProp(object, "idempotency");
|
|
19732
|
+
const value = stringLiteralProp(object, "idempotency");
|
|
19733
|
+
if (expression && value !== "required" && value !== "none") {
|
|
19734
|
+
const file = sourcePath(ctx.rootDir, expression.getSourceFile().fileName);
|
|
19735
|
+
ctx.diagnostics.push({
|
|
19736
|
+
severity: "error",
|
|
19737
|
+
code: "invalid-route-idempotency",
|
|
19738
|
+
errorCode: "SC3028",
|
|
19739
|
+
docsUrl: "https://supacloud.dev/errors/SC3028",
|
|
19740
|
+
message: `${route}.idempotency must be the explicit literal "required" or "none".`,
|
|
19741
|
+
file,
|
|
19742
|
+
line: lineOf(expression),
|
|
19743
|
+
suggestion: `Choose the intended idempotency policy explicitly; use "required" for replayable mutations or "none" for routes without that contract.`
|
|
19744
|
+
});
|
|
19745
|
+
}
|
|
19746
|
+
return value === "required" || value === "none" ? value : undefined;
|
|
19747
|
+
}
|
|
19748
|
+
function checkedRouteTags(expression, ctx, route) {
|
|
19749
|
+
if (expression === undefined)
|
|
19750
|
+
return;
|
|
19751
|
+
if (!ts7.isArrayLiteralExpression(expression) || expression.elements.some((element) => !ts7.isStringLiteral(element) || element.text.trim().length === 0)) {
|
|
19752
|
+
ctx.diagnostics.push({
|
|
19753
|
+
severity: "error",
|
|
19754
|
+
code: "invalid-route-tags",
|
|
19755
|
+
errorCode: "SC3029",
|
|
19756
|
+
docsUrl: "https://supacloud.dev/errors/SC3029",
|
|
19757
|
+
message: `${route}.tags must be a static array of non-empty string literals.`,
|
|
19758
|
+
file: sourcePath(ctx.rootDir, expression.getSourceFile().fileName),
|
|
19759
|
+
line: lineOf(expression),
|
|
19760
|
+
suggestion: 'Declare stable cache tags explicitly, for example tags: ["cases.list"].'
|
|
19761
|
+
});
|
|
19762
|
+
return;
|
|
19763
|
+
}
|
|
19764
|
+
const tags = expression.elements.map((element) => element.text);
|
|
19765
|
+
if (new Set(tags).size !== tags.length) {
|
|
19766
|
+
ctx.diagnostics.push({
|
|
19767
|
+
severity: "error",
|
|
19768
|
+
code: "duplicate-route-tags",
|
|
19769
|
+
errorCode: "SC3030",
|
|
19770
|
+
docsUrl: "https://supacloud.dev/errors/SC3030",
|
|
19771
|
+
message: `${route}.tags must not contain duplicate values.`,
|
|
19772
|
+
file: sourcePath(ctx.rootDir, expression.getSourceFile().fileName),
|
|
19773
|
+
line: lineOf(expression)
|
|
19774
|
+
});
|
|
19775
|
+
}
|
|
19776
|
+
return tags;
|
|
19777
|
+
}
|
|
19616
19778
|
function parseProvider(el, exportsSet, ctx) {
|
|
19617
19779
|
const file = sourcePath(ctx.rootDir, el.getSourceFile().fileName);
|
|
19618
19780
|
const line = lineOf(el);
|
|
@@ -20392,7 +20554,11 @@ function parseController(input, ctx) {
|
|
|
20392
20554
|
if (optionsObject) {
|
|
20393
20555
|
const parsePolicy = routePolicyProperty(optionsObject, "parse", ctx);
|
|
20394
20556
|
const deletePolicy = routePolicyProperty(optionsObject, "allowDeleteBody", ctx);
|
|
20395
|
-
|
|
20557
|
+
const idempotency = checkedRouteIdempotency(optionsObject, ctx, route.handler);
|
|
20558
|
+
if (idempotency !== undefined)
|
|
20559
|
+
route.idempotency = idempotency;
|
|
20560
|
+
const tagsPolicy = routePolicyProperty(optionsObject, "tags", ctx);
|
|
20561
|
+
if (parsePolicy.unresolved || deletePolicy.unresolved || tagsPolicy.unresolved) {
|
|
20396
20562
|
ctx.diagnostics.push({
|
|
20397
20563
|
severity: "error",
|
|
20398
20564
|
code: "unresolved-route-body-policy",
|
|
@@ -20400,6 +20566,9 @@ function parseController(input, ctx) {
|
|
|
20400
20566
|
message: `Route ${route.handler} body policy must be statically resolvable; dynamic spreads, computed keys and shorthand policies are unsupported.`
|
|
20401
20567
|
});
|
|
20402
20568
|
}
|
|
20569
|
+
const tags = checkedRouteTags(tagsPolicy.value, ctx, route.handler);
|
|
20570
|
+
if (tags !== undefined)
|
|
20571
|
+
route.tags = tags;
|
|
20403
20572
|
const parse = parsePolicy.value;
|
|
20404
20573
|
if (parse) {
|
|
20405
20574
|
if (ts7.isStringLiteral(parse) && parse.text === "none")
|
|
@@ -22250,6 +22419,7 @@ function validateClientSchemaBoundaries(graph, options) {
|
|
|
22250
22419
|
|
|
22251
22420
|
// src/effect-governance.ts
|
|
22252
22421
|
init_validate();
|
|
22422
|
+
init_feature();
|
|
22253
22423
|
function diagnostic(code, message, file, suggestion) {
|
|
22254
22424
|
const metadata = COMPILER_DIAGNOSTIC_CODES[code];
|
|
22255
22425
|
return {
|
|
@@ -22287,8 +22457,10 @@ function validateEffectPolicies(graph, options = {}) {
|
|
|
22287
22457
|
if (effect.retry === "explicit" && effect.maxAttempts === undefined) {
|
|
22288
22458
|
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."));
|
|
22289
22459
|
}
|
|
22290
|
-
if (
|
|
22291
|
-
const
|
|
22460
|
+
if (effect.retry === "explicit") {
|
|
22461
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
22462
|
+
const commandRef = route.command ?? featureTransition?.command;
|
|
22463
|
+
const command = commandRef === undefined ? undefined : commands.get(commandRef) ?? [...commands.values()].find((candidate) => candidate.name === commandRef);
|
|
22292
22464
|
if (command && command.idempotency !== "required") {
|
|
22293
22465
|
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".'));
|
|
22294
22466
|
}
|
|
@@ -22543,6 +22715,7 @@ function resolveTypeSafety(options) {
|
|
|
22543
22715
|
init_delivery_schema();
|
|
22544
22716
|
|
|
22545
22717
|
// src/delivery-plan.ts
|
|
22718
|
+
init_feature();
|
|
22546
22719
|
init_validate();
|
|
22547
22720
|
var compare2 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
22548
22721
|
var sorted = (values) => [...new Set(values)].sort(compare2);
|
|
@@ -22662,6 +22835,10 @@ function createDeliveryPlan(graph, input, validation) {
|
|
|
22662
22835
|
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.");
|
|
22663
22836
|
}
|
|
22664
22837
|
publicRoutes.set(key, target.name);
|
|
22838
|
+
const command = route.command === undefined ? undefined : module.commands.find((candidate) => candidate.className === route.command || candidate.name === route.command);
|
|
22839
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
22840
|
+
const effectiveCommand = route.command ?? featureTransition?.command;
|
|
22841
|
+
const idempotency = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency;
|
|
22665
22842
|
target.roots.push(module.name);
|
|
22666
22843
|
target.routes.push({
|
|
22667
22844
|
module: module.name,
|
|
@@ -22669,7 +22846,8 @@ function createDeliveryPlan(graph, input, validation) {
|
|
|
22669
22846
|
handler: route.handler,
|
|
22670
22847
|
method: route.method,
|
|
22671
22848
|
path,
|
|
22672
|
-
...
|
|
22849
|
+
...effectiveCommand === undefined ? {} : { command: effectiveCommand },
|
|
22850
|
+
...idempotency === undefined ? {} : { idempotency },
|
|
22673
22851
|
reason: owner === undefined ? "default-http" : "explicit-module"
|
|
22674
22852
|
});
|
|
22675
22853
|
}
|
|
@@ -23700,7 +23878,12 @@ async function buildDeliveryProject(options, delivery) {
|
|
|
23700
23878
|
deploymentReady: false,
|
|
23701
23879
|
plan: planned.plan,
|
|
23702
23880
|
objects,
|
|
23703
|
-
routes: planned.plan.targets.flatMap((target) => target.routes.map((route) => ({
|
|
23881
|
+
routes: planned.plan.targets.flatMap((target) => target.routes.map((route) => ({
|
|
23882
|
+
method: route.method,
|
|
23883
|
+
path: route.path,
|
|
23884
|
+
target: target.name,
|
|
23885
|
+
...route.idempotency === undefined ? {} : { idempotency: route.idempotency }
|
|
23886
|
+
}))),
|
|
23704
23887
|
jobs: planned.plan.targets.flatMap((target) => target.jobs.map((job) => ({ name: job.name, target: target.name })))
|
|
23705
23888
|
});
|
|
23706
23889
|
phase = "artifact-integrity";
|
|
@@ -24450,10 +24633,10 @@ import { readFileSync as readFileSync4 } from "node:fs";
|
|
|
24450
24633
|
import { resolve as resolve17 } from "node:path";
|
|
24451
24634
|
|
|
24452
24635
|
// src/migration-app-version.ts
|
|
24453
|
-
var MIGRATION_APP_VERSION = "0.
|
|
24636
|
+
var MIGRATION_APP_VERSION = "0.30.0";
|
|
24454
24637
|
|
|
24455
24638
|
// src/migration-elysia-version.ts
|
|
24456
|
-
var MIGRATION_ELYSIA_VERSION = "0.
|
|
24639
|
+
var MIGRATION_ELYSIA_VERSION = "0.35.0";
|
|
24457
24640
|
|
|
24458
24641
|
// src/migration-policy.ts
|
|
24459
24642
|
var cachedCompilerVersion;
|
|
@@ -25488,8 +25671,8 @@ function validFeatureInventory(value, jobs, commands, routes, rpc) {
|
|
|
25488
25671
|
return false;
|
|
25489
25672
|
}
|
|
25490
25673
|
const commandName = transition["command"];
|
|
25674
|
+
const command = commandName === undefined ? undefined : commands.find((candidate) => isRecord2(candidate) && (candidate["name"] === commandName || candidate["className"] === commandName));
|
|
25491
25675
|
if (commandName !== undefined) {
|
|
25492
|
-
const command = commands.find((candidate) => isRecord2(candidate) && (candidate["name"] === commandName || candidate["className"] === commandName));
|
|
25493
25676
|
if (!isRecord2(command))
|
|
25494
25677
|
return false;
|
|
25495
25678
|
if (transition["permission"] !== undefined && command["permission"] !== transition["permission"])
|
|
@@ -25520,9 +25703,21 @@ function validFeatureInventory(value, jobs, commands, routes, rpc) {
|
|
|
25520
25703
|
if (routeBinding === "controller") {
|
|
25521
25704
|
if (route["command"] !== undefined)
|
|
25522
25705
|
return false;
|
|
25523
|
-
} else
|
|
25524
|
-
|
|
25706
|
+
} else {
|
|
25707
|
+
if (typeof route["command"] !== "string")
|
|
25708
|
+
return false;
|
|
25709
|
+
const routeCommand = commands.find((candidate) => isRecord2(candidate) && (candidate["name"] === route["command"] || candidate["className"] === route["command"]));
|
|
25710
|
+
if (!isRecord2(routeCommand) || !isRecord2(command) || routeCommand["name"] !== command["name"])
|
|
25711
|
+
return false;
|
|
25525
25712
|
}
|
|
25713
|
+
if (transition["permission"] !== undefined && route["permission"] !== transition["permission"])
|
|
25714
|
+
return false;
|
|
25715
|
+
if (transition["idempotency"] !== undefined && route["idempotency"] !== transition["idempotency"])
|
|
25716
|
+
return false;
|
|
25717
|
+
if (persistence !== undefined && route["idempotency"] !== "required")
|
|
25718
|
+
return false;
|
|
25719
|
+
if (route["idempotency"] === "required" && (route["method"] === "GET" || route["method"] === "HEAD" || route["method"] === "OPTIONS"))
|
|
25720
|
+
return false;
|
|
25526
25721
|
}
|
|
25527
25722
|
const job = transition["job"];
|
|
25528
25723
|
if (job === undefined)
|