@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/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) => {
|
|
@@ -265,6 +269,21 @@ function validateFeatureSpec(spec, module) {
|
|
|
265
269
|
if (spec.tenantField !== undefined && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(spec.tenantField)) {
|
|
266
270
|
error("feature-tenant-boundary-invalid", `Feature ${spec.name} declares an unsafe tenant field.`);
|
|
267
271
|
}
|
|
272
|
+
const databaseTransition = spec.transitions.some((transition) => transition.persistence?.boundary === "database");
|
|
273
|
+
if (databaseTransition && spec.database === undefined && spec.file !== undefined) {
|
|
274
|
+
error("feature-database-source-required", `Database Feature ${spec.name} must declare its database source, table and rowType.`);
|
|
275
|
+
}
|
|
276
|
+
if (spec.database !== undefined) {
|
|
277
|
+
if (spec.database.source.startsWith("/") || spec.database.source.includes("\\") || spec.database.source.split("/").includes("..") || !/^[A-Za-z0-9._/-]+$/.test(spec.database.source)) {
|
|
278
|
+
error("feature-database-source-invalid", `Feature ${spec.name} declares an unsafe database source.`);
|
|
279
|
+
}
|
|
280
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*$/.test(spec.database.table)) {
|
|
281
|
+
error("feature-database-table-invalid", `Feature ${spec.name} must use a schema-qualified database table.`);
|
|
282
|
+
}
|
|
283
|
+
if (!/^[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)*$/.test(spec.database.rowType)) {
|
|
284
|
+
error("feature-database-row-type-invalid", `Feature ${spec.name} declares an unsafe database row type.`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
268
287
|
const names = new Set;
|
|
269
288
|
for (const transition of spec.transitions) {
|
|
270
289
|
if (!transition.name.trim() || names.has(transition.name)) {
|
|
@@ -277,6 +296,11 @@ function validateFeatureSpec(spec, module) {
|
|
|
277
296
|
if (transition.permission !== undefined && !transition.permission.trim()) {
|
|
278
297
|
error("feature-governance-drift", `Transition ${transition.name} declares an empty permission.`);
|
|
279
298
|
}
|
|
299
|
+
if (transition.invalidateTags !== undefined) {
|
|
300
|
+
if (transition.invalidateTags.length === 0 || transition.invalidateTags.some((tag) => !tag.trim()) || new Set(transition.invalidateTags).size !== transition.invalidateTags.length) {
|
|
301
|
+
error("feature-invalidation-tags-invalid", `Transition ${transition.name} must declare unique, non-empty invalidateTags.`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
280
304
|
if (transition.persistence) {
|
|
281
305
|
const persistence = transition.persistence;
|
|
282
306
|
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 +393,7 @@ function validateFeatureSpec(spec, module) {
|
|
|
369
393
|
const routes = module.controllers.flatMap((controller) => controller.routes.filter((route) => `${route.method} ${joinRoutePaths(controller.path, route.path)}` === transition.route));
|
|
370
394
|
if (routes.length !== 1) {
|
|
371
395
|
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) {
|
|
396
|
+
} else if (command && transition.routeBinding !== "controller" && routes[0]?.command !== command.className && routes[0]?.command !== command.name) {
|
|
373
397
|
error("feature-route-drift", `Route ${transition.route} is not bound to ${command.className}.`);
|
|
374
398
|
} else if (transition.routeBinding === "controller" && routes[0]?.command !== undefined) {
|
|
375
399
|
error("feature-route-drift", `Controller-bound route ${transition.route} must not also declare a framework command binding.`);
|
|
@@ -396,6 +420,7 @@ function generateFeatureSource(spec) {
|
|
|
396
420
|
const sourceSpec = {
|
|
397
421
|
name: spec.name,
|
|
398
422
|
states: spec.states,
|
|
423
|
+
...spec.database === undefined ? {} : { database: spec.database },
|
|
399
424
|
tenantField: spec.tenantField ?? "tenant_id",
|
|
400
425
|
transitions: Object.fromEntries(transitions.map((transition) => {
|
|
401
426
|
const { name, ...options } = transition;
|
|
@@ -614,13 +639,17 @@ function validateGraph(graph, options = false) {
|
|
|
614
639
|
handler: route.handler,
|
|
615
640
|
...route.redirectTo === undefined ? {} : { redirectTo: route.redirectTo }
|
|
616
641
|
});
|
|
617
|
-
|
|
618
|
-
|
|
642
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
643
|
+
const effectiveCommandRef = route.command ?? featureTransition?.command;
|
|
644
|
+
const effectiveCommand = effectiveCommandRef === undefined ? undefined : module.commands.find((command) => command.className === effectiveCommandRef || command.name === effectiveCommandRef);
|
|
645
|
+
const effectiveIdempotency = route.idempotency ?? featureTransition?.idempotency;
|
|
646
|
+
if (route.command && !effectiveCommand) {
|
|
647
|
+
error("route-command-unresolved", `路由 ${key} 绑定的 command ${route.command} 未在模块 ${module.name} 声明`, controller.file);
|
|
619
648
|
}
|
|
620
|
-
if ((route.method === "GET" || route.method === "HEAD") &&
|
|
621
|
-
const boundCommand =
|
|
649
|
+
if ((route.method === "GET" || route.method === "HEAD") && effectiveCommand) {
|
|
650
|
+
const boundCommand = effectiveCommand;
|
|
622
651
|
if (boundCommand && boundCommand.transaction === "required") {
|
|
623
|
-
error("command-transaction-readonly", `GET route '${route.path}' in '${controller.className}.${route.handler}' binds mutating command '${
|
|
652
|
+
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
653
|
}
|
|
625
654
|
}
|
|
626
655
|
if (typeof options === "object" && options.allowRouteCommandBindings === false && route.command) {
|
|
@@ -691,6 +720,9 @@ function validateGraph(graph, options = false) {
|
|
|
691
720
|
if (route.allowDeleteBody && (route.method !== "DELETE" || !route.body)) {
|
|
692
721
|
error("invalid-delete-body-opt-in", `Route ${controller.className}.${route.handler} allowDeleteBody requires DELETE and an explicit body schema.`, controller.file);
|
|
693
722
|
}
|
|
723
|
+
if (effectiveIdempotency === "required" && (route.method === "GET" || route.method === "HEAD" || route.method === "OPTIONS")) {
|
|
724
|
+
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.`);
|
|
725
|
+
}
|
|
694
726
|
if (route.parse === "none" && (route.body || route.hasBodyBinding || route.contract?.body !== "domain" || !route.contract.evidence?.trim())) {
|
|
695
727
|
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
728
|
}
|
|
@@ -1365,6 +1397,7 @@ var init_validate = __esm(() => {
|
|
|
1365
1397
|
"command-transaction-unsupported": { code: "SC4026", docsUrl: "https://supacloud.dev/errors/SC4026" },
|
|
1366
1398
|
"command-reconcile-required": { code: "SC4028", docsUrl: "https://supacloud.dev/errors/SC4028" },
|
|
1367
1399
|
"invalid-route-contract": { code: "SC3020", docsUrl: "https://supacloud.dev/errors/SC3020" },
|
|
1400
|
+
"invalid-route-idempotency": { code: "SC3028", docsUrl: "https://supacloud.dev/errors/SC3028" },
|
|
1368
1401
|
"route-contract-required": { code: "SC3021", docsUrl: "https://supacloud.dev/errors/SC3021" },
|
|
1369
1402
|
"route-contract-unverified": { code: "SC3022", docsUrl: "https://supacloud.dev/errors/SC3022" },
|
|
1370
1403
|
"route-contract-evidence-required": { code: "SC3023", docsUrl: "https://supacloud.dev/errors/SC3023" },
|
|
@@ -1416,6 +1449,10 @@ var init_validate = __esm(() => {
|
|
|
1416
1449
|
"feature-job-mode": { code: "SC6119", docsUrl: "https://supacloud.dev/errors/SC6119" },
|
|
1417
1450
|
"feature-job-contract": { code: "SC6120", docsUrl: "https://supacloud.dev/errors/SC6120" },
|
|
1418
1451
|
"feature-job-context": { code: "SC6121", docsUrl: "https://supacloud.dev/errors/SC6121" },
|
|
1452
|
+
"feature-database-source-required": { code: "SC6122", docsUrl: "https://supacloud.dev/errors/SC6122" },
|
|
1453
|
+
"feature-database-source-invalid": { code: "SC6123", docsUrl: "https://supacloud.dev/errors/SC6123" },
|
|
1454
|
+
"feature-database-table-invalid": { code: "SC6124", docsUrl: "https://supacloud.dev/errors/SC6124" },
|
|
1455
|
+
"feature-database-row-type-invalid": { code: "SC6125", docsUrl: "https://supacloud.dev/errors/SC6125" },
|
|
1419
1456
|
"feature-route-unresolved": { code: "SC6106", docsUrl: "https://supacloud.dev/errors/SC6106" },
|
|
1420
1457
|
"feature-route-drift": { code: "SC6107", docsUrl: "https://supacloud.dev/errors/SC6107" },
|
|
1421
1458
|
"invalid-feature-spec": { code: "SC6108", docsUrl: "https://supacloud.dev/errors/SC6108" }
|
|
@@ -1496,6 +1533,7 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1496
1533
|
features.push({
|
|
1497
1534
|
name: module.featureSpec.name,
|
|
1498
1535
|
states: [...module.featureSpec.states],
|
|
1536
|
+
...module.featureSpec.database === undefined ? {} : { database: module.featureSpec.database },
|
|
1499
1537
|
...module.featureSpec.tenantField === undefined ? {} : { tenantField: module.featureSpec.tenantField },
|
|
1500
1538
|
transitions: module.featureSpec.transitions.map((transition) => {
|
|
1501
1539
|
const job = transition.job === undefined ? undefined : (module.jobs ?? []).find((candidate) => candidate.className === transition.job || candidate.name === transition.job);
|
|
@@ -1511,6 +1549,7 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1511
1549
|
...transition.idempotency === undefined ? {} : { idempotency: transition.idempotency },
|
|
1512
1550
|
...transition.audit === undefined ? {} : { audit: transition.audit },
|
|
1513
1551
|
...transition.job === undefined ? {} : { job: transition.job },
|
|
1552
|
+
...transition.invalidateTags === undefined ? {} : { invalidateTags: [...transition.invalidateTags] },
|
|
1514
1553
|
...job === undefined ? {} : {
|
|
1515
1554
|
jobDetails: {
|
|
1516
1555
|
name: job.name,
|
|
@@ -1531,9 +1570,14 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1531
1570
|
}
|
|
1532
1571
|
for (const controller of module.controllers) {
|
|
1533
1572
|
for (const route of controller.routes) {
|
|
1534
|
-
const command = route.command ? module.commands.find((candidate) => candidate.className === route.command) : undefined;
|
|
1535
|
-
|
|
1536
|
-
|
|
1573
|
+
const command = route.command ? module.commands.find((candidate) => candidate.className === route.command || candidate.name === route.command) : undefined;
|
|
1574
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
1575
|
+
const featureCommand = featureTransition?.command === undefined ? undefined : module.commands.find((candidate) => candidate.className === featureTransition.command || candidate.name === featureTransition.command);
|
|
1576
|
+
const governedCommand = command ?? featureCommand;
|
|
1577
|
+
const effectiveCommand = route.command ?? featureTransition?.command;
|
|
1578
|
+
const idempotency = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency;
|
|
1579
|
+
if (governedCommand?.permission)
|
|
1580
|
+
permissions.add(governedCommand.permission);
|
|
1537
1581
|
if (route.contract?.evidence)
|
|
1538
1582
|
fixtures.add(route.contract.evidence);
|
|
1539
1583
|
routes.push({
|
|
@@ -1542,8 +1586,10 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1542
1586
|
module: module.name,
|
|
1543
1587
|
controller: controller.className,
|
|
1544
1588
|
handler: route.handler,
|
|
1545
|
-
...
|
|
1546
|
-
...
|
|
1589
|
+
...effectiveCommand === undefined ? {} : { command: effectiveCommand },
|
|
1590
|
+
...governedCommand?.permission === undefined ? {} : { permission: governedCommand.permission },
|
|
1591
|
+
...idempotency === undefined ? {} : { idempotency },
|
|
1592
|
+
...route.tags === undefined ? {} : { tags: [...route.tags] },
|
|
1547
1593
|
requestSchemas: Object.fromEntries(["body", "params", "query", "headers", "cookie"].flatMap((key) => route[key] === undefined ? [] : [[key, route[key]]])),
|
|
1548
1594
|
...route.response === undefined ? {} : { responseSchema: route.response },
|
|
1549
1595
|
...route.responses === undefined ? {} : {
|
|
@@ -1582,7 +1628,9 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1582
1628
|
}
|
|
1583
1629
|
};
|
|
1584
1630
|
}
|
|
1585
|
-
var init_contract_manifest = () => {
|
|
1631
|
+
var init_contract_manifest = __esm(() => {
|
|
1632
|
+
init_feature();
|
|
1633
|
+
});
|
|
1586
1634
|
|
|
1587
1635
|
// src/generate.ts
|
|
1588
1636
|
import { createHash as createHash4 } from "node:crypto";
|
|
@@ -1994,6 +2042,9 @@ class ModuleGenerator {
|
|
|
1994
2042
|
return "[]";
|
|
1995
2043
|
const items = this.module.controllers.map((controller) => {
|
|
1996
2044
|
const routes = controller.routes.map((route) => {
|
|
2045
|
+
const command = route.command === undefined ? undefined : this.module.commands.find((candidate) => candidate.className === route.command || candidate.name === route.command);
|
|
2046
|
+
const featureTransition = featureTransitionForRoute(this.module, controller, route);
|
|
2047
|
+
const idempotency = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency;
|
|
1997
2048
|
const responseEntries = normalizedResponseEntries(route.responses);
|
|
1998
2049
|
const hasResponseMap = responseEntries.length > 0;
|
|
1999
2050
|
const fields = [
|
|
@@ -2020,6 +2071,9 @@ class ModuleGenerator {
|
|
|
2020
2071
|
if (route.effect !== undefined) {
|
|
2021
2072
|
fields.push(`effect: ${JSON.stringify(route.effect)}`);
|
|
2022
2073
|
}
|
|
2074
|
+
if (idempotency !== undefined) {
|
|
2075
|
+
fields.push(`idempotency: ${JSON.stringify(idempotency)}`);
|
|
2076
|
+
}
|
|
2023
2077
|
if (route.command)
|
|
2024
2078
|
fields.push(`command: ${JSON.stringify(route.command)}`);
|
|
2025
2079
|
if (route.guards && route.guards.length > 0) {
|
|
@@ -2502,7 +2556,18 @@ function renderClient(graph, options) {
|
|
|
2502
2556
|
const usedTypeNames = new Set;
|
|
2503
2557
|
const allRoutes = [];
|
|
2504
2558
|
const allProcedures = [];
|
|
2505
|
-
const commandByClassName = new Map(graph.modules.flatMap((module) => module.commands.
|
|
2559
|
+
const commandByClassName = new Map(graph.modules.flatMap((module) => module.commands.flatMap((command) => [
|
|
2560
|
+
[command.className, command],
|
|
2561
|
+
[command.name, command]
|
|
2562
|
+
])));
|
|
2563
|
+
const featureInvalidationTags = new Map;
|
|
2564
|
+
for (const module of graph.modules) {
|
|
2565
|
+
for (const transition of module.featureSpec?.transitions ?? []) {
|
|
2566
|
+
if (transition.route && transition.invalidateTags) {
|
|
2567
|
+
featureInvalidationTags.set(`${module.name}:${transition.route}`, [...transition.invalidateTags]);
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2506
2571
|
let usesStatic = false;
|
|
2507
2572
|
let usesValue = false;
|
|
2508
2573
|
const uniqueTypeName = (candidate) => {
|
|
@@ -2580,6 +2645,13 @@ function renderClient(graph, options) {
|
|
|
2580
2645
|
const requestType = `ClientRequestOptions<${paramsType}, ${queryType}, ${bodyType}, ${headersType}, ${cookieType}>${requiredRequestFields.length > 0 ? ` & { ${requiredRequestFields.join("; ")} }` : ""}`;
|
|
2581
2646
|
routeTypes.push(`export type ${requestTypeName} = ${requestType};`);
|
|
2582
2647
|
routeTypes.push(`export type ${responseTypeName} = ${responseType === "never" ? "unknown" : responseType};`);
|
|
2648
|
+
const command = route.command === undefined ? undefined : commandByClassName.get(route.command);
|
|
2649
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
2650
|
+
const featureCommand = featureTransition?.command === undefined ? undefined : commandByClassName.get(featureTransition.command);
|
|
2651
|
+
const governedCommand = command ?? featureCommand;
|
|
2652
|
+
const idempotencyMode = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency ?? "none";
|
|
2653
|
+
const transitionRoute = `${route.method} ${fullPath}`;
|
|
2654
|
+
const invalidationTags = featureInvalidationTags.get(`${module.name}:${transitionRoute}`);
|
|
2583
2655
|
const routeFields = [
|
|
2584
2656
|
`method: ${JSON.stringify(route.method)}`,
|
|
2585
2657
|
`path: ${JSON.stringify(fullPath)}`,
|
|
@@ -2587,7 +2659,7 @@ function renderClient(graph, options) {
|
|
|
2587
2659
|
`handler: ${JSON.stringify(route.handler)}`
|
|
2588
2660
|
];
|
|
2589
2661
|
for (const [key, value] of [
|
|
2590
|
-
["command", route.command],
|
|
2662
|
+
["command", route.command ?? featureTransition?.command],
|
|
2591
2663
|
["guards", route.guards],
|
|
2592
2664
|
["canMatch", route.canMatch],
|
|
2593
2665
|
["canDeactivate", route.canDeactivate],
|
|
@@ -2601,7 +2673,10 @@ function renderClient(graph, options) {
|
|
|
2601
2673
|
["title", route.title],
|
|
2602
2674
|
["data", route.data],
|
|
2603
2675
|
["parse", route.parse],
|
|
2604
|
-
["allowDeleteBody", route.allowDeleteBody]
|
|
2676
|
+
["allowDeleteBody", route.allowDeleteBody],
|
|
2677
|
+
["idempotency", idempotencyMode === "none" ? undefined : idempotencyMode],
|
|
2678
|
+
["tags", route.tags],
|
|
2679
|
+
["invalidateTags", invalidationTags]
|
|
2605
2680
|
]) {
|
|
2606
2681
|
if (value !== undefined)
|
|
2607
2682
|
routeFields.push(`${key}: ${JSON.stringify(value)}`);
|
|
@@ -2634,9 +2709,7 @@ function renderClient(graph, options) {
|
|
|
2634
2709
|
const responseSchemaEntries = hasResponseMap ? Object.entries(responseSchemas).map(([status, ref]) => [status, ref.local]) : responseSchema ? [["200", responseSchema.local]] : [];
|
|
2635
2710
|
const responseSchemaArgument = responseSchemaEntries.length > 0 ? `{ ${responseSchemaEntries.map(([status, local]) => `${JSON.stringify(status)}: ${local}`).join(", ")} }` : "undefined";
|
|
2636
2711
|
const responseKindArgument = responseKind === undefined ? "undefined" : JSON.stringify(responseKind);
|
|
2637
|
-
const command = route.command === undefined ? undefined : commandByClassName.get(route.command);
|
|
2638
2712
|
const procedureKind = route.method === "GET" || route.method === "HEAD" ? "query" : "mutation";
|
|
2639
|
-
const idempotencyMode = command?.idempotency ?? "none";
|
|
2640
2713
|
const procedureMetadata = {
|
|
2641
2714
|
key: `${controller.className}.${route.handler}`,
|
|
2642
2715
|
method: route.method,
|
|
@@ -2644,7 +2717,9 @@ function renderClient(graph, options) {
|
|
|
2644
2717
|
controller: controller.className,
|
|
2645
2718
|
handler: route.handler,
|
|
2646
2719
|
kind: procedureKind,
|
|
2647
|
-
idempotency: idempotencyMode
|
|
2720
|
+
idempotency: idempotencyMode,
|
|
2721
|
+
...route.tags === undefined ? {} : { tags: route.tags },
|
|
2722
|
+
...invalidationTags === undefined ? {} : { invalidateTags: invalidationTags }
|
|
2648
2723
|
};
|
|
2649
2724
|
allProcedures.push({
|
|
2650
2725
|
method: route.method,
|
|
@@ -2652,9 +2727,11 @@ function renderClient(graph, options) {
|
|
|
2652
2727
|
controller: controller.className,
|
|
2653
2728
|
handler: route.handler,
|
|
2654
2729
|
kind: procedureKind,
|
|
2655
|
-
...
|
|
2656
|
-
...
|
|
2657
|
-
idempotency: idempotencyMode
|
|
2730
|
+
...governedCommand?.name === undefined ? {} : { command: governedCommand.name },
|
|
2731
|
+
...governedCommand?.permission === undefined ? {} : { permission: governedCommand.permission },
|
|
2732
|
+
idempotency: idempotencyMode,
|
|
2733
|
+
...route.tags === undefined ? {} : { tags: [...route.tags] },
|
|
2734
|
+
...invalidationTags === undefined ? {} : { invalidateTags: [...invalidationTags] }
|
|
2658
2735
|
});
|
|
2659
2736
|
routeMethods.push(`
|
|
2660
2737
|
${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 +2785,8 @@ ${procedureMethods.join(`
|
|
|
2708
2785
|
" handler: string;",
|
|
2709
2786
|
" kind: Kind;",
|
|
2710
2787
|
" idempotency: Idempotency;",
|
|
2788
|
+
" tags?: readonly string[];",
|
|
2789
|
+
" invalidateTags?: readonly string[];",
|
|
2711
2790
|
"}",
|
|
2712
2791
|
"",
|
|
2713
2792
|
"export interface ProcedureExecutionOptions {",
|
|
@@ -2813,6 +2892,8 @@ ${procedureMethods.join(`
|
|
|
2813
2892
|
" command?: string;",
|
|
2814
2893
|
" permission?: string;",
|
|
2815
2894
|
" idempotency: IdempotencyMode;",
|
|
2895
|
+
" tags?: readonly string[];",
|
|
2896
|
+
" invalidateTags?: readonly string[];",
|
|
2816
2897
|
"}",
|
|
2817
2898
|
"",
|
|
2818
2899
|
...routeTypes,
|
|
@@ -3312,7 +3393,10 @@ function renderOpenApi(graph, options) {
|
|
|
3312
3393
|
}
|
|
3313
3394
|
return schemaComponents.get(key);
|
|
3314
3395
|
};
|
|
3315
|
-
const commandByClass = new Map(graph.modules.flatMap((module) => module.commands.
|
|
3396
|
+
const commandByClass = new Map(graph.modules.flatMap((module) => module.commands.flatMap((command) => [
|
|
3397
|
+
[command.className, command],
|
|
3398
|
+
[command.name, command]
|
|
3399
|
+
])));
|
|
3316
3400
|
for (const module of graph.modules) {
|
|
3317
3401
|
for (const controller of module.controllers) {
|
|
3318
3402
|
for (const route of controller.routes) {
|
|
@@ -3334,7 +3418,12 @@ function renderOpenApi(graph, options) {
|
|
|
3334
3418
|
const operationBase = pascalName(`${module.name}_${controller.className}_${route.handler}`);
|
|
3335
3419
|
const operationId = uniqueName(operationBase, operationIds);
|
|
3336
3420
|
const command = route.command ? commandByClass.get(route.command) : undefined;
|
|
3337
|
-
const
|
|
3421
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
3422
|
+
const featureCommand = featureTransition?.command === undefined ? undefined : commandByClass.get(featureTransition.command);
|
|
3423
|
+
const governedCommand = command ?? featureCommand;
|
|
3424
|
+
const idempotencyMode = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency ?? "none";
|
|
3425
|
+
const permission = governedCommand?.permission ?? featureTransition?.permission;
|
|
3426
|
+
const requiresAuth = (route.guards?.length ?? 0) > 0 || permission !== undefined || governedCommand !== undefined;
|
|
3338
3427
|
const responseKind = route.contract?.response;
|
|
3339
3428
|
const responseContentType = responseKind === "binary" || responseKind === "stream" ? "application/octet-stream" : "application/json";
|
|
3340
3429
|
const definition = {
|
|
@@ -3359,11 +3448,12 @@ function renderOpenApi(graph, options) {
|
|
|
3359
3448
|
responseKind: responseKind ?? (route.nativeResponse ? "native-response" : undefined),
|
|
3360
3449
|
...route.parse === undefined ? {} : { parse: route.parse },
|
|
3361
3450
|
...route.allowDeleteBody === undefined ? {} : { allowDeleteBody: route.allowDeleteBody },
|
|
3451
|
+
...idempotencyMode === "none" ? {} : { idempotency: idempotencyMode },
|
|
3362
3452
|
module: module.name,
|
|
3363
3453
|
controller: controller.className,
|
|
3364
3454
|
handler: route.handler,
|
|
3365
|
-
...route.command === undefined ? {} : { command: route.command },
|
|
3366
|
-
...
|
|
3455
|
+
...route.command === undefined && featureTransition?.command === undefined ? {} : { command: route.command ?? featureTransition?.command },
|
|
3456
|
+
...permission === undefined ? {} : { permission },
|
|
3367
3457
|
...route.guards && route.guards.length > 0 ? { guards: route.guards } : {},
|
|
3368
3458
|
...route.canMatch && route.canMatch.length > 0 ? { canMatch: route.canMatch } : {},
|
|
3369
3459
|
...route.canDeactivate && route.canDeactivate.length > 0 ? { canDeactivate: route.canDeactivate } : {},
|
|
@@ -3438,6 +3528,7 @@ function renderOpenApi(graph, options) {
|
|
|
3438
3528
|
" handler: string;",
|
|
3439
3529
|
" command?: string;",
|
|
3440
3530
|
" permission?: string;",
|
|
3531
|
+
' idempotency?: "required" | "none";',
|
|
3441
3532
|
" guards?: readonly string[];",
|
|
3442
3533
|
" canMatch?: readonly string[];",
|
|
3443
3534
|
" canDeactivate?: readonly string[];",
|
|
@@ -3690,6 +3781,18 @@ function renderOpenApi(graph, options) {
|
|
|
3690
3781
|
" for (const name of headersSchema ? schemaPropertyNames(headersSchema) : []) parameters.push({",
|
|
3691
3782
|
' name, in: "header", required: schemaRequired(headersSchema, name), schema: schemaProperty(headersSchema, name),',
|
|
3692
3783
|
" });",
|
|
3784
|
+
' if (definition.idempotency === "required") {',
|
|
3785
|
+
' const keySchema = { type: "string", minLength: 1, maxLength: 512, pattern: "^[A-Za-z0-9._:-]{1,512}$" };',
|
|
3786
|
+
' const declaredHeaders = parameters.filter((parameter) => parameter["in"] === "header" && typeof parameter["name"] === "string" && parameter["name"].toLowerCase() === "idempotency-key");',
|
|
3787
|
+
" const header = declaredHeaders[0];",
|
|
3788
|
+
" if (header) {",
|
|
3789
|
+
' header["required"] = true;',
|
|
3790
|
+
' header["schema"] = { allOf: [...declaredHeaders.map((parameter) => parameter["schema"]), keySchema] };',
|
|
3791
|
+
" for (const duplicate of declaredHeaders.slice(1)) parameters.splice(parameters.indexOf(duplicate), 1);",
|
|
3792
|
+
" } else {",
|
|
3793
|
+
' parameters.push({ name: "Idempotency-Key", in: "header", required: true, schema: keySchema });',
|
|
3794
|
+
" }",
|
|
3795
|
+
" }",
|
|
3693
3796
|
" const cookieSchema = definition.cookieSchema ? OPENAPI_SCHEMA_REGISTRY[definition.cookieSchema as keyof typeof OPENAPI_SCHEMA_REGISTRY] : undefined;",
|
|
3694
3797
|
" for (const name of cookieSchema ? schemaPropertyNames(cookieSchema) : []) parameters.push({",
|
|
3695
3798
|
' name, in: "cookie", required: schemaRequired(cookieSchema, name), schema: schemaProperty(cookieSchema, name),',
|
|
@@ -3712,6 +3815,7 @@ function renderOpenApi(graph, options) {
|
|
|
3712
3815
|
' responses["500"] = { description: "Internal server error", content: { "application/json": { schema: { $ref: "#/components/schemas/SupaCloudError" } } } };',
|
|
3713
3816
|
' responses["200"] = { description: "Successful response" };',
|
|
3714
3817
|
" }",
|
|
3818
|
+
' 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
3819
|
" const operation: Record<string, unknown> = {",
|
|
3716
3820
|
" operationId: definition.operationId, tags: [...definition.tags], summary: definition.summary, responses,",
|
|
3717
3821
|
" ...(parameters.length === 0 ? {} : { parameters }),",
|
|
@@ -3722,6 +3826,7 @@ function renderOpenApi(graph, options) {
|
|
|
3722
3826
|
" module: definition.module, controller: definition.controller, handler: definition.handler,",
|
|
3723
3827
|
" ...(definition.command ? { command: definition.command } : {}),",
|
|
3724
3828
|
" ...(definition.permission ? { permission: definition.permission } : {}),",
|
|
3829
|
+
" ...(definition.idempotency ? { idempotency: definition.idempotency } : {}),",
|
|
3725
3830
|
" ...(definition.guards ? { guards: [...definition.guards] } : {}),",
|
|
3726
3831
|
" ...(definition.canMatch ? { canMatch: [...definition.canMatch] } : {}),",
|
|
3727
3832
|
" ...(definition.canDeactivate ? { canDeactivate: [...definition.canDeactivate] } : {}),",
|
|
@@ -3772,11 +3877,13 @@ function renderPermissions(graph) {
|
|
|
3772
3877
|
}
|
|
3773
3878
|
for (const controller of module.controllers) {
|
|
3774
3879
|
for (const route of controller.routes) {
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3880
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
3881
|
+
const command = route.command === undefined ? undefined : module.commands.find((candidate) => candidate.className === route.command || candidate.name === route.command);
|
|
3882
|
+
const featureCommand = featureTransition?.command === undefined ? undefined : module.commands.find((candidate) => candidate.className === featureTransition.command || candidate.name === featureTransition.command);
|
|
3883
|
+
const governedCommand = command ?? featureCommand;
|
|
3884
|
+
const effectiveCommand = route.command ?? featureTransition?.command;
|
|
3885
|
+
const perm = governedCommand?.permission ?? featureTransition?.permission;
|
|
3886
|
+
const idempotency = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency;
|
|
3780
3887
|
if (perm)
|
|
3781
3888
|
permissions.add(perm);
|
|
3782
3889
|
bindings.push({
|
|
@@ -3784,8 +3891,9 @@ function renderPermissions(graph) {
|
|
|
3784
3891
|
path: joinRoutePaths(controller.path, route.path),
|
|
3785
3892
|
controller: controller.className,
|
|
3786
3893
|
handler: route.handler,
|
|
3787
|
-
...
|
|
3788
|
-
...perm === undefined ? {} : { permission: perm }
|
|
3894
|
+
...effectiveCommand === undefined ? {} : { command: effectiveCommand },
|
|
3895
|
+
...perm === undefined ? {} : { permission: perm },
|
|
3896
|
+
...idempotency === undefined ? {} : { idempotency }
|
|
3789
3897
|
});
|
|
3790
3898
|
}
|
|
3791
3899
|
}
|
|
@@ -3811,6 +3919,7 @@ function renderPermissions(graph) {
|
|
|
3811
3919
|
" handler: string;",
|
|
3812
3920
|
" command?: string;",
|
|
3813
3921
|
" permission?: string;",
|
|
3922
|
+
' idempotency?: "required" | "none";',
|
|
3814
3923
|
"}",
|
|
3815
3924
|
"",
|
|
3816
3925
|
"export const RoutePermissions: RoutePermissionBinding[] = " + JSON.stringify(bindings, null, 2) + ";",
|
|
@@ -3854,6 +3963,8 @@ var HEADER = "// GENERATED BY @supacloud/compiler — do not edit", INTERFACES =
|
|
|
3854
3963
|
/** True when the handler returns a native Response rather than a framework value. */
|
|
3855
3964
|
nativeResponse?: boolean;
|
|
3856
3965
|
command?: string;
|
|
3966
|
+
/** Transport-level idempotency contract, independent of route command binding. */
|
|
3967
|
+
idempotency?: "required" | "none";
|
|
3857
3968
|
guards?: string[];
|
|
3858
3969
|
canMatch?: string[];
|
|
3859
3970
|
canDeactivate?: string[];
|
|
@@ -4091,6 +4202,7 @@ async function destroyServiceInstances(
|
|
|
4091
4202
|
`;
|
|
4092
4203
|
var init_generate = __esm(() => {
|
|
4093
4204
|
init_contract_manifest();
|
|
4205
|
+
init_feature();
|
|
4094
4206
|
});
|
|
4095
4207
|
|
|
4096
4208
|
// src/route-contracts.ts
|
|
@@ -14813,7 +14925,7 @@ function parseDeliveryPlanResult(value) {
|
|
|
14813
14925
|
throw new Error("Invalid delivery plan result.");
|
|
14814
14926
|
return value;
|
|
14815
14927
|
}
|
|
14816
|
-
var objectOptions, reference, references, kind, isolation, DeliveryFilePathSchema, functionTarget, DeliveryOptionsSchema, DeliveryDiagnosticSchema, routeSchema, jobSchema, DeliveryTargetSchema, DeliveryPlanSchema, DeliveryPlanResultSchema, DeliveryConfigurationError;
|
|
14928
|
+
var objectOptions, reference, references, kind, isolation, mode, DeliveryFilePathSchema, functionTarget, DeliveryOptionsSchema, DeliveryDiagnosticSchema, routeSchema, jobSchema, DeliveryTargetSchema, DeliveryPlanSchema, DeliveryPlanResultSchema, DeliveryConfigurationError;
|
|
14817
14929
|
var init_delivery_schema = __esm(() => {
|
|
14818
14930
|
init_build();
|
|
14819
14931
|
init_value2();
|
|
@@ -14823,6 +14935,7 @@ var init_delivery_schema = __esm(() => {
|
|
|
14823
14935
|
references = _Array_(reference, { uniqueItems: true });
|
|
14824
14936
|
kind = Union([Literal("api"), Literal("jobs"), Literal("webhook"), Literal("function")]);
|
|
14825
14937
|
isolation = Union([Literal("shared"), Literal("process")]);
|
|
14938
|
+
mode = Union([Literal("required"), Literal("none")]);
|
|
14826
14939
|
DeliveryFilePathSchema = String2({
|
|
14827
14940
|
maxLength: 512,
|
|
14828
14941
|
pattern: "^(?:[A-Za-z0-9_-][A-Za-z0-9_.-]*/)*[A-Za-z0-9_-][A-Za-z0-9_.-]*$"
|
|
@@ -14896,6 +15009,7 @@ var init_delivery_schema = __esm(() => {
|
|
|
14896
15009
|
]),
|
|
14897
15010
|
path: String2(),
|
|
14898
15011
|
command: Optional(String2()),
|
|
15012
|
+
idempotency: Optional(mode),
|
|
14899
15013
|
reason: Union([Literal("default-http"), Literal("explicit-module")])
|
|
14900
15014
|
}, objectOptions);
|
|
14901
15015
|
jobSchema = _Object_({
|
|
@@ -15154,7 +15268,12 @@ function parseDeliveryBuildManifest(value) {
|
|
|
15154
15268
|
throw new Error("Invalid delivery object inventory.");
|
|
15155
15269
|
}
|
|
15156
15270
|
}
|
|
15157
|
-
const routes = value.plan.targets.flatMap((target) => target.routes.map((route) => ({
|
|
15271
|
+
const routes = value.plan.targets.flatMap((target) => target.routes.map((route) => ({
|
|
15272
|
+
method: route.method,
|
|
15273
|
+
path: route.path,
|
|
15274
|
+
target: target.name,
|
|
15275
|
+
...route.idempotency === undefined ? {} : { idempotency: route.idempotency }
|
|
15276
|
+
})));
|
|
15158
15277
|
const jobs = value.plan.targets.flatMap((target) => target.jobs.map((job) => ({ name: job.name, target: target.name })));
|
|
15159
15278
|
if (canonical2(value.routes) !== canonical2(routes) || canonical2(value.jobs) !== canonical2(jobs)) {
|
|
15160
15279
|
throw new Error("Delivery mappings do not match planned ownership.");
|
|
@@ -15205,7 +15324,8 @@ var init_delivery_build_schema = __esm(() => {
|
|
|
15205
15324
|
routes: _Array_(_Object_({
|
|
15206
15325
|
method: String2(),
|
|
15207
15326
|
path: String2(),
|
|
15208
|
-
target: String2()
|
|
15327
|
+
target: String2(),
|
|
15328
|
+
idempotency: Optional(Union([Literal("required"), Literal("none")]))
|
|
15209
15329
|
}, closed2)),
|
|
15210
15330
|
jobs: _Array_(_Object_({ name: String2(), target: String2() }, closed2))
|
|
15211
15331
|
}, closed2);
|
|
@@ -15424,7 +15544,9 @@ function createExecutionPlans(graph) {
|
|
|
15424
15544
|
const aspects = (boundary, refs = []) => refs.map((ref, index) => `${boundary}.aspect[${index}]:${ref.name}`);
|
|
15425
15545
|
return graph.modules.flatMap((module) => [
|
|
15426
15546
|
...module.controllers.flatMap((controller) => controller.routes.map((route) => {
|
|
15427
|
-
const
|
|
15547
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
15548
|
+
const commandRef = route.command ?? featureTransition?.command;
|
|
15549
|
+
const command = module.commands.find((item) => item.className === commandRef || item.name === commandRef);
|
|
15428
15550
|
const path = `${controller.path}/${route.path}`.replace(/\/+/g, "/");
|
|
15429
15551
|
return {
|
|
15430
15552
|
module: module.name,
|
|
@@ -15574,6 +15696,7 @@ function exportGraphDot(graph) {
|
|
|
15574
15696
|
}
|
|
15575
15697
|
var init_inspect = __esm(() => {
|
|
15576
15698
|
init_route_contracts();
|
|
15699
|
+
init_feature();
|
|
15577
15700
|
});
|
|
15578
15701
|
|
|
15579
15702
|
// src/repair-plan.ts
|
|
@@ -15827,7 +15950,7 @@ function parseDevelopmentContext(input) {
|
|
|
15827
15950
|
throw invalid();
|
|
15828
15951
|
}
|
|
15829
15952
|
}
|
|
15830
|
-
var APPLICATION_DEVELOPMENT_SCHEMA = "supacloud.application-development.v1", APPLICATION_DEVELOPMENT_ARCHIVE_MAX_BYTES = 524288, APPLICATION_DEVELOPMENT_LIMITS, DevelopmentContractError, closed4, text2, texts, count,
|
|
15953
|
+
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
15954
|
var init_application_development = __esm(() => {
|
|
15832
15955
|
init_build();
|
|
15833
15956
|
init_value2();
|
|
@@ -15855,7 +15978,7 @@ var init_application_development = __esm(() => {
|
|
|
15855
15978
|
text2 = String2({ minLength: 1, maxLength: 512, pattern: "^[^\\u0000-\\u001f\\u007f]+$" });
|
|
15856
15979
|
texts = _Array_(text2);
|
|
15857
15980
|
count = Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER });
|
|
15858
|
-
|
|
15981
|
+
mode2 = Union([Literal("required"), Literal("none")]);
|
|
15859
15982
|
operations2 = _Array_(Union([Literal("read"), Literal("write"), Literal("publish"), Literal("consume")]));
|
|
15860
15983
|
resourceUse = _Object_({ resource: text2, operations: operations2 }, closed4);
|
|
15861
15984
|
schemaKind = Optional(Union([Literal("opaque"), Literal("declared")]));
|
|
@@ -15890,6 +16013,7 @@ var init_application_development = __esm(() => {
|
|
|
15890
16013
|
controller: text2,
|
|
15891
16014
|
handler: text2,
|
|
15892
16015
|
command: Optional(text2),
|
|
16016
|
+
idempotency: Optional(mode2),
|
|
15893
16017
|
aspects: texts,
|
|
15894
16018
|
schemaKinds: Optional(_Object_({
|
|
15895
16019
|
body: schemaKind,
|
|
@@ -15904,8 +16028,8 @@ var init_application_development = __esm(() => {
|
|
|
15904
16028
|
module: text2,
|
|
15905
16029
|
name: text2,
|
|
15906
16030
|
permission: Optional(text2),
|
|
15907
|
-
transaction:
|
|
15908
|
-
idempotency:
|
|
16031
|
+
transaction: mode2,
|
|
16032
|
+
idempotency: mode2,
|
|
15909
16033
|
audit: Optional(text2),
|
|
15910
16034
|
resources: _Array_(resourceUse)
|
|
15911
16035
|
}, closed4), { maxItems: APPLICATION_DEVELOPMENT_LIMITS.commands }),
|
|
@@ -16012,7 +16136,11 @@ function createApplicationDevelopmentContext(graph, options = {}) {
|
|
|
16012
16136
|
for (const controller of module.controllers) {
|
|
16013
16137
|
for (const route of controller.routes) {
|
|
16014
16138
|
const candidates = module.commands.filter((command) => command.className === route.command || command.name === route.command);
|
|
16015
|
-
const
|
|
16139
|
+
const commandNode = candidates.length === 1 ? candidates[0] : undefined;
|
|
16140
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
16141
|
+
const featureCommandNode = featureTransition?.command === undefined ? undefined : module.commands.find((candidate) => candidate.className === featureTransition.command || candidate.name === featureTransition.command);
|
|
16142
|
+
const command = commandNode?.name ?? featureCommandNode?.name;
|
|
16143
|
+
const idempotency = route.idempotency ?? commandNode?.idempotency ?? featureTransition?.idempotency;
|
|
16016
16144
|
const kinds = schemaKinds(route);
|
|
16017
16145
|
routes.push({
|
|
16018
16146
|
module: module.name,
|
|
@@ -16021,6 +16149,7 @@ function createApplicationDevelopmentContext(graph, options = {}) {
|
|
|
16021
16149
|
controller: controller.className,
|
|
16022
16150
|
handler: route.handler,
|
|
16023
16151
|
...command === undefined ? {} : { command },
|
|
16152
|
+
...idempotency === undefined ? {} : { idempotency },
|
|
16024
16153
|
aspects: (route.aspects ?? []).map((aspect) => aspect.name),
|
|
16025
16154
|
...kinds === undefined ? {} : { schemaKinds: kinds }
|
|
16026
16155
|
});
|
|
@@ -16141,6 +16270,7 @@ var init_application_development2 = __esm(() => {
|
|
|
16141
16270
|
init_application_development();
|
|
16142
16271
|
init_inspect();
|
|
16143
16272
|
init_execution_snapshot();
|
|
16273
|
+
init_feature();
|
|
16144
16274
|
APPLICATION_DEVELOPMENT_LIMITS2 = APPLICATION_DEVELOPMENT_LIMITS;
|
|
16145
16275
|
ApplicationDevelopmentError = class ApplicationDevelopmentError extends Error {
|
|
16146
16276
|
code;
|
|
@@ -19463,6 +19593,10 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
|
|
|
19463
19593
|
if (!ts7.isObjectLiteralExpression(input))
|
|
19464
19594
|
return invalid();
|
|
19465
19595
|
const name = stringLiteralProp(input, "name");
|
|
19596
|
+
const databaseExpr = getProp(input, "database");
|
|
19597
|
+
const database = databaseExpr && ts7.isObjectLiteralExpression(databaseExpr) ? parseFeatureDatabaseSource(databaseExpr) : undefined;
|
|
19598
|
+
if (databaseExpr !== undefined && database === undefined)
|
|
19599
|
+
return invalid();
|
|
19466
19600
|
const tenantField = stringLiteralProp(input, "tenantField");
|
|
19467
19601
|
const statesExpr = getProp(input, "states");
|
|
19468
19602
|
const transitionObject = getProp(input, "transitions");
|
|
@@ -19495,6 +19629,10 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
|
|
|
19495
19629
|
const idempotency = commandModeProp(options, "idempotency");
|
|
19496
19630
|
const audit = stringLiteralProp(options, "audit");
|
|
19497
19631
|
const job = stringLiteralProp(options, "job");
|
|
19632
|
+
const invalidateTagsExpr = getProp(options, "invalidateTags");
|
|
19633
|
+
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;
|
|
19634
|
+
if (invalidateTagsExpr !== undefined && invalidateTags === undefined)
|
|
19635
|
+
return invalid();
|
|
19498
19636
|
const persistenceExpr = getProp(options, "persistence");
|
|
19499
19637
|
const persistence = persistenceExpr && ts7.isObjectLiteralExpression(persistenceExpr) ? parseFeaturePersistence(persistenceExpr) : undefined;
|
|
19500
19638
|
if (persistenceExpr && !persistence)
|
|
@@ -19511,18 +19649,36 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
|
|
|
19511
19649
|
...idempotency === undefined ? {} : { idempotency },
|
|
19512
19650
|
...audit === undefined ? {} : { audit },
|
|
19513
19651
|
...job === undefined ? {} : { job },
|
|
19652
|
+
...invalidateTags === undefined ? {} : { invalidateTags },
|
|
19514
19653
|
...persistence === undefined ? {} : { persistence }
|
|
19515
19654
|
});
|
|
19516
19655
|
}
|
|
19517
19656
|
return {
|
|
19518
19657
|
name,
|
|
19519
19658
|
states,
|
|
19659
|
+
...database === undefined ? {} : { database },
|
|
19520
19660
|
...tenantField === undefined ? {} : { tenantField },
|
|
19521
19661
|
transitions,
|
|
19522
19662
|
file: sourcePath(ctx.rootDir, input.getSourceFile().fileName),
|
|
19523
19663
|
line: lineOf(input)
|
|
19524
19664
|
};
|
|
19525
19665
|
}
|
|
19666
|
+
function parseFeatureDatabaseSource(input) {
|
|
19667
|
+
if (input.properties.some((property) => !ts7.isPropertyAssignment(property)))
|
|
19668
|
+
return;
|
|
19669
|
+
const source = stringLiteralProp(input, "source");
|
|
19670
|
+
const table = stringLiteralProp(input, "table");
|
|
19671
|
+
const rowType = stringLiteralProp(input, "rowType");
|
|
19672
|
+
if (!source || !table || !rowType)
|
|
19673
|
+
return;
|
|
19674
|
+
if (source.startsWith("/") || source.includes("\\") || source.split("/").includes("..") || !/^[A-Za-z0-9._/-]+$/.test(source))
|
|
19675
|
+
return;
|
|
19676
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*$/.test(table))
|
|
19677
|
+
return;
|
|
19678
|
+
if (!/^[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)*$/.test(rowType))
|
|
19679
|
+
return;
|
|
19680
|
+
return { source, table, rowType };
|
|
19681
|
+
}
|
|
19526
19682
|
function parseFeaturePersistence(input) {
|
|
19527
19683
|
if (input.properties.some((property) => !ts7.isPropertyAssignment(property)))
|
|
19528
19684
|
return;
|
|
@@ -19613,6 +19769,54 @@ function checkedCommandMode(object, property, ctx, command) {
|
|
|
19613
19769
|
}
|
|
19614
19770
|
return value;
|
|
19615
19771
|
}
|
|
19772
|
+
function checkedRouteIdempotency(object, ctx, route) {
|
|
19773
|
+
const expression = getProp(object, "idempotency");
|
|
19774
|
+
const value = stringLiteralProp(object, "idempotency");
|
|
19775
|
+
if (expression && value !== "required" && value !== "none") {
|
|
19776
|
+
const file = sourcePath(ctx.rootDir, expression.getSourceFile().fileName);
|
|
19777
|
+
ctx.diagnostics.push({
|
|
19778
|
+
severity: "error",
|
|
19779
|
+
code: "invalid-route-idempotency",
|
|
19780
|
+
errorCode: "SC3028",
|
|
19781
|
+
docsUrl: "https://supacloud.dev/errors/SC3028",
|
|
19782
|
+
message: `${route}.idempotency must be the explicit literal "required" or "none".`,
|
|
19783
|
+
file,
|
|
19784
|
+
line: lineOf(expression),
|
|
19785
|
+
suggestion: `Choose the intended idempotency policy explicitly; use "required" for replayable mutations or "none" for routes without that contract.`
|
|
19786
|
+
});
|
|
19787
|
+
}
|
|
19788
|
+
return value === "required" || value === "none" ? value : undefined;
|
|
19789
|
+
}
|
|
19790
|
+
function checkedRouteTags(expression, ctx, route) {
|
|
19791
|
+
if (expression === undefined)
|
|
19792
|
+
return;
|
|
19793
|
+
if (!ts7.isArrayLiteralExpression(expression) || expression.elements.some((element) => !ts7.isStringLiteral(element) || element.text.trim().length === 0)) {
|
|
19794
|
+
ctx.diagnostics.push({
|
|
19795
|
+
severity: "error",
|
|
19796
|
+
code: "invalid-route-tags",
|
|
19797
|
+
errorCode: "SC3029",
|
|
19798
|
+
docsUrl: "https://supacloud.dev/errors/SC3029",
|
|
19799
|
+
message: `${route}.tags must be a static array of non-empty string literals.`,
|
|
19800
|
+
file: sourcePath(ctx.rootDir, expression.getSourceFile().fileName),
|
|
19801
|
+
line: lineOf(expression),
|
|
19802
|
+
suggestion: 'Declare stable cache tags explicitly, for example tags: ["cases.list"].'
|
|
19803
|
+
});
|
|
19804
|
+
return;
|
|
19805
|
+
}
|
|
19806
|
+
const tags = expression.elements.map((element) => element.text);
|
|
19807
|
+
if (new Set(tags).size !== tags.length) {
|
|
19808
|
+
ctx.diagnostics.push({
|
|
19809
|
+
severity: "error",
|
|
19810
|
+
code: "duplicate-route-tags",
|
|
19811
|
+
errorCode: "SC3030",
|
|
19812
|
+
docsUrl: "https://supacloud.dev/errors/SC3030",
|
|
19813
|
+
message: `${route}.tags must not contain duplicate values.`,
|
|
19814
|
+
file: sourcePath(ctx.rootDir, expression.getSourceFile().fileName),
|
|
19815
|
+
line: lineOf(expression)
|
|
19816
|
+
});
|
|
19817
|
+
}
|
|
19818
|
+
return tags;
|
|
19819
|
+
}
|
|
19616
19820
|
function parseProvider(el, exportsSet, ctx) {
|
|
19617
19821
|
const file = sourcePath(ctx.rootDir, el.getSourceFile().fileName);
|
|
19618
19822
|
const line = lineOf(el);
|
|
@@ -20392,7 +20596,11 @@ function parseController(input, ctx) {
|
|
|
20392
20596
|
if (optionsObject) {
|
|
20393
20597
|
const parsePolicy = routePolicyProperty(optionsObject, "parse", ctx);
|
|
20394
20598
|
const deletePolicy = routePolicyProperty(optionsObject, "allowDeleteBody", ctx);
|
|
20395
|
-
|
|
20599
|
+
const idempotency = checkedRouteIdempotency(optionsObject, ctx, route.handler);
|
|
20600
|
+
if (idempotency !== undefined)
|
|
20601
|
+
route.idempotency = idempotency;
|
|
20602
|
+
const tagsPolicy = routePolicyProperty(optionsObject, "tags", ctx);
|
|
20603
|
+
if (parsePolicy.unresolved || deletePolicy.unresolved || tagsPolicy.unresolved) {
|
|
20396
20604
|
ctx.diagnostics.push({
|
|
20397
20605
|
severity: "error",
|
|
20398
20606
|
code: "unresolved-route-body-policy",
|
|
@@ -20400,6 +20608,9 @@ function parseController(input, ctx) {
|
|
|
20400
20608
|
message: `Route ${route.handler} body policy must be statically resolvable; dynamic spreads, computed keys and shorthand policies are unsupported.`
|
|
20401
20609
|
});
|
|
20402
20610
|
}
|
|
20611
|
+
const tags = checkedRouteTags(tagsPolicy.value, ctx, route.handler);
|
|
20612
|
+
if (tags !== undefined)
|
|
20613
|
+
route.tags = tags;
|
|
20403
20614
|
const parse = parsePolicy.value;
|
|
20404
20615
|
if (parse) {
|
|
20405
20616
|
if (ts7.isStringLiteral(parse) && parse.text === "none")
|
|
@@ -22250,6 +22461,7 @@ function validateClientSchemaBoundaries(graph, options) {
|
|
|
22250
22461
|
|
|
22251
22462
|
// src/effect-governance.ts
|
|
22252
22463
|
init_validate();
|
|
22464
|
+
init_feature();
|
|
22253
22465
|
function diagnostic(code, message, file, suggestion) {
|
|
22254
22466
|
const metadata = COMPILER_DIAGNOSTIC_CODES[code];
|
|
22255
22467
|
return {
|
|
@@ -22287,8 +22499,10 @@ function validateEffectPolicies(graph, options = {}) {
|
|
|
22287
22499
|
if (effect.retry === "explicit" && effect.maxAttempts === undefined) {
|
|
22288
22500
|
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
22501
|
}
|
|
22290
|
-
if (
|
|
22291
|
-
const
|
|
22502
|
+
if (effect.retry === "explicit") {
|
|
22503
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
22504
|
+
const commandRef = route.command ?? featureTransition?.command;
|
|
22505
|
+
const command = commandRef === undefined ? undefined : commands.get(commandRef) ?? [...commands.values()].find((candidate) => candidate.name === commandRef);
|
|
22292
22506
|
if (command && command.idempotency !== "required") {
|
|
22293
22507
|
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
22508
|
}
|
|
@@ -22543,6 +22757,7 @@ function resolveTypeSafety(options) {
|
|
|
22543
22757
|
init_delivery_schema();
|
|
22544
22758
|
|
|
22545
22759
|
// src/delivery-plan.ts
|
|
22760
|
+
init_feature();
|
|
22546
22761
|
init_validate();
|
|
22547
22762
|
var compare2 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
22548
22763
|
var sorted = (values) => [...new Set(values)].sort(compare2);
|
|
@@ -22662,6 +22877,10 @@ function createDeliveryPlan(graph, input, validation) {
|
|
|
22662
22877
|
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
22878
|
}
|
|
22664
22879
|
publicRoutes.set(key, target.name);
|
|
22880
|
+
const command = route.command === undefined ? undefined : module.commands.find((candidate) => candidate.className === route.command || candidate.name === route.command);
|
|
22881
|
+
const featureTransition = featureTransitionForRoute(module, controller, route);
|
|
22882
|
+
const effectiveCommand = route.command ?? featureTransition?.command;
|
|
22883
|
+
const idempotency = route.idempotency ?? command?.idempotency ?? featureTransition?.idempotency;
|
|
22665
22884
|
target.roots.push(module.name);
|
|
22666
22885
|
target.routes.push({
|
|
22667
22886
|
module: module.name,
|
|
@@ -22669,7 +22888,8 @@ function createDeliveryPlan(graph, input, validation) {
|
|
|
22669
22888
|
handler: route.handler,
|
|
22670
22889
|
method: route.method,
|
|
22671
22890
|
path,
|
|
22672
|
-
...
|
|
22891
|
+
...effectiveCommand === undefined ? {} : { command: effectiveCommand },
|
|
22892
|
+
...idempotency === undefined ? {} : { idempotency },
|
|
22673
22893
|
reason: owner === undefined ? "default-http" : "explicit-module"
|
|
22674
22894
|
});
|
|
22675
22895
|
}
|
|
@@ -23700,7 +23920,12 @@ async function buildDeliveryProject(options, delivery) {
|
|
|
23700
23920
|
deploymentReady: false,
|
|
23701
23921
|
plan: planned.plan,
|
|
23702
23922
|
objects,
|
|
23703
|
-
routes: planned.plan.targets.flatMap((target) => target.routes.map((route) => ({
|
|
23923
|
+
routes: planned.plan.targets.flatMap((target) => target.routes.map((route) => ({
|
|
23924
|
+
method: route.method,
|
|
23925
|
+
path: route.path,
|
|
23926
|
+
target: target.name,
|
|
23927
|
+
...route.idempotency === undefined ? {} : { idempotency: route.idempotency }
|
|
23928
|
+
}))),
|
|
23704
23929
|
jobs: planned.plan.targets.flatMap((target) => target.jobs.map((job) => ({ name: job.name, target: target.name })))
|
|
23705
23930
|
});
|
|
23706
23931
|
phase = "artifact-integrity";
|
|
@@ -24450,10 +24675,10 @@ import { readFileSync as readFileSync4 } from "node:fs";
|
|
|
24450
24675
|
import { resolve as resolve17 } from "node:path";
|
|
24451
24676
|
|
|
24452
24677
|
// src/migration-app-version.ts
|
|
24453
|
-
var MIGRATION_APP_VERSION = "0.
|
|
24678
|
+
var MIGRATION_APP_VERSION = "0.31.0";
|
|
24454
24679
|
|
|
24455
24680
|
// src/migration-elysia-version.ts
|
|
24456
|
-
var MIGRATION_ELYSIA_VERSION = "0.
|
|
24681
|
+
var MIGRATION_ELYSIA_VERSION = "0.35.1";
|
|
24457
24682
|
|
|
24458
24683
|
// src/migration-policy.ts
|
|
24459
24684
|
var cachedCompilerVersion;
|
|
@@ -25488,8 +25713,8 @@ function validFeatureInventory(value, jobs, commands, routes, rpc) {
|
|
|
25488
25713
|
return false;
|
|
25489
25714
|
}
|
|
25490
25715
|
const commandName = transition["command"];
|
|
25716
|
+
const command = commandName === undefined ? undefined : commands.find((candidate) => isRecord2(candidate) && (candidate["name"] === commandName || candidate["className"] === commandName));
|
|
25491
25717
|
if (commandName !== undefined) {
|
|
25492
|
-
const command = commands.find((candidate) => isRecord2(candidate) && (candidate["name"] === commandName || candidate["className"] === commandName));
|
|
25493
25718
|
if (!isRecord2(command))
|
|
25494
25719
|
return false;
|
|
25495
25720
|
if (transition["permission"] !== undefined && command["permission"] !== transition["permission"])
|
|
@@ -25520,9 +25745,21 @@ function validFeatureInventory(value, jobs, commands, routes, rpc) {
|
|
|
25520
25745
|
if (routeBinding === "controller") {
|
|
25521
25746
|
if (route["command"] !== undefined)
|
|
25522
25747
|
return false;
|
|
25523
|
-
} else
|
|
25524
|
-
|
|
25748
|
+
} else {
|
|
25749
|
+
if (typeof route["command"] !== "string")
|
|
25750
|
+
return false;
|
|
25751
|
+
const routeCommand = commands.find((candidate) => isRecord2(candidate) && (candidate["name"] === route["command"] || candidate["className"] === route["command"]));
|
|
25752
|
+
if (!isRecord2(routeCommand) || !isRecord2(command) || routeCommand["name"] !== command["name"])
|
|
25753
|
+
return false;
|
|
25525
25754
|
}
|
|
25755
|
+
if (transition["permission"] !== undefined && route["permission"] !== transition["permission"])
|
|
25756
|
+
return false;
|
|
25757
|
+
if (transition["idempotency"] !== undefined && route["idempotency"] !== transition["idempotency"])
|
|
25758
|
+
return false;
|
|
25759
|
+
if (persistence !== undefined && route["idempotency"] !== "required")
|
|
25760
|
+
return false;
|
|
25761
|
+
if (route["idempotency"] === "required" && (route["method"] === "GET" || route["method"] === "HEAD" || route["method"] === "OPTIONS"))
|
|
25762
|
+
return false;
|
|
25526
25763
|
}
|
|
25527
25764
|
const job = transition["job"];
|
|
25528
25765
|
if (job === undefined)
|