@supacloud/compiler 0.48.0 → 0.50.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 +284 -47
- package/dist/contract-manifest.d.ts +5 -1
- package/dist/delivery-bundle-worker.js +3 -1
- package/dist/feature.d.ts +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +288 -51
- package/dist/migration-app-version.d.ts +1 -1
- package/dist/migration-elysia-version.d.ts +1 -1
- package/dist/types.d.ts +12 -0
- package/package.json +5 -5
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) => {
|
|
@@ -266,6 +270,21 @@ function validateFeatureSpec(spec, module) {
|
|
|
266
270
|
if (spec.tenantField !== undefined && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(spec.tenantField)) {
|
|
267
271
|
error("feature-tenant-boundary-invalid", `Feature ${spec.name} declares an unsafe tenant field.`);
|
|
268
272
|
}
|
|
273
|
+
const databaseTransition = spec.transitions.some((transition) => transition.persistence?.boundary === "database");
|
|
274
|
+
if (databaseTransition && spec.database === undefined && spec.file !== undefined) {
|
|
275
|
+
error("feature-database-source-required", `Database Feature ${spec.name} must declare its database source, table and rowType.`);
|
|
276
|
+
}
|
|
277
|
+
if (spec.database !== undefined) {
|
|
278
|
+
if (spec.database.source.startsWith("/") || spec.database.source.includes("\\") || spec.database.source.split("/").includes("..") || !/^[A-Za-z0-9._/-]+$/.test(spec.database.source)) {
|
|
279
|
+
error("feature-database-source-invalid", `Feature ${spec.name} declares an unsafe database source.`);
|
|
280
|
+
}
|
|
281
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*$/.test(spec.database.table)) {
|
|
282
|
+
error("feature-database-table-invalid", `Feature ${spec.name} must use a schema-qualified database table.`);
|
|
283
|
+
}
|
|
284
|
+
if (!/^[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)*$/.test(spec.database.rowType)) {
|
|
285
|
+
error("feature-database-row-type-invalid", `Feature ${spec.name} declares an unsafe database row type.`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
269
288
|
const names = new Set;
|
|
270
289
|
for (const transition of spec.transitions) {
|
|
271
290
|
if (!transition.name.trim() || names.has(transition.name)) {
|
|
@@ -278,6 +297,11 @@ function validateFeatureSpec(spec, module) {
|
|
|
278
297
|
if (transition.permission !== undefined && !transition.permission.trim()) {
|
|
279
298
|
error("feature-governance-drift", `Transition ${transition.name} declares an empty permission.`);
|
|
280
299
|
}
|
|
300
|
+
if (transition.invalidateTags !== undefined) {
|
|
301
|
+
if (transition.invalidateTags.length === 0 || transition.invalidateTags.some((tag) => !tag.trim()) || new Set(transition.invalidateTags).size !== transition.invalidateTags.length) {
|
|
302
|
+
error("feature-invalidation-tags-invalid", `Transition ${transition.name} must declare unique, non-empty invalidateTags.`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
281
305
|
if (transition.persistence) {
|
|
282
306
|
const persistence = transition.persistence;
|
|
283
307
|
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 +394,7 @@ function validateFeatureSpec(spec, module) {
|
|
|
370
394
|
const routes = module.controllers.flatMap((controller) => controller.routes.filter((route) => `${route.method} ${joinRoutePaths(controller.path, route.path)}` === transition.route));
|
|
371
395
|
if (routes.length !== 1) {
|
|
372
396
|
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) {
|
|
397
|
+
} else if (command && transition.routeBinding !== "controller" && routes[0]?.command !== command.className && routes[0]?.command !== command.name) {
|
|
374
398
|
error("feature-route-drift", `Route ${transition.route} is not bound to ${command.className}.`);
|
|
375
399
|
} else if (transition.routeBinding === "controller" && routes[0]?.command !== undefined) {
|
|
376
400
|
error("feature-route-drift", `Controller-bound route ${transition.route} must not also declare a framework command binding.`);
|
|
@@ -397,6 +421,7 @@ function generateFeatureSource(spec) {
|
|
|
397
421
|
const sourceSpec = {
|
|
398
422
|
name: spec.name,
|
|
399
423
|
states: spec.states,
|
|
424
|
+
...spec.database === undefined ? {} : { database: spec.database },
|
|
400
425
|
tenantField: spec.tenantField ?? "tenant_id",
|
|
401
426
|
transitions: Object.fromEntries(transitions.map((transition) => {
|
|
402
427
|
const { name, ...options } = transition;
|
|
@@ -615,13 +640,17 @@ function validateGraph(graph, options = false) {
|
|
|
615
640
|
handler: route.handler,
|
|
616
641
|
...route.redirectTo === undefined ? {} : { redirectTo: route.redirectTo }
|
|
617
642
|
});
|
|
618
|
-
|
|
619
|
-
|
|
643
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
644
|
+
const effectiveCommandRef = route.command ?? featureTransition?.command;
|
|
645
|
+
const effectiveCommand = effectiveCommandRef === undefined ? undefined : module.commands.find((command) => command.className === effectiveCommandRef || command.name === effectiveCommandRef);
|
|
646
|
+
const effectiveIdempotency = route.idempotency ?? featureTransition?.idempotency;
|
|
647
|
+
if (route.command && !effectiveCommand) {
|
|
648
|
+
error("route-command-unresolved", `路由 ${key} 绑定的 command ${route.command} 未在模块 ${module.name} 声明`, controller.file);
|
|
620
649
|
}
|
|
621
|
-
if ((route.method === "GET" || route.method === "HEAD") &&
|
|
622
|
-
const boundCommand =
|
|
650
|
+
if ((route.method === "GET" || route.method === "HEAD") && effectiveCommand) {
|
|
651
|
+
const boundCommand = effectiveCommand;
|
|
623
652
|
if (boundCommand && boundCommand.transaction === "required") {
|
|
624
|
-
error("command-transaction-readonly", `GET route '${route.path}' in '${controller.className}.${route.handler}' binds mutating command '${
|
|
653
|
+
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
654
|
}
|
|
626
655
|
}
|
|
627
656
|
if (typeof options === "object" && options.allowRouteCommandBindings === false && route.command) {
|
|
@@ -692,6 +721,9 @@ function validateGraph(graph, options = false) {
|
|
|
692
721
|
if (route.allowDeleteBody && (route.method !== "DELETE" || !route.body)) {
|
|
693
722
|
error("invalid-delete-body-opt-in", `Route ${controller.className}.${route.handler} allowDeleteBody requires DELETE and an explicit body schema.`, controller.file);
|
|
694
723
|
}
|
|
724
|
+
if (effectiveIdempotency === "required" && (route.method === "GET" || route.method === "HEAD" || route.method === "OPTIONS")) {
|
|
725
|
+
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.`);
|
|
726
|
+
}
|
|
695
727
|
if (route.parse === "none" && (route.body || route.hasBodyBinding || route.contract?.body !== "domain" || !route.contract.evidence?.trim())) {
|
|
696
728
|
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
729
|
}
|
|
@@ -1366,6 +1398,7 @@ var init_validate = __esm(() => {
|
|
|
1366
1398
|
"command-transaction-unsupported": { code: "SC4026", docsUrl: "https://supacloud.dev/errors/SC4026" },
|
|
1367
1399
|
"command-reconcile-required": { code: "SC4028", docsUrl: "https://supacloud.dev/errors/SC4028" },
|
|
1368
1400
|
"invalid-route-contract": { code: "SC3020", docsUrl: "https://supacloud.dev/errors/SC3020" },
|
|
1401
|
+
"invalid-route-idempotency": { code: "SC3028", docsUrl: "https://supacloud.dev/errors/SC3028" },
|
|
1369
1402
|
"route-contract-required": { code: "SC3021", docsUrl: "https://supacloud.dev/errors/SC3021" },
|
|
1370
1403
|
"route-contract-unverified": { code: "SC3022", docsUrl: "https://supacloud.dev/errors/SC3022" },
|
|
1371
1404
|
"route-contract-evidence-required": { code: "SC3023", docsUrl: "https://supacloud.dev/errors/SC3023" },
|
|
@@ -1417,6 +1450,10 @@ var init_validate = __esm(() => {
|
|
|
1417
1450
|
"feature-job-mode": { code: "SC6119", docsUrl: "https://supacloud.dev/errors/SC6119" },
|
|
1418
1451
|
"feature-job-contract": { code: "SC6120", docsUrl: "https://supacloud.dev/errors/SC6120" },
|
|
1419
1452
|
"feature-job-context": { code: "SC6121", docsUrl: "https://supacloud.dev/errors/SC6121" },
|
|
1453
|
+
"feature-database-source-required": { code: "SC6122", docsUrl: "https://supacloud.dev/errors/SC6122" },
|
|
1454
|
+
"feature-database-source-invalid": { code: "SC6123", docsUrl: "https://supacloud.dev/errors/SC6123" },
|
|
1455
|
+
"feature-database-table-invalid": { code: "SC6124", docsUrl: "https://supacloud.dev/errors/SC6124" },
|
|
1456
|
+
"feature-database-row-type-invalid": { code: "SC6125", docsUrl: "https://supacloud.dev/errors/SC6125" },
|
|
1420
1457
|
"feature-route-unresolved": { code: "SC6106", docsUrl: "https://supacloud.dev/errors/SC6106" },
|
|
1421
1458
|
"feature-route-drift": { code: "SC6107", docsUrl: "https://supacloud.dev/errors/SC6107" },
|
|
1422
1459
|
"invalid-feature-spec": { code: "SC6108", docsUrl: "https://supacloud.dev/errors/SC6108" }
|
|
@@ -1497,6 +1534,7 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1497
1534
|
features.push({
|
|
1498
1535
|
name: module.featureSpec.name,
|
|
1499
1536
|
states: [...module.featureSpec.states],
|
|
1537
|
+
...module.featureSpec.database === undefined ? {} : { database: module.featureSpec.database },
|
|
1500
1538
|
...module.featureSpec.tenantField === undefined ? {} : { tenantField: module.featureSpec.tenantField },
|
|
1501
1539
|
transitions: module.featureSpec.transitions.map((transition) => {
|
|
1502
1540
|
const job = transition.job === undefined ? undefined : (module.jobs ?? []).find((candidate) => candidate.className === transition.job || candidate.name === transition.job);
|
|
@@ -1512,6 +1550,7 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1512
1550
|
...transition.idempotency === undefined ? {} : { idempotency: transition.idempotency },
|
|
1513
1551
|
...transition.audit === undefined ? {} : { audit: transition.audit },
|
|
1514
1552
|
...transition.job === undefined ? {} : { job: transition.job },
|
|
1553
|
+
...transition.invalidateTags === undefined ? {} : { invalidateTags: [...transition.invalidateTags] },
|
|
1515
1554
|
...job === undefined ? {} : {
|
|
1516
1555
|
jobDetails: {
|
|
1517
1556
|
name: job.name,
|
|
@@ -1532,9 +1571,14 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1532
1571
|
}
|
|
1533
1572
|
for (const controller of module.controllers) {
|
|
1534
1573
|
for (const route of controller.routes) {
|
|
1535
|
-
const command = route.command ? module.commands.find((candidate) => candidate.className === route.command) : undefined;
|
|
1536
|
-
|
|
1537
|
-
|
|
1574
|
+
const command = route.command ? module.commands.find((candidate) => candidate.className === route.command || candidate.name === route.command) : undefined;
|
|
1575
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
1576
|
+
const featureCommand = featureTransition?.command === undefined ? undefined : module.commands.find((candidate) => candidate.className === featureTransition.command || candidate.name === featureTransition.command);
|
|
1577
|
+
const governedCommand = command ?? featureCommand;
|
|
1578
|
+
const effectiveCommand = route.command ?? featureTransition?.command;
|
|
1579
|
+
const idempotency = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency;
|
|
1580
|
+
if (governedCommand?.permission)
|
|
1581
|
+
permissions.add(governedCommand.permission);
|
|
1538
1582
|
if (route.contract?.evidence)
|
|
1539
1583
|
fixtures.add(route.contract.evidence);
|
|
1540
1584
|
routes.push({
|
|
@@ -1543,8 +1587,10 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1543
1587
|
module: module.name,
|
|
1544
1588
|
controller: controller.className,
|
|
1545
1589
|
handler: route.handler,
|
|
1546
|
-
...
|
|
1547
|
-
...
|
|
1590
|
+
...effectiveCommand === undefined ? {} : { command: effectiveCommand },
|
|
1591
|
+
...governedCommand?.permission === undefined ? {} : { permission: governedCommand.permission },
|
|
1592
|
+
...idempotency === undefined ? {} : { idempotency },
|
|
1593
|
+
...route.tags === undefined ? {} : { tags: [...route.tags] },
|
|
1548
1594
|
requestSchemas: Object.fromEntries(["body", "params", "query", "headers", "cookie"].flatMap((key) => route[key] === undefined ? [] : [[key, route[key]]])),
|
|
1549
1595
|
...route.response === undefined ? {} : { responseSchema: route.response },
|
|
1550
1596
|
...route.responses === undefined ? {} : {
|
|
@@ -1583,7 +1629,9 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1583
1629
|
}
|
|
1584
1630
|
};
|
|
1585
1631
|
}
|
|
1586
|
-
var init_contract_manifest = () => {
|
|
1632
|
+
var init_contract_manifest = __esm(() => {
|
|
1633
|
+
init_feature();
|
|
1634
|
+
});
|
|
1587
1635
|
|
|
1588
1636
|
// src/generate.ts
|
|
1589
1637
|
import { createHash as createHash4 } from "node:crypto";
|
|
@@ -1995,6 +2043,9 @@ class ModuleGenerator {
|
|
|
1995
2043
|
return "[]";
|
|
1996
2044
|
const items = this.module.controllers.map((controller) => {
|
|
1997
2045
|
const routes = controller.routes.map((route) => {
|
|
2046
|
+
const command = route.command === undefined ? undefined : this.module.commands.find((candidate) => candidate.className === route.command || candidate.name === route.command);
|
|
2047
|
+
const featureTransition = featureTransitionForRoute(this.module, controller, route);
|
|
2048
|
+
const idempotency = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency;
|
|
1998
2049
|
const responseEntries = normalizedResponseEntries(route.responses);
|
|
1999
2050
|
const hasResponseMap = responseEntries.length > 0;
|
|
2000
2051
|
const fields = [
|
|
@@ -2021,6 +2072,9 @@ class ModuleGenerator {
|
|
|
2021
2072
|
if (route.effect !== undefined) {
|
|
2022
2073
|
fields.push(`effect: ${JSON.stringify(route.effect)}`);
|
|
2023
2074
|
}
|
|
2075
|
+
if (idempotency !== undefined) {
|
|
2076
|
+
fields.push(`idempotency: ${JSON.stringify(idempotency)}`);
|
|
2077
|
+
}
|
|
2024
2078
|
if (route.command)
|
|
2025
2079
|
fields.push(`command: ${JSON.stringify(route.command)}`);
|
|
2026
2080
|
if (route.guards && route.guards.length > 0) {
|
|
@@ -2503,7 +2557,18 @@ function renderClient(graph, options) {
|
|
|
2503
2557
|
const usedTypeNames = new Set;
|
|
2504
2558
|
const allRoutes = [];
|
|
2505
2559
|
const allProcedures = [];
|
|
2506
|
-
const commandByClassName = new Map(graph.modules.flatMap((module) => module.commands.
|
|
2560
|
+
const commandByClassName = new Map(graph.modules.flatMap((module) => module.commands.flatMap((command) => [
|
|
2561
|
+
[command.className, command],
|
|
2562
|
+
[command.name, command]
|
|
2563
|
+
])));
|
|
2564
|
+
const featureInvalidationTags = new Map;
|
|
2565
|
+
for (const module of graph.modules) {
|
|
2566
|
+
for (const transition of module.featureSpec?.transitions ?? []) {
|
|
2567
|
+
if (transition.route && transition.invalidateTags) {
|
|
2568
|
+
featureInvalidationTags.set(`${module.name}:${transition.route}`, [...transition.invalidateTags]);
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2507
2572
|
let usesStatic = false;
|
|
2508
2573
|
let usesValue = false;
|
|
2509
2574
|
const uniqueTypeName = (candidate) => {
|
|
@@ -2581,6 +2646,13 @@ function renderClient(graph, options) {
|
|
|
2581
2646
|
const requestType = `ClientRequestOptions<${paramsType}, ${queryType}, ${bodyType}, ${headersType}, ${cookieType}>${requiredRequestFields.length > 0 ? ` & { ${requiredRequestFields.join("; ")} }` : ""}`;
|
|
2582
2647
|
routeTypes.push(`export type ${requestTypeName} = ${requestType};`);
|
|
2583
2648
|
routeTypes.push(`export type ${responseTypeName} = ${responseType === "never" ? "unknown" : responseType};`);
|
|
2649
|
+
const command = route.command === undefined ? undefined : commandByClassName.get(route.command);
|
|
2650
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
2651
|
+
const featureCommand = featureTransition?.command === undefined ? undefined : commandByClassName.get(featureTransition.command);
|
|
2652
|
+
const governedCommand = command ?? featureCommand;
|
|
2653
|
+
const idempotencyMode = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency ?? "none";
|
|
2654
|
+
const transitionRoute = `${route.method} ${fullPath}`;
|
|
2655
|
+
const invalidationTags = featureInvalidationTags.get(`${module.name}:${transitionRoute}`);
|
|
2584
2656
|
const routeFields = [
|
|
2585
2657
|
`method: ${JSON.stringify(route.method)}`,
|
|
2586
2658
|
`path: ${JSON.stringify(fullPath)}`,
|
|
@@ -2588,7 +2660,7 @@ function renderClient(graph, options) {
|
|
|
2588
2660
|
`handler: ${JSON.stringify(route.handler)}`
|
|
2589
2661
|
];
|
|
2590
2662
|
for (const [key, value] of [
|
|
2591
|
-
["command", route.command],
|
|
2663
|
+
["command", route.command ?? featureTransition?.command],
|
|
2592
2664
|
["guards", route.guards],
|
|
2593
2665
|
["canMatch", route.canMatch],
|
|
2594
2666
|
["canDeactivate", route.canDeactivate],
|
|
@@ -2602,7 +2674,10 @@ function renderClient(graph, options) {
|
|
|
2602
2674
|
["title", route.title],
|
|
2603
2675
|
["data", route.data],
|
|
2604
2676
|
["parse", route.parse],
|
|
2605
|
-
["allowDeleteBody", route.allowDeleteBody]
|
|
2677
|
+
["allowDeleteBody", route.allowDeleteBody],
|
|
2678
|
+
["idempotency", idempotencyMode === "none" ? undefined : idempotencyMode],
|
|
2679
|
+
["tags", route.tags],
|
|
2680
|
+
["invalidateTags", invalidationTags]
|
|
2606
2681
|
]) {
|
|
2607
2682
|
if (value !== undefined)
|
|
2608
2683
|
routeFields.push(`${key}: ${JSON.stringify(value)}`);
|
|
@@ -2635,9 +2710,7 @@ function renderClient(graph, options) {
|
|
|
2635
2710
|
const responseSchemaEntries = hasResponseMap ? Object.entries(responseSchemas).map(([status, ref]) => [status, ref.local]) : responseSchema ? [["200", responseSchema.local]] : [];
|
|
2636
2711
|
const responseSchemaArgument = responseSchemaEntries.length > 0 ? `{ ${responseSchemaEntries.map(([status, local]) => `${JSON.stringify(status)}: ${local}`).join(", ")} }` : "undefined";
|
|
2637
2712
|
const responseKindArgument = responseKind === undefined ? "undefined" : JSON.stringify(responseKind);
|
|
2638
|
-
const command = route.command === undefined ? undefined : commandByClassName.get(route.command);
|
|
2639
2713
|
const procedureKind = route.method === "GET" || route.method === "HEAD" ? "query" : "mutation";
|
|
2640
|
-
const idempotencyMode = command?.idempotency ?? "none";
|
|
2641
2714
|
const procedureMetadata = {
|
|
2642
2715
|
key: `${controller.className}.${route.handler}`,
|
|
2643
2716
|
method: route.method,
|
|
@@ -2645,7 +2718,9 @@ function renderClient(graph, options) {
|
|
|
2645
2718
|
controller: controller.className,
|
|
2646
2719
|
handler: route.handler,
|
|
2647
2720
|
kind: procedureKind,
|
|
2648
|
-
idempotency: idempotencyMode
|
|
2721
|
+
idempotency: idempotencyMode,
|
|
2722
|
+
...route.tags === undefined ? {} : { tags: route.tags },
|
|
2723
|
+
...invalidationTags === undefined ? {} : { invalidateTags: invalidationTags }
|
|
2649
2724
|
};
|
|
2650
2725
|
allProcedures.push({
|
|
2651
2726
|
method: route.method,
|
|
@@ -2653,9 +2728,11 @@ function renderClient(graph, options) {
|
|
|
2653
2728
|
controller: controller.className,
|
|
2654
2729
|
handler: route.handler,
|
|
2655
2730
|
kind: procedureKind,
|
|
2656
|
-
...
|
|
2657
|
-
...
|
|
2658
|
-
idempotency: idempotencyMode
|
|
2731
|
+
...governedCommand?.name === undefined ? {} : { command: governedCommand.name },
|
|
2732
|
+
...governedCommand?.permission === undefined ? {} : { permission: governedCommand.permission },
|
|
2733
|
+
idempotency: idempotencyMode,
|
|
2734
|
+
...route.tags === undefined ? {} : { tags: [...route.tags] },
|
|
2735
|
+
...invalidationTags === undefined ? {} : { invalidateTags: [...invalidationTags] }
|
|
2659
2736
|
});
|
|
2660
2737
|
routeMethods.push(`
|
|
2661
2738
|
${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 +2786,8 @@ ${procedureMethods.join(`
|
|
|
2709
2786
|
" handler: string;",
|
|
2710
2787
|
" kind: Kind;",
|
|
2711
2788
|
" idempotency: Idempotency;",
|
|
2789
|
+
" tags?: readonly string[];",
|
|
2790
|
+
" invalidateTags?: readonly string[];",
|
|
2712
2791
|
"}",
|
|
2713
2792
|
"",
|
|
2714
2793
|
"export interface ProcedureExecutionOptions {",
|
|
@@ -2814,6 +2893,8 @@ ${procedureMethods.join(`
|
|
|
2814
2893
|
" command?: string;",
|
|
2815
2894
|
" permission?: string;",
|
|
2816
2895
|
" idempotency: IdempotencyMode;",
|
|
2896
|
+
" tags?: readonly string[];",
|
|
2897
|
+
" invalidateTags?: readonly string[];",
|
|
2817
2898
|
"}",
|
|
2818
2899
|
"",
|
|
2819
2900
|
...routeTypes,
|
|
@@ -3313,7 +3394,10 @@ function renderOpenApi(graph, options) {
|
|
|
3313
3394
|
}
|
|
3314
3395
|
return schemaComponents.get(key);
|
|
3315
3396
|
};
|
|
3316
|
-
const commandByClass = new Map(graph.modules.flatMap((module) => module.commands.
|
|
3397
|
+
const commandByClass = new Map(graph.modules.flatMap((module) => module.commands.flatMap((command) => [
|
|
3398
|
+
[command.className, command],
|
|
3399
|
+
[command.name, command]
|
|
3400
|
+
])));
|
|
3317
3401
|
for (const module of graph.modules) {
|
|
3318
3402
|
for (const controller of module.controllers) {
|
|
3319
3403
|
for (const route of controller.routes) {
|
|
@@ -3335,7 +3419,12 @@ function renderOpenApi(graph, options) {
|
|
|
3335
3419
|
const operationBase = pascalName(`${module.name}_${controller.className}_${route.handler}`);
|
|
3336
3420
|
const operationId = uniqueName(operationBase, operationIds);
|
|
3337
3421
|
const command = route.command ? commandByClass.get(route.command) : undefined;
|
|
3338
|
-
const
|
|
3422
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
3423
|
+
const featureCommand = featureTransition?.command === undefined ? undefined : commandByClass.get(featureTransition.command);
|
|
3424
|
+
const governedCommand = command ?? featureCommand;
|
|
3425
|
+
const idempotencyMode = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency ?? "none";
|
|
3426
|
+
const permission = governedCommand?.permission ?? featureTransition?.permission;
|
|
3427
|
+
const requiresAuth = (route.guards?.length ?? 0) > 0 || permission !== undefined || governedCommand !== undefined;
|
|
3339
3428
|
const responseKind = route.contract?.response;
|
|
3340
3429
|
const responseContentType = responseKind === "binary" || responseKind === "stream" ? "application/octet-stream" : "application/json";
|
|
3341
3430
|
const definition = {
|
|
@@ -3360,11 +3449,12 @@ function renderOpenApi(graph, options) {
|
|
|
3360
3449
|
responseKind: responseKind ?? (route.nativeResponse ? "native-response" : undefined),
|
|
3361
3450
|
...route.parse === undefined ? {} : { parse: route.parse },
|
|
3362
3451
|
...route.allowDeleteBody === undefined ? {} : { allowDeleteBody: route.allowDeleteBody },
|
|
3452
|
+
...idempotencyMode === "none" ? {} : { idempotency: idempotencyMode },
|
|
3363
3453
|
module: module.name,
|
|
3364
3454
|
controller: controller.className,
|
|
3365
3455
|
handler: route.handler,
|
|
3366
|
-
...route.command === undefined ? {} : { command: route.command },
|
|
3367
|
-
...
|
|
3456
|
+
...route.command === undefined && featureTransition?.command === undefined ? {} : { command: route.command ?? featureTransition?.command },
|
|
3457
|
+
...permission === undefined ? {} : { permission },
|
|
3368
3458
|
...route.guards && route.guards.length > 0 ? { guards: route.guards } : {},
|
|
3369
3459
|
...route.canMatch && route.canMatch.length > 0 ? { canMatch: route.canMatch } : {},
|
|
3370
3460
|
...route.canDeactivate && route.canDeactivate.length > 0 ? { canDeactivate: route.canDeactivate } : {},
|
|
@@ -3439,6 +3529,7 @@ function renderOpenApi(graph, options) {
|
|
|
3439
3529
|
" handler: string;",
|
|
3440
3530
|
" command?: string;",
|
|
3441
3531
|
" permission?: string;",
|
|
3532
|
+
' idempotency?: "required" | "none";',
|
|
3442
3533
|
" guards?: readonly string[];",
|
|
3443
3534
|
" canMatch?: readonly string[];",
|
|
3444
3535
|
" canDeactivate?: readonly string[];",
|
|
@@ -3691,6 +3782,18 @@ function renderOpenApi(graph, options) {
|
|
|
3691
3782
|
" for (const name of headersSchema ? schemaPropertyNames(headersSchema) : []) parameters.push({",
|
|
3692
3783
|
' name, in: "header", required: schemaRequired(headersSchema, name), schema: schemaProperty(headersSchema, name),',
|
|
3693
3784
|
" });",
|
|
3785
|
+
' if (definition.idempotency === "required") {',
|
|
3786
|
+
' const keySchema = { type: "string", minLength: 1, maxLength: 512, pattern: "^[A-Za-z0-9._:-]{1,512}$" };',
|
|
3787
|
+
' const declaredHeaders = parameters.filter((parameter) => parameter["in"] === "header" && typeof parameter["name"] === "string" && parameter["name"].toLowerCase() === "idempotency-key");',
|
|
3788
|
+
" const header = declaredHeaders[0];",
|
|
3789
|
+
" if (header) {",
|
|
3790
|
+
' header["required"] = true;',
|
|
3791
|
+
' header["schema"] = { allOf: [...declaredHeaders.map((parameter) => parameter["schema"]), keySchema] };',
|
|
3792
|
+
" for (const duplicate of declaredHeaders.slice(1)) parameters.splice(parameters.indexOf(duplicate), 1);",
|
|
3793
|
+
" } else {",
|
|
3794
|
+
' parameters.push({ name: "Idempotency-Key", in: "header", required: true, schema: keySchema });',
|
|
3795
|
+
" }",
|
|
3796
|
+
" }",
|
|
3694
3797
|
" const cookieSchema = definition.cookieSchema ? OPENAPI_SCHEMA_REGISTRY[definition.cookieSchema as keyof typeof OPENAPI_SCHEMA_REGISTRY] : undefined;",
|
|
3695
3798
|
" for (const name of cookieSchema ? schemaPropertyNames(cookieSchema) : []) parameters.push({",
|
|
3696
3799
|
' name, in: "cookie", required: schemaRequired(cookieSchema, name), schema: schemaProperty(cookieSchema, name),',
|
|
@@ -3713,6 +3816,7 @@ function renderOpenApi(graph, options) {
|
|
|
3713
3816
|
' responses["500"] = { description: "Internal server error", content: { "application/json": { schema: { $ref: "#/components/schemas/SupaCloudError" } } } };',
|
|
3714
3817
|
' responses["200"] = { description: "Successful response" };',
|
|
3715
3818
|
" }",
|
|
3819
|
+
' 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
3820
|
" const operation: Record<string, unknown> = {",
|
|
3717
3821
|
" operationId: definition.operationId, tags: [...definition.tags], summary: definition.summary, responses,",
|
|
3718
3822
|
" ...(parameters.length === 0 ? {} : { parameters }),",
|
|
@@ -3723,6 +3827,7 @@ function renderOpenApi(graph, options) {
|
|
|
3723
3827
|
" module: definition.module, controller: definition.controller, handler: definition.handler,",
|
|
3724
3828
|
" ...(definition.command ? { command: definition.command } : {}),",
|
|
3725
3829
|
" ...(definition.permission ? { permission: definition.permission } : {}),",
|
|
3830
|
+
" ...(definition.idempotency ? { idempotency: definition.idempotency } : {}),",
|
|
3726
3831
|
" ...(definition.guards ? { guards: [...definition.guards] } : {}),",
|
|
3727
3832
|
" ...(definition.canMatch ? { canMatch: [...definition.canMatch] } : {}),",
|
|
3728
3833
|
" ...(definition.canDeactivate ? { canDeactivate: [...definition.canDeactivate] } : {}),",
|
|
@@ -3773,11 +3878,13 @@ function renderPermissions(graph) {
|
|
|
3773
3878
|
}
|
|
3774
3879
|
for (const controller of module.controllers) {
|
|
3775
3880
|
for (const route of controller.routes) {
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3881
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
3882
|
+
const command = route.command === undefined ? undefined : module.commands.find((candidate) => candidate.className === route.command || candidate.name === route.command);
|
|
3883
|
+
const featureCommand = featureTransition?.command === undefined ? undefined : module.commands.find((candidate) => candidate.className === featureTransition.command || candidate.name === featureTransition.command);
|
|
3884
|
+
const governedCommand = command ?? featureCommand;
|
|
3885
|
+
const effectiveCommand = route.command ?? featureTransition?.command;
|
|
3886
|
+
const perm = governedCommand?.permission ?? featureTransition?.permission;
|
|
3887
|
+
const idempotency = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency;
|
|
3781
3888
|
if (perm)
|
|
3782
3889
|
permissions.add(perm);
|
|
3783
3890
|
bindings.push({
|
|
@@ -3785,8 +3892,9 @@ function renderPermissions(graph) {
|
|
|
3785
3892
|
path: joinRoutePaths(controller.path, route.path),
|
|
3786
3893
|
controller: controller.className,
|
|
3787
3894
|
handler: route.handler,
|
|
3788
|
-
...
|
|
3789
|
-
...perm === undefined ? {} : { permission: perm }
|
|
3895
|
+
...effectiveCommand === undefined ? {} : { command: effectiveCommand },
|
|
3896
|
+
...perm === undefined ? {} : { permission: perm },
|
|
3897
|
+
...idempotency === undefined ? {} : { idempotency }
|
|
3790
3898
|
});
|
|
3791
3899
|
}
|
|
3792
3900
|
}
|
|
@@ -3812,6 +3920,7 @@ function renderPermissions(graph) {
|
|
|
3812
3920
|
" handler: string;",
|
|
3813
3921
|
" command?: string;",
|
|
3814
3922
|
" permission?: string;",
|
|
3923
|
+
' idempotency?: "required" | "none";',
|
|
3815
3924
|
"}",
|
|
3816
3925
|
"",
|
|
3817
3926
|
"export const RoutePermissions: RoutePermissionBinding[] = " + JSON.stringify(bindings, null, 2) + ";",
|
|
@@ -3855,6 +3964,8 @@ var HEADER = "// GENERATED BY @supacloud/compiler — do not edit", INTERFACES =
|
|
|
3855
3964
|
/** True when the handler returns a native Response rather than a framework value. */
|
|
3856
3965
|
nativeResponse?: boolean;
|
|
3857
3966
|
command?: string;
|
|
3967
|
+
/** Transport-level idempotency contract, independent of route command binding. */
|
|
3968
|
+
idempotency?: "required" | "none";
|
|
3858
3969
|
guards?: string[];
|
|
3859
3970
|
canMatch?: string[];
|
|
3860
3971
|
canDeactivate?: string[];
|
|
@@ -4092,6 +4203,7 @@ async function destroyServiceInstances(
|
|
|
4092
4203
|
`;
|
|
4093
4204
|
var init_generate = __esm(() => {
|
|
4094
4205
|
init_contract_manifest();
|
|
4206
|
+
init_feature();
|
|
4095
4207
|
});
|
|
4096
4208
|
|
|
4097
4209
|
// src/route-contracts.ts
|
|
@@ -4945,7 +5057,9 @@ function createExecutionPlans(graph) {
|
|
|
4945
5057
|
const aspects = (boundary, refs = []) => refs.map((ref, index) => `${boundary}.aspect[${index}]:${ref.name}`);
|
|
4946
5058
|
return graph.modules.flatMap((module) => [
|
|
4947
5059
|
...module.controllers.flatMap((controller) => controller.routes.map((route) => {
|
|
4948
|
-
const
|
|
5060
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
5061
|
+
const commandRef = route.command ?? featureTransition?.command;
|
|
5062
|
+
const command = module.commands.find((item) => item.className === commandRef || item.name === commandRef);
|
|
4949
5063
|
const path = `${controller.path}/${route.path}`.replace(/\/+/g, "/");
|
|
4950
5064
|
return {
|
|
4951
5065
|
module: module.name,
|
|
@@ -5095,6 +5209,7 @@ function exportGraphDot(graph) {
|
|
|
5095
5209
|
}
|
|
5096
5210
|
var init_inspect = __esm(() => {
|
|
5097
5211
|
init_route_contracts();
|
|
5212
|
+
init_feature();
|
|
5098
5213
|
});
|
|
5099
5214
|
|
|
5100
5215
|
// node_modules/typebox/build/system/memory/metrics.mjs
|
|
@@ -15662,6 +15777,7 @@ var init_application_development = __esm(() => {
|
|
|
15662
15777
|
controller: text2,
|
|
15663
15778
|
handler: text2,
|
|
15664
15779
|
command: Optional(text2),
|
|
15780
|
+
idempotency: Optional(mode),
|
|
15665
15781
|
aspects: texts,
|
|
15666
15782
|
schemaKinds: Optional(_Object_({
|
|
15667
15783
|
body: schemaKind,
|
|
@@ -15784,7 +15900,11 @@ function createApplicationDevelopmentContext(graph, options = {}) {
|
|
|
15784
15900
|
for (const controller of module.controllers) {
|
|
15785
15901
|
for (const route of controller.routes) {
|
|
15786
15902
|
const candidates = module.commands.filter((command) => command.className === route.command || command.name === route.command);
|
|
15787
|
-
const
|
|
15903
|
+
const commandNode = candidates.length === 1 ? candidates[0] : undefined;
|
|
15904
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
15905
|
+
const featureCommandNode = featureTransition?.command === undefined ? undefined : module.commands.find((candidate) => candidate.className === featureTransition.command || candidate.name === featureTransition.command);
|
|
15906
|
+
const command = commandNode?.name ?? featureCommandNode?.name;
|
|
15907
|
+
const idempotency = route.idempotency ?? commandNode?.idempotency ?? featureTransition?.idempotency;
|
|
15788
15908
|
const kinds = schemaKinds(route);
|
|
15789
15909
|
routes.push({
|
|
15790
15910
|
module: module.name,
|
|
@@ -15793,6 +15913,7 @@ function createApplicationDevelopmentContext(graph, options = {}) {
|
|
|
15793
15913
|
controller: controller.className,
|
|
15794
15914
|
handler: route.handler,
|
|
15795
15915
|
...command === undefined ? {} : { command },
|
|
15916
|
+
...idempotency === undefined ? {} : { idempotency },
|
|
15796
15917
|
aspects: (route.aspects ?? []).map((aspect) => aspect.name),
|
|
15797
15918
|
...kinds === undefined ? {} : { schemaKinds: kinds }
|
|
15798
15919
|
});
|
|
@@ -15913,6 +16034,7 @@ var init_application_development2 = __esm(() => {
|
|
|
15913
16034
|
init_application_development();
|
|
15914
16035
|
init_inspect();
|
|
15915
16036
|
init_execution_snapshot();
|
|
16037
|
+
init_feature();
|
|
15916
16038
|
APPLICATION_DEVELOPMENT_LIMITS2 = APPLICATION_DEVELOPMENT_LIMITS;
|
|
15917
16039
|
ApplicationDevelopmentError = class ApplicationDevelopmentError extends Error {
|
|
15918
16040
|
code;
|
|
@@ -16014,7 +16136,7 @@ function parseDeliveryPlanResult(value) {
|
|
|
16014
16136
|
throw new Error("Invalid delivery plan result.");
|
|
16015
16137
|
return value;
|
|
16016
16138
|
}
|
|
16017
|
-
var objectOptions, reference2, references, kind2, isolation, DeliveryFilePathSchema, functionTarget, DeliveryOptionsSchema, DeliveryDiagnosticSchema, routeSchema, jobSchema, DeliveryTargetSchema, DeliveryPlanSchema, DeliveryPlanResultSchema, DeliveryConfigurationError;
|
|
16139
|
+
var objectOptions, reference2, references, kind2, isolation, mode2, DeliveryFilePathSchema, functionTarget, DeliveryOptionsSchema, DeliveryDiagnosticSchema, routeSchema, jobSchema, DeliveryTargetSchema, DeliveryPlanSchema, DeliveryPlanResultSchema, DeliveryConfigurationError;
|
|
16018
16140
|
var init_delivery_schema = __esm(() => {
|
|
16019
16141
|
init_build();
|
|
16020
16142
|
init_value2();
|
|
@@ -16024,6 +16146,7 @@ var init_delivery_schema = __esm(() => {
|
|
|
16024
16146
|
references = _Array_(reference2, { uniqueItems: true });
|
|
16025
16147
|
kind2 = Union([Literal("api"), Literal("jobs"), Literal("webhook"), Literal("function")]);
|
|
16026
16148
|
isolation = Union([Literal("shared"), Literal("process")]);
|
|
16149
|
+
mode2 = Union([Literal("required"), Literal("none")]);
|
|
16027
16150
|
DeliveryFilePathSchema = String2({
|
|
16028
16151
|
maxLength: 512,
|
|
16029
16152
|
pattern: "^(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*/)*[A-Za-z0-9_-][A-Za-z0-9_.-]*$"
|
|
@@ -16097,6 +16220,7 @@ var init_delivery_schema = __esm(() => {
|
|
|
16097
16220
|
]),
|
|
16098
16221
|
path: String2(),
|
|
16099
16222
|
command: Optional(String2()),
|
|
16223
|
+
idempotency: Optional(mode2),
|
|
16100
16224
|
reason: Union([Literal("default-http"), Literal("explicit-module")])
|
|
16101
16225
|
}, objectOptions);
|
|
16102
16226
|
jobSchema = _Object_({
|
|
@@ -16355,7 +16479,12 @@ function parseDeliveryBuildManifest(value) {
|
|
|
16355
16479
|
throw new Error("Invalid delivery object inventory.");
|
|
16356
16480
|
}
|
|
16357
16481
|
}
|
|
16358
|
-
const routes = value.plan.targets.flatMap((target) => target.routes.map((route) => ({
|
|
16482
|
+
const routes = value.plan.targets.flatMap((target) => target.routes.map((route) => ({
|
|
16483
|
+
method: route.method,
|
|
16484
|
+
path: route.path,
|
|
16485
|
+
target: target.name,
|
|
16486
|
+
...route.idempotency === undefined ? {} : { idempotency: route.idempotency }
|
|
16487
|
+
})));
|
|
16359
16488
|
const jobs = value.plan.targets.flatMap((target) => target.jobs.map((job) => ({ name: job.name, target: target.name })));
|
|
16360
16489
|
if (canonical2(value.routes) !== canonical2(routes) || canonical2(value.jobs) !== canonical2(jobs)) {
|
|
16361
16490
|
throw new Error("Delivery mappings do not match planned ownership.");
|
|
@@ -16406,7 +16535,8 @@ var init_delivery_build_schema = __esm(() => {
|
|
|
16406
16535
|
routes: _Array_(_Object_({
|
|
16407
16536
|
method: String2(),
|
|
16408
16537
|
path: String2(),
|
|
16409
|
-
target: String2()
|
|
16538
|
+
target: String2(),
|
|
16539
|
+
idempotency: Optional(Union([Literal("required"), Literal("none")]))
|
|
16410
16540
|
}, closed4)),
|
|
16411
16541
|
jobs: _Array_(_Object_({ name: String2(), target: String2() }, closed4))
|
|
16412
16542
|
}, closed4);
|
|
@@ -19504,6 +19634,10 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
|
|
|
19504
19634
|
if (!ts7.isObjectLiteralExpression(input))
|
|
19505
19635
|
return invalid();
|
|
19506
19636
|
const name = stringLiteralProp(input, "name");
|
|
19637
|
+
const databaseExpr = getProp(input, "database");
|
|
19638
|
+
const database = databaseExpr && ts7.isObjectLiteralExpression(databaseExpr) ? parseFeatureDatabaseSource(databaseExpr) : undefined;
|
|
19639
|
+
if (databaseExpr !== undefined && database === undefined)
|
|
19640
|
+
return invalid();
|
|
19507
19641
|
const tenantField = stringLiteralProp(input, "tenantField");
|
|
19508
19642
|
const statesExpr = getProp(input, "states");
|
|
19509
19643
|
const transitionObject = getProp(input, "transitions");
|
|
@@ -19536,6 +19670,10 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
|
|
|
19536
19670
|
const idempotency = commandModeProp(options, "idempotency");
|
|
19537
19671
|
const audit = stringLiteralProp(options, "audit");
|
|
19538
19672
|
const job = stringLiteralProp(options, "job");
|
|
19673
|
+
const invalidateTagsExpr = getProp(options, "invalidateTags");
|
|
19674
|
+
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;
|
|
19675
|
+
if (invalidateTagsExpr !== undefined && invalidateTags === undefined)
|
|
19676
|
+
return invalid();
|
|
19539
19677
|
const persistenceExpr = getProp(options, "persistence");
|
|
19540
19678
|
const persistence = persistenceExpr && ts7.isObjectLiteralExpression(persistenceExpr) ? parseFeaturePersistence(persistenceExpr) : undefined;
|
|
19541
19679
|
if (persistenceExpr && !persistence)
|
|
@@ -19552,18 +19690,36 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
|
|
|
19552
19690
|
...idempotency === undefined ? {} : { idempotency },
|
|
19553
19691
|
...audit === undefined ? {} : { audit },
|
|
19554
19692
|
...job === undefined ? {} : { job },
|
|
19693
|
+
...invalidateTags === undefined ? {} : { invalidateTags },
|
|
19555
19694
|
...persistence === undefined ? {} : { persistence }
|
|
19556
19695
|
});
|
|
19557
19696
|
}
|
|
19558
19697
|
return {
|
|
19559
19698
|
name,
|
|
19560
19699
|
states,
|
|
19700
|
+
...database === undefined ? {} : { database },
|
|
19561
19701
|
...tenantField === undefined ? {} : { tenantField },
|
|
19562
19702
|
transitions,
|
|
19563
19703
|
file: sourcePath(ctx.rootDir, input.getSourceFile().fileName),
|
|
19564
19704
|
line: lineOf(input)
|
|
19565
19705
|
};
|
|
19566
19706
|
}
|
|
19707
|
+
function parseFeatureDatabaseSource(input) {
|
|
19708
|
+
if (input.properties.some((property) => !ts7.isPropertyAssignment(property)))
|
|
19709
|
+
return;
|
|
19710
|
+
const source = stringLiteralProp(input, "source");
|
|
19711
|
+
const table = stringLiteralProp(input, "table");
|
|
19712
|
+
const rowType = stringLiteralProp(input, "rowType");
|
|
19713
|
+
if (!source || !table || !rowType)
|
|
19714
|
+
return;
|
|
19715
|
+
if (source.startsWith("/") || source.includes("\\") || source.split("/").includes("..") || !/^[A-Za-z0-9._/-]+$/.test(source))
|
|
19716
|
+
return;
|
|
19717
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*$/.test(table))
|
|
19718
|
+
return;
|
|
19719
|
+
if (!/^[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)*$/.test(rowType))
|
|
19720
|
+
return;
|
|
19721
|
+
return { source, table, rowType };
|
|
19722
|
+
}
|
|
19567
19723
|
function parseFeaturePersistence(input) {
|
|
19568
19724
|
if (input.properties.some((property) => !ts7.isPropertyAssignment(property)))
|
|
19569
19725
|
return;
|
|
@@ -19654,6 +19810,54 @@ function checkedCommandMode(object, property, ctx, command) {
|
|
|
19654
19810
|
}
|
|
19655
19811
|
return value;
|
|
19656
19812
|
}
|
|
19813
|
+
function checkedRouteIdempotency(object, ctx, route) {
|
|
19814
|
+
const expression = getProp(object, "idempotency");
|
|
19815
|
+
const value = stringLiteralProp(object, "idempotency");
|
|
19816
|
+
if (expression && value !== "required" && value !== "none") {
|
|
19817
|
+
const file = sourcePath(ctx.rootDir, expression.getSourceFile().fileName);
|
|
19818
|
+
ctx.diagnostics.push({
|
|
19819
|
+
severity: "error",
|
|
19820
|
+
code: "invalid-route-idempotency",
|
|
19821
|
+
errorCode: "SC3028",
|
|
19822
|
+
docsUrl: "https://supacloud.dev/errors/SC3028",
|
|
19823
|
+
message: `${route}.idempotency must be the explicit literal "required" or "none".`,
|
|
19824
|
+
file,
|
|
19825
|
+
line: lineOf(expression),
|
|
19826
|
+
suggestion: `Choose the intended idempotency policy explicitly; use "required" for replayable mutations or "none" for routes without that contract.`
|
|
19827
|
+
});
|
|
19828
|
+
}
|
|
19829
|
+
return value === "required" || value === "none" ? value : undefined;
|
|
19830
|
+
}
|
|
19831
|
+
function checkedRouteTags(expression, ctx, route) {
|
|
19832
|
+
if (expression === undefined)
|
|
19833
|
+
return;
|
|
19834
|
+
if (!ts7.isArrayLiteralExpression(expression) || expression.elements.some((element) => !ts7.isStringLiteral(element) || element.text.trim().length === 0)) {
|
|
19835
|
+
ctx.diagnostics.push({
|
|
19836
|
+
severity: "error",
|
|
19837
|
+
code: "invalid-route-tags",
|
|
19838
|
+
errorCode: "SC3029",
|
|
19839
|
+
docsUrl: "https://supacloud.dev/errors/SC3029",
|
|
19840
|
+
message: `${route}.tags must be a static array of non-empty string literals.`,
|
|
19841
|
+
file: sourcePath(ctx.rootDir, expression.getSourceFile().fileName),
|
|
19842
|
+
line: lineOf(expression),
|
|
19843
|
+
suggestion: 'Declare stable cache tags explicitly, for example tags: ["cases.list"].'
|
|
19844
|
+
});
|
|
19845
|
+
return;
|
|
19846
|
+
}
|
|
19847
|
+
const tags = expression.elements.map((element) => element.text);
|
|
19848
|
+
if (new Set(tags).size !== tags.length) {
|
|
19849
|
+
ctx.diagnostics.push({
|
|
19850
|
+
severity: "error",
|
|
19851
|
+
code: "duplicate-route-tags",
|
|
19852
|
+
errorCode: "SC3030",
|
|
19853
|
+
docsUrl: "https://supacloud.dev/errors/SC3030",
|
|
19854
|
+
message: `${route}.tags must not contain duplicate values.`,
|
|
19855
|
+
file: sourcePath(ctx.rootDir, expression.getSourceFile().fileName),
|
|
19856
|
+
line: lineOf(expression)
|
|
19857
|
+
});
|
|
19858
|
+
}
|
|
19859
|
+
return tags;
|
|
19860
|
+
}
|
|
19657
19861
|
function parseProvider(el, exportsSet, ctx) {
|
|
19658
19862
|
const file = sourcePath(ctx.rootDir, el.getSourceFile().fileName);
|
|
19659
19863
|
const line = lineOf(el);
|
|
@@ -20433,7 +20637,11 @@ function parseController(input, ctx) {
|
|
|
20433
20637
|
if (optionsObject) {
|
|
20434
20638
|
const parsePolicy = routePolicyProperty(optionsObject, "parse", ctx);
|
|
20435
20639
|
const deletePolicy = routePolicyProperty(optionsObject, "allowDeleteBody", ctx);
|
|
20436
|
-
|
|
20640
|
+
const idempotency = checkedRouteIdempotency(optionsObject, ctx, route.handler);
|
|
20641
|
+
if (idempotency !== undefined)
|
|
20642
|
+
route.idempotency = idempotency;
|
|
20643
|
+
const tagsPolicy = routePolicyProperty(optionsObject, "tags", ctx);
|
|
20644
|
+
if (parsePolicy.unresolved || deletePolicy.unresolved || tagsPolicy.unresolved) {
|
|
20437
20645
|
ctx.diagnostics.push({
|
|
20438
20646
|
severity: "error",
|
|
20439
20647
|
code: "unresolved-route-body-policy",
|
|
@@ -20441,6 +20649,9 @@ function parseController(input, ctx) {
|
|
|
20441
20649
|
message: `Route ${route.handler} body policy must be statically resolvable; dynamic spreads, computed keys and shorthand policies are unsupported.`
|
|
20442
20650
|
});
|
|
20443
20651
|
}
|
|
20652
|
+
const tags = checkedRouteTags(tagsPolicy.value, ctx, route.handler);
|
|
20653
|
+
if (tags !== undefined)
|
|
20654
|
+
route.tags = tags;
|
|
20444
20655
|
const parse = parsePolicy.value;
|
|
20445
20656
|
if (parse) {
|
|
20446
20657
|
if (ts7.isStringLiteral(parse) && parse.text === "none")
|
|
@@ -22289,6 +22500,7 @@ function validateClientSchemaBoundaries(graph, options) {
|
|
|
22289
22500
|
|
|
22290
22501
|
// src/effect-governance.ts
|
|
22291
22502
|
init_validate();
|
|
22503
|
+
init_feature();
|
|
22292
22504
|
function diagnostic(code, message, file, suggestion) {
|
|
22293
22505
|
const metadata = COMPILER_DIAGNOSTIC_CODES[code];
|
|
22294
22506
|
return {
|
|
@@ -22326,8 +22538,10 @@ function validateEffectPolicies(graph, options = {}) {
|
|
|
22326
22538
|
if (effect.retry === "explicit" && effect.maxAttempts === undefined) {
|
|
22327
22539
|
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
22540
|
}
|
|
22329
|
-
if (
|
|
22330
|
-
const
|
|
22541
|
+
if (effect.retry === "explicit") {
|
|
22542
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
22543
|
+
const commandRef = route.command ?? featureTransition?.command;
|
|
22544
|
+
const command = commandRef === undefined ? undefined : commands.get(commandRef) ?? [...commands.values()].find((candidate) => candidate.name === commandRef);
|
|
22331
22545
|
if (command && command.idempotency !== "required") {
|
|
22332
22546
|
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
22547
|
}
|
|
@@ -23544,6 +23758,7 @@ init_graphql_options();
|
|
|
23544
23758
|
|
|
23545
23759
|
// src/delivery-plan.ts
|
|
23546
23760
|
import { createHash as createHash10 } from "node:crypto";
|
|
23761
|
+
init_feature();
|
|
23547
23762
|
init_validate();
|
|
23548
23763
|
var compare2 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
23549
23764
|
var sorted = (values) => [...new Set(values)].sort(compare2);
|
|
@@ -23663,6 +23878,10 @@ function createDeliveryPlan(graph, input, validation) {
|
|
|
23663
23878
|
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
23879
|
}
|
|
23665
23880
|
publicRoutes.set(key, target.name);
|
|
23881
|
+
const command = route.command === undefined ? undefined : module.commands.find((candidate) => candidate.className === route.command || candidate.name === route.command);
|
|
23882
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
23883
|
+
const effectiveCommand = route.command ?? featureTransition?.command;
|
|
23884
|
+
const idempotency = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency;
|
|
23666
23885
|
target.roots.push(module.name);
|
|
23667
23886
|
target.routes.push({
|
|
23668
23887
|
module: module.name,
|
|
@@ -23670,7 +23889,8 @@ function createDeliveryPlan(graph, input, validation) {
|
|
|
23670
23889
|
handler: route.handler,
|
|
23671
23890
|
method: route.method,
|
|
23672
23891
|
path,
|
|
23673
|
-
...
|
|
23892
|
+
...effectiveCommand === undefined ? {} : { command: effectiveCommand },
|
|
23893
|
+
...idempotency === undefined ? {} : { idempotency },
|
|
23674
23894
|
reason: owner === undefined ? "default-http" : "explicit-module"
|
|
23675
23895
|
});
|
|
23676
23896
|
}
|
|
@@ -24702,7 +24922,12 @@ async function buildDeliveryProject(options, delivery) {
|
|
|
24702
24922
|
deploymentReady: false,
|
|
24703
24923
|
plan: planned.plan,
|
|
24704
24924
|
objects,
|
|
24705
|
-
routes: planned.plan.targets.flatMap((target) => target.routes.map((route) => ({
|
|
24925
|
+
routes: planned.plan.targets.flatMap((target) => target.routes.map((route) => ({
|
|
24926
|
+
method: route.method,
|
|
24927
|
+
path: route.path,
|
|
24928
|
+
target: target.name,
|
|
24929
|
+
...route.idempotency === undefined ? {} : { idempotency: route.idempotency }
|
|
24930
|
+
}))),
|
|
24706
24931
|
jobs: planned.plan.targets.flatMap((target) => target.jobs.map((job) => ({ name: job.name, target: target.name })))
|
|
24707
24932
|
});
|
|
24708
24933
|
phase = "artifact-integrity";
|
|
@@ -24768,10 +24993,10 @@ import { readFileSync as readFileSync4 } from "node:fs";
|
|
|
24768
24993
|
import { resolve as resolve18 } from "node:path";
|
|
24769
24994
|
|
|
24770
24995
|
// src/migration-app-version.ts
|
|
24771
|
-
var MIGRATION_APP_VERSION = "0.
|
|
24996
|
+
var MIGRATION_APP_VERSION = "0.31.0";
|
|
24772
24997
|
|
|
24773
24998
|
// src/migration-elysia-version.ts
|
|
24774
|
-
var MIGRATION_ELYSIA_VERSION = "0.
|
|
24999
|
+
var MIGRATION_ELYSIA_VERSION = "0.35.1";
|
|
24775
25000
|
|
|
24776
25001
|
// src/migration-policy.ts
|
|
24777
25002
|
var cachedCompilerVersion;
|
|
@@ -25807,8 +26032,8 @@ function validFeatureInventory(value, jobs, commands, routes, rpc) {
|
|
|
25807
26032
|
return false;
|
|
25808
26033
|
}
|
|
25809
26034
|
const commandName = transition["command"];
|
|
26035
|
+
const command = commandName === undefined ? undefined : commands.find((candidate) => isRecord2(candidate) && (candidate["name"] === commandName || candidate["className"] === commandName));
|
|
25810
26036
|
if (commandName !== undefined) {
|
|
25811
|
-
const command = commands.find((candidate) => isRecord2(candidate) && (candidate["name"] === commandName || candidate["className"] === commandName));
|
|
25812
26037
|
if (!isRecord2(command))
|
|
25813
26038
|
return false;
|
|
25814
26039
|
if (transition["permission"] !== undefined && command["permission"] !== transition["permission"])
|
|
@@ -25839,9 +26064,21 @@ function validFeatureInventory(value, jobs, commands, routes, rpc) {
|
|
|
25839
26064
|
if (routeBinding === "controller") {
|
|
25840
26065
|
if (route["command"] !== undefined)
|
|
25841
26066
|
return false;
|
|
25842
|
-
} else
|
|
25843
|
-
|
|
26067
|
+
} else {
|
|
26068
|
+
if (typeof route["command"] !== "string")
|
|
26069
|
+
return false;
|
|
26070
|
+
const routeCommand = commands.find((candidate) => isRecord2(candidate) && (candidate["name"] === route["command"] || candidate["className"] === route["command"]));
|
|
26071
|
+
if (!isRecord2(routeCommand) || !isRecord2(command) || routeCommand["name"] !== command["name"])
|
|
26072
|
+
return false;
|
|
25844
26073
|
}
|
|
26074
|
+
if (transition["permission"] !== undefined && route["permission"] !== transition["permission"])
|
|
26075
|
+
return false;
|
|
26076
|
+
if (transition["idempotency"] !== undefined && route["idempotency"] !== transition["idempotency"])
|
|
26077
|
+
return false;
|
|
26078
|
+
if (persistence !== undefined && route["idempotency"] !== "required")
|
|
26079
|
+
return false;
|
|
26080
|
+
if (route["idempotency"] === "required" && (route["method"] === "GET" || route["method"] === "HEAD" || route["method"] === "OPTIONS"))
|
|
26081
|
+
return false;
|
|
25845
26082
|
}
|
|
25846
26083
|
const job = transition["job"];
|
|
25847
26084
|
if (job === undefined)
|