@warlock.js/core 5.26.0 → 5.27.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.
Files changed (98) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/esm/cli/commands/openapi-generator.action.mjs +43 -0
  3. package/esm/cli/commands/openapi-generator.action.mjs.map +1 -0
  4. package/esm/cli/commands/openapi-generator.command.mjs +44 -0
  5. package/esm/cli/commands/openapi-generator.command.mjs.map +1 -0
  6. package/esm/cli/framework-cli-commands.mjs +2 -0
  7. package/esm/cli/framework-cli-commands.mjs.map +1 -1
  8. package/esm/dev-server/build-development-openapi-document.mjs +36 -0
  9. package/esm/dev-server/build-development-openapi-document.mjs.map +1 -0
  10. package/esm/dev-server/collect-development-api-routes.mjs +27 -0
  11. package/esm/dev-server/collect-development-api-routes.mjs.map +1 -0
  12. package/esm/dev-server/dev-logger.mjs +9 -1
  13. package/esm/dev-server/dev-logger.mjs.map +1 -1
  14. package/esm/dev-server/development-server.mjs +6 -3
  15. package/esm/dev-server/development-server.mjs.map +1 -1
  16. package/esm/dev-server/files-orchestrator.mjs +17 -1
  17. package/esm/dev-server/files-orchestrator.mjs.map +1 -1
  18. package/esm/dev-server/model-resource-typings.mjs +246 -0
  19. package/esm/dev-server/model-resource-typings.mjs.map +1 -0
  20. package/esm/dev-server/type-generator.mjs +46 -2
  21. package/esm/dev-server/type-generator.mjs.map +1 -1
  22. package/esm/http/index.d.mts +2 -1
  23. package/esm/http/request.d.mts +2 -2
  24. package/esm/http/request.d.mts.map +1 -1
  25. package/esm/http/serialized.d.mts +114 -0
  26. package/esm/http/serialized.d.mts.map +1 -0
  27. package/esm/index.d.mts +8 -3
  28. package/esm/index.mjs +4 -1
  29. package/esm/openapi/build-openapi-document.d.mts +17 -0
  30. package/esm/openapi/build-openapi-document.d.mts.map +1 -0
  31. package/esm/openapi/build-openapi-document.mjs +124 -0
  32. package/esm/openapi/build-openapi-document.mjs.map +1 -0
  33. package/esm/openapi/cast-schema.mjs +89 -0
  34. package/esm/openapi/cast-schema.mjs.map +1 -0
  35. package/esm/openapi/collect-openapi-document.mjs +40 -0
  36. package/esm/openapi/collect-openapi-document.mjs.map +1 -0
  37. package/esm/openapi/development-openapi-provider.d.mts +17 -0
  38. package/esm/openapi/development-openapi-provider.d.mts.map +1 -0
  39. package/esm/openapi/development-openapi-provider.mjs +21 -0
  40. package/esm/openapi/development-openapi-provider.mjs.map +1 -0
  41. package/esm/openapi/error-responses.mjs +67 -0
  42. package/esm/openapi/error-responses.mjs.map +1 -0
  43. package/esm/openapi/index.d.mts +3 -0
  44. package/esm/openapi/index.mjs +4 -0
  45. package/esm/openapi/openapi-child-output.mjs +57 -0
  46. package/esm/openapi/openapi-child-output.mjs.map +1 -0
  47. package/esm/openapi/openapi-protocol.mjs +30 -0
  48. package/esm/openapi/openapi-protocol.mjs.map +1 -0
  49. package/esm/openapi/paths.mjs +63 -0
  50. package/esm/openapi/paths.mjs.map +1 -0
  51. package/esm/openapi/request-schemas.mjs +138 -0
  52. package/esm/openapi/request-schemas.mjs.map +1 -0
  53. package/esm/openapi/response-schemas.mjs +85 -0
  54. package/esm/openapi/response-schemas.mjs.map +1 -0
  55. package/esm/openapi/schema-registry.mjs +143 -0
  56. package/esm/openapi/schema-registry.mjs.map +1 -0
  57. package/esm/openapi/security.mjs +98 -0
  58. package/esm/openapi/security.mjs.map +1 -0
  59. package/esm/openapi/types.d.mts +119 -0
  60. package/esm/openapi/types.d.mts.map +1 -0
  61. package/esm/production/registration-child-runner.mjs +139 -0
  62. package/esm/production/registration-child-runner.mjs.map +1 -0
  63. package/esm/production/route-registration-child.mjs +30 -2
  64. package/esm/production/route-registration-child.mjs.map +1 -1
  65. package/esm/production/route-registration-snapshot.mjs +22 -119
  66. package/esm/production/route-registration-snapshot.mjs.map +1 -1
  67. package/esm/resource/define-resource.d.mts +44 -14
  68. package/esm/resource/define-resource.d.mts.map +1 -1
  69. package/esm/resource/define-resource.mjs +0 -40
  70. package/esm/resource/define-resource.mjs.map +1 -1
  71. package/esm/resource/index.d.mts +1 -1
  72. package/esm/resource/resource.d.mts +4 -4
  73. package/esm/resource/resource.d.mts.map +1 -1
  74. package/esm/resource/resource.mjs.map +1 -1
  75. package/esm/resource/types.d.mts +89 -4
  76. package/esm/resource/types.d.mts.map +1 -1
  77. package/esm/restful/restful.d.mts +5 -5
  78. package/esm/restful/restful.d.mts.map +1 -1
  79. package/esm/router/index.d.mts +1 -1
  80. package/esm/router/named-api-routes-with-responses.mjs +59 -0
  81. package/esm/router/named-api-routes-with-responses.mjs.map +1 -0
  82. package/esm/router/response-schema-types.mjs +100 -0
  83. package/esm/router/response-schema-types.mjs.map +1 -0
  84. package/esm/router/router.d.mts +2 -2
  85. package/esm/router/router.d.mts.map +1 -1
  86. package/esm/router/router.mjs +38 -11
  87. package/esm/router/router.mjs.map +1 -1
  88. package/esm/router/types.d.mts +16 -1
  89. package/esm/router/types.d.mts.map +1 -1
  90. package/llms-full.txt +265 -5
  91. package/llms.txt +3 -2
  92. package/package.json +13 -13
  93. package/skills/README.md +3 -2
  94. package/skills/create-controller/SKILL.md +39 -2
  95. package/skills/define-resource/SKILL.md +100 -1
  96. package/skills/generate-openapi/SKILL.md +120 -0
  97. package/skills/index.md +3 -3
  98. package/skills/validate-input/SKILL.md +2 -2
@@ -0,0 +1,21 @@
1
+ //#region ../core/src/openapi/development-openapi-provider.ts
2
+ let provider;
3
+ /**
4
+ * Called by the dev server at startup. The builder lives under `dev-server/`, so the
5
+ * production entry never reaches it; this registry is the only thing the root exports.
6
+ */
7
+ function registerDevelopmentOpenApiProvider(build) {
8
+ provider = build;
9
+ }
10
+ /**
11
+ * The OpenAPI document for the routes registered in this process, built by the dev
12
+ * server's provider. Throws outside `warlock dev`, where no provider is registered.
13
+ */
14
+ async function getDevelopmentOpenApiDocument() {
15
+ if (!provider) throw new Error("No development OpenAPI provider is registered; the API docs are only available under `warlock dev`.");
16
+ return provider();
17
+ }
18
+
19
+ //#endregion
20
+ export { getDevelopmentOpenApiDocument, registerDevelopmentOpenApiProvider };
21
+ //# sourceMappingURL=development-openapi-provider.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"development-openapi-provider.mjs","names":[],"sources":["../../../../../../../core/src/openapi/development-openapi-provider.ts"],"sourcesContent":["import type { OpenApiBuildResult } from \"./types\";\n\ntype DevelopmentOpenApiProvider = () => Promise<OpenApiBuildResult>;\n\nlet provider: DevelopmentOpenApiProvider | undefined;\n\n/**\n * Called by the dev server at startup. The builder lives under `dev-server/`, so the\n * production entry never reaches it; this registry is the only thing the root exports.\n */\nexport function registerDevelopmentOpenApiProvider(build: DevelopmentOpenApiProvider): void {\n provider = build;\n}\n\n/**\n * The OpenAPI document for the routes registered in this process, built by the dev\n * server's provider. Throws outside `warlock dev`, where no provider is registered.\n */\nexport async function getDevelopmentOpenApiDocument(): Promise<OpenApiBuildResult> {\n if (!provider) {\n throw new Error(\n \"No development OpenAPI provider is registered; the API docs are only available under `warlock dev`.\",\n );\n }\n\n return provider();\n}\n"],"mappings":";AAIA,IAAI;;;;;AAMJ,SAAgB,mCAAmC,OAAyC;CAC1F,WAAW;AACb;;;;;AAMA,eAAsB,gCAA6D;CACjF,IAAI,CAAC,UACH,MAAM,IAAI,MACR,qGACF;CAGF,OAAO,SAAS;AAClB"}
@@ -0,0 +1,67 @@
1
+ import { describeStatus } from "./response-schemas.mjs";
2
+
3
+ //#region ../core/src/openapi/error-responses.ts
4
+ const DEFAULT_VALIDATION_RESPONSE = {
5
+ errors: "errors",
6
+ inputKey: "input",
7
+ inputError: "error",
8
+ status: 422
9
+ };
10
+ /**
11
+ * The `validation.response` config over its documented defaults, as `Response.failedSchema`
12
+ * reads it.
13
+ */
14
+ function resolveValidationResponse(config) {
15
+ const merged = { ...DEFAULT_VALIDATION_RESPONSE };
16
+ if (typeof config?.errors === "string" && config.errors) merged.errors = config.errors;
17
+ if (typeof config?.inputKey === "string" && config.inputKey) merged.inputKey = config.inputKey;
18
+ if (typeof config?.inputError === "string" && config.inputError) merged.inputError = config.inputError;
19
+ if (typeof config?.status === "number" && Number.isInteger(config.status)) merged.status = config.status;
20
+ return merged;
21
+ }
22
+ /**
23
+ * The failed-validation response: `{ [errors]: [{ [inputKey]: string, [inputError]: string }] }`
24
+ * under the configured status. The body is a shared component, emitted once.
25
+ */
26
+ function buildValidationFailedResponse(shape, registry) {
27
+ const ref = registry.shared("ValidationFailed", {
28
+ type: "object",
29
+ properties: { [shape.errors]: {
30
+ type: "array",
31
+ items: {
32
+ type: "object",
33
+ properties: {
34
+ [shape.inputKey]: { type: "string" },
35
+ [shape.inputError]: { type: "string" }
36
+ },
37
+ required: [shape.inputKey, shape.inputError]
38
+ }
39
+ } },
40
+ required: [shape.errors]
41
+ });
42
+ return {
43
+ status: String(shape.status),
44
+ response: {
45
+ description: describeStatus("422"),
46
+ content: { "application/json": { schema: ref } }
47
+ }
48
+ };
49
+ }
50
+ /**
51
+ * The 401 response of a guarded route: the conventional `{ error: string }` body.
52
+ */
53
+ function buildUnauthorizedResponse(registry) {
54
+ const ref = registry.shared("Unauthorized", {
55
+ type: "object",
56
+ properties: { error: { type: "string" } },
57
+ required: ["error"]
58
+ });
59
+ return {
60
+ description: describeStatus("401"),
61
+ content: { "application/json": { schema: ref } }
62
+ };
63
+ }
64
+
65
+ //#endregion
66
+ export { buildUnauthorizedResponse, buildValidationFailedResponse, resolveValidationResponse };
67
+ //# sourceMappingURL=error-responses.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-responses.mjs","names":[],"sources":["../../../../../../../core/src/openapi/error-responses.ts"],"sourcesContent":["import { describeStatus } from \"./response-schemas\";\nimport type { SchemaRegistry } from \"./schema-registry\";\nimport type { OpenApiResponse, OpenApiValidationResponseConfig } from \"./types\";\n\nconst DEFAULT_VALIDATION_RESPONSE = {\n errors: \"errors\",\n inputKey: \"input\",\n inputError: \"error\",\n status: 422,\n};\n\nexport type ValidationResponseShape = typeof DEFAULT_VALIDATION_RESPONSE;\n\n/**\n * The `validation.response` config over its documented defaults, as `Response.failedSchema`\n * reads it.\n */\nexport function resolveValidationResponse(\n config: OpenApiValidationResponseConfig | undefined,\n): ValidationResponseShape {\n const merged = { ...DEFAULT_VALIDATION_RESPONSE };\n\n if (typeof config?.errors === \"string\" && config.errors) {\n merged.errors = config.errors;\n }\n\n if (typeof config?.inputKey === \"string\" && config.inputKey) {\n merged.inputKey = config.inputKey;\n }\n\n if (typeof config?.inputError === \"string\" && config.inputError) {\n merged.inputError = config.inputError;\n }\n\n if (typeof config?.status === \"number\" && Number.isInteger(config.status)) {\n merged.status = config.status;\n }\n\n return merged;\n}\n\n/**\n * The failed-validation response: `{ [errors]: [{ [inputKey]: string, [inputError]: string }] }`\n * under the configured status. The body is a shared component, emitted once.\n */\nexport function buildValidationFailedResponse(\n shape: ValidationResponseShape,\n registry: SchemaRegistry,\n): { status: string; response: OpenApiResponse } {\n const ref = registry.shared(\"ValidationFailed\", {\n type: \"object\",\n properties: {\n [shape.errors]: {\n type: \"array\",\n items: {\n type: \"object\",\n properties: {\n [shape.inputKey]: { type: \"string\" },\n [shape.inputError]: { type: \"string\" },\n },\n required: [shape.inputKey, shape.inputError],\n },\n },\n },\n required: [shape.errors],\n });\n\n return {\n status: String(shape.status),\n response: {\n description: describeStatus(\"422\"),\n content: { \"application/json\": { schema: ref } },\n },\n };\n}\n\n/**\n * The 401 response of a guarded route: the conventional `{ error: string }` body.\n */\nexport function buildUnauthorizedResponse(registry: SchemaRegistry): OpenApiResponse {\n const ref = registry.shared(\"Unauthorized\", {\n type: \"object\",\n properties: { error: { type: \"string\" } },\n required: [\"error\"],\n });\n\n return {\n description: describeStatus(\"401\"),\n content: { \"application/json\": { schema: ref } },\n };\n}\n"],"mappings":";;;AAIA,MAAM,8BAA8B;CAClC,QAAQ;CACR,UAAU;CACV,YAAY;CACZ,QAAQ;AACV;;;;;AAQA,SAAgB,0BACd,QACyB;CACzB,MAAM,SAAS,EAAE,GAAG,4BAA4B;CAEhD,IAAI,OAAO,QAAQ,WAAW,YAAY,OAAO,QAC/C,OAAO,SAAS,OAAO;CAGzB,IAAI,OAAO,QAAQ,aAAa,YAAY,OAAO,UACjD,OAAO,WAAW,OAAO;CAG3B,IAAI,OAAO,QAAQ,eAAe,YAAY,OAAO,YACnD,OAAO,aAAa,OAAO;CAG7B,IAAI,OAAO,QAAQ,WAAW,YAAY,OAAO,UAAU,OAAO,MAAM,GACtE,OAAO,SAAS,OAAO;CAGzB,OAAO;AACT;;;;;AAMA,SAAgB,8BACd,OACA,UAC+C;CAC/C,MAAM,MAAM,SAAS,OAAO,oBAAoB;EAC9C,MAAM;EACN,YAAY,GACT,MAAM,SAAS;GACd,MAAM;GACN,OAAO;IACL,MAAM;IACN,YAAY;MACT,MAAM,WAAW,EAAE,MAAM,SAAS;MAClC,MAAM,aAAa,EAAE,MAAM,SAAS;IACvC;IACA,UAAU,CAAC,MAAM,UAAU,MAAM,UAAU;GAC7C;EACF,EACF;EACA,UAAU,CAAC,MAAM,MAAM;CACzB,CAAC;CAED,OAAO;EACL,QAAQ,OAAO,MAAM,MAAM;EAC3B,UAAU;GACR,aAAa,eAAe,KAAK;GACjC,SAAS,EAAE,oBAAoB,EAAE,QAAQ,IAAI,EAAE;EACjD;CACF;AACF;;;;AAKA,SAAgB,0BAA0B,UAA2C;CACnF,MAAM,MAAM,SAAS,OAAO,gBAAgB;EAC1C,MAAM;EACN,YAAY,EAAE,OAAO,EAAE,MAAM,SAAS,EAAE;EACxC,UAAU,CAAC,OAAO;CACpB,CAAC;CAED,OAAO;EACL,aAAa,eAAe,KAAK;EACjC,SAAS,EAAE,oBAAoB,EAAE,QAAQ,IAAI,EAAE;CACjD;AACF"}
@@ -0,0 +1,3 @@
1
+ import { AuthDescriptor, OpenApiBuildResult, OpenApiComponents, OpenApiContext, OpenApiDocument, OpenApiOperation, OpenApiParameter, OpenApiRequestBody, OpenApiResponse, OpenApiRouteInput, OpenApiSchema, OpenApiSecurityRequirement, OpenApiSecurityScheme, OpenApiValidationResponseConfig, WarningSink } from "./types.mjs";
2
+ import { getDevelopmentOpenApiDocument, registerDevelopmentOpenApiProvider } from "./development-openapi-provider.mjs";
3
+ import { OPENAPI_JSON_SCHEMA_DIALECT, buildOpenApiDocument } from "./build-openapi-document.mjs";
@@ -0,0 +1,4 @@
1
+ import { getDevelopmentOpenApiDocument, registerDevelopmentOpenApiProvider } from "./development-openapi-provider.mjs";
2
+ import { OPENAPI_JSON_SCHEMA_DIALECT, buildOpenApiDocument } from "./build-openapi-document.mjs";
3
+
4
+ export { };
@@ -0,0 +1,57 @@
1
+ import { httpConfig } from "../http/config.mjs";
2
+ import { router } from "../router/router.mjs";
3
+ import { buildOpenApiDocument } from "./build-openapi-document.mjs";
4
+ import { collectResourceExportMap } from "../router/named-api-routes-with-responses.mjs";
5
+ import config from "@mongez/config";
6
+ import path from "node:path";
7
+ import { readFile } from "node:fs/promises";
8
+
9
+ //#region ../core/src/openapi/openapi-child-output.ts
10
+ async function readAppPackage(cwd) {
11
+ try {
12
+ return JSON.parse(await readFile(path.join(cwd, "package.json"), "utf8"));
13
+ } catch {
14
+ return {};
15
+ }
16
+ }
17
+ function stringOr(value, fallback) {
18
+ return typeof value === "string" && value.length > 0 ? value : fallback;
19
+ }
20
+ /**
21
+ * `http://<host>:<port>` from the `http` config. A wildcard bind address is not a place a
22
+ * client can call, so it becomes `localhost`.
23
+ */
24
+ function resolveDefaultServer() {
25
+ const host = stringOr(httpConfig("host"), "localhost");
26
+ const port = httpConfig("port");
27
+ return `http://${host === "0.0.0.0" || host === "::" ? "localhost" : host}${typeof port === "number" || typeof port === "string" ? `:${port}` : ""}`;
28
+ }
29
+ /**
30
+ * Build the OpenAPI document from the routes registered in THIS process.
31
+ *
32
+ * Runs inside the registration child, after the app's route modules are loaded and config is
33
+ * available. Maps resource classes to their export names, reads `info` from the app
34
+ * `package.json`, the server from the `http` config and the 422 shape from
35
+ * `validation.response`.
36
+ */
37
+ async function buildOpenApiForChild(options) {
38
+ const routes = router.list();
39
+ const resourceMap = routes.some((route) => route.handler?.responseSchema) ? await collectResourceExportMap(options.files, options.onWarn) : void 0;
40
+ const pkg = await readAppPackage(options.cwd);
41
+ const validationResponse = config.get("validation.response", {});
42
+ return buildOpenApiDocument(routes, {
43
+ info: {
44
+ title: options.request.title ?? stringOr(pkg.name, "Warlock API"),
45
+ version: stringOr(pkg.version, "0.0.0"),
46
+ ...typeof pkg.description === "string" && pkg.description ? { description: pkg.description } : {}
47
+ },
48
+ servers: [options.request.server ?? resolveDefaultServer()],
49
+ includePages: options.request.includePages === true,
50
+ resolveResourceName: (resource) => resourceMap?.get(resource)?.exportName,
51
+ validationResponse
52
+ });
53
+ }
54
+
55
+ //#endregion
56
+ export { buildOpenApiForChild, resolveDefaultServer };
57
+ //# sourceMappingURL=openapi-child-output.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openapi-child-output.mjs","names":[],"sources":["../../../../../../../core/src/openapi/openapi-child-output.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport { readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { httpConfig } from \"../http/config\";\nimport {\n collectResourceExportMap,\n type ResourceSourceFile,\n} from \"../router/named-api-routes-with-responses\";\nimport { router } from \"../router/router\";\nimport { buildOpenApiDocument } from \"./build-openapi-document\";\nimport type { OpenApiChildRequestOptions } from \"./openapi-protocol\";\nimport type { OpenApiBuildResult, OpenApiValidationResponseConfig } from \"./types\";\n\nexport type BuildOpenApiForChildOptions = Readonly<{\n /** Application root (the child's cwd). */\n cwd: string;\n /** Every source file the orchestrator knows; `*.resource.ts(x)` ones are mapped to exports. */\n files: Iterable<ResourceSourceFile>;\n request: OpenApiChildRequestOptions;\n onWarn?: (message: string) => void;\n}>;\n\ntype AppPackage = { name?: unknown; version?: unknown; description?: unknown };\n\nasync function readAppPackage(cwd: string): Promise<AppPackage> {\n try {\n return JSON.parse(await readFile(path.join(cwd, \"package.json\"), \"utf8\")) as AppPackage;\n } catch {\n return {};\n }\n}\n\nfunction stringOr(value: unknown, fallback: string): string {\n return typeof value === \"string\" && value.length > 0 ? value : fallback;\n}\n\n/**\n * `http://<host>:<port>` from the `http` config. A wildcard bind address is not a place a\n * client can call, so it becomes `localhost`.\n */\nexport function resolveDefaultServer(): string {\n const host = stringOr(httpConfig(\"host\"), \"localhost\");\n const port = httpConfig(\"port\");\n const callable = host === \"0.0.0.0\" || host === \"::\" ? \"localhost\" : host;\n\n return `http://${callable}${typeof port === \"number\" || typeof port === \"string\" ? `:${port}` : \"\"}`;\n}\n\n/**\n * Build the OpenAPI document from the routes registered in THIS process.\n *\n * Runs inside the registration child, after the app's route modules are loaded and config is\n * available. Maps resource classes to their export names, reads `info` from the app\n * `package.json`, the server from the `http` config and the 422 shape from\n * `validation.response`.\n */\nexport async function buildOpenApiForChild(\n options: BuildOpenApiForChildOptions,\n): Promise<OpenApiBuildResult> {\n const routes = router.list();\n const declaresSchema = routes.some((route) => route.handler?.responseSchema);\n const resourceMap = declaresSchema\n ? await collectResourceExportMap(options.files, options.onWarn)\n : undefined;\n const pkg = await readAppPackage(options.cwd);\n const validationResponse = config.get(\n \"validation.response\",\n {},\n ) as OpenApiValidationResponseConfig;\n\n return buildOpenApiDocument(routes, {\n info: {\n title: options.request.title ?? stringOr(pkg.name, \"Warlock API\"),\n version: stringOr(pkg.version, \"0.0.0\"),\n ...(typeof pkg.description === \"string\" && pkg.description\n ? { description: pkg.description }\n : {}),\n },\n servers: [options.request.server ?? resolveDefaultServer()],\n includePages: options.request.includePages === true,\n resolveResourceName: (resource) => resourceMap?.get(resource)?.exportName,\n validationResponse,\n });\n}\n"],"mappings":";;;;;;;;;AAwBA,eAAe,eAAe,KAAkC;CAC9D,IAAI;EACF,OAAO,KAAK,MAAM,MAAM,SAAS,KAAK,KAAK,KAAK,cAAc,GAAG,MAAM,CAAC;CAC1E,QAAQ;EACN,OAAO,CAAC;CACV;AACF;AAEA,SAAS,SAAS,OAAgB,UAA0B;CAC1D,OAAO,OAAO,UAAU,YAAY,MAAM,SAAS,IAAI,QAAQ;AACjE;;;;;AAMA,SAAgB,uBAA+B;CAC7C,MAAM,OAAO,SAAS,WAAW,MAAM,GAAG,WAAW;CACrD,MAAM,OAAO,WAAW,MAAM;CAG9B,OAAO,UAFU,SAAS,aAAa,SAAS,OAAO,cAAc,OAEzC,OAAO,SAAS,YAAY,OAAO,SAAS,WAAW,IAAI,SAAS;AAClG;;;;;;;;;AAUA,eAAsB,qBACpB,SAC6B;CAC7B,MAAM,SAAS,OAAO,KAAK;CAE3B,MAAM,cADiB,OAAO,MAAM,UAAU,MAAM,SAAS,cAC5B,IAC7B,MAAM,yBAAyB,QAAQ,OAAO,QAAQ,MAAM,IAC5D;CACJ,MAAM,MAAM,MAAM,eAAe,QAAQ,GAAG;CAC5C,MAAM,qBAAqB,OAAO,IAChC,uBACA,CAAC,CACH;CAEA,OAAO,qBAAqB,QAAQ;EAClC,MAAM;GACJ,OAAO,QAAQ,QAAQ,SAAS,SAAS,IAAI,MAAM,aAAa;GAChE,SAAS,SAAS,IAAI,SAAS,OAAO;GACtC,GAAI,OAAO,IAAI,gBAAgB,YAAY,IAAI,cAC3C,EAAE,aAAa,IAAI,YAAY,IAC/B,CAAC;EACP;EACA,SAAS,CAAC,QAAQ,QAAQ,UAAU,qBAAqB,CAAC;EAC1D,cAAc,QAAQ,QAAQ,iBAAiB;EAC/C,sBAAsB,aAAa,aAAa,IAAI,QAAQ,CAAC,EAAE;EAC/D;CACF,CAAC;AACH"}
@@ -0,0 +1,30 @@
1
+ //#region ../core/src/openapi/openapi-protocol.ts
2
+ /** Version of the OpenAPI mode of the registration child protocol. */
3
+ const OPENAPI_PROTOCOL_VERSION = 1;
4
+ /** `type` of the one message the child answers with in OpenAPI mode. */
5
+ const OPENAPI_RESULT_MESSAGE = "openapi-registration:document";
6
+ function isRecord(value) {
7
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8
+ }
9
+ /**
10
+ * Validate the `openapi` request field inside the child. Throws on anything unexpected.
11
+ */
12
+ function parseOpenApiChildOptions(value) {
13
+ if (!isRecord(value) || value.version !== 1 || !(value.title === void 0 || typeof value.title === "string") || !(value.server === void 0 || typeof value.server === "string") || !(value.includePages === void 0 || typeof value.includePages === "boolean")) throw new Error("Route registration child received an invalid OpenAPI request.");
14
+ return value;
15
+ }
16
+ /**
17
+ * Validate the result message in the parent. Throws on anything unexpected.
18
+ */
19
+ function parseOpenApiChildResult(value) {
20
+ if (!isRecord(value) || value.version !== 1 || !isRecord(value.document) || typeof value.document.openapi !== "string" || !isRecord(value.document.paths) || !Array.isArray(value.warnings) || !value.warnings.every((warning) => typeof warning === "string")) throw new Error("Route registration child sent an invalid OpenAPI document payload.");
21
+ return Object.freeze({
22
+ version: 1,
23
+ document: value.document,
24
+ warnings: Object.freeze([...value.warnings])
25
+ });
26
+ }
27
+
28
+ //#endregion
29
+ export { OPENAPI_RESULT_MESSAGE, parseOpenApiChildOptions, parseOpenApiChildResult };
30
+ //# sourceMappingURL=openapi-protocol.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openapi-protocol.mjs","names":[],"sources":["../../../../../../../core/src/openapi/openapi-protocol.ts"],"sourcesContent":["import type { OpenApiDocument } from \"./types\";\n\n/** Version of the OpenAPI mode of the registration child protocol. */\nexport const OPENAPI_PROTOCOL_VERSION = 1;\n\n/** `type` of the one message the child answers with in OpenAPI mode. */\nexport const OPENAPI_RESULT_MESSAGE = \"openapi-registration:document\";\n\n/**\n * The `openapi` field of the registration child request. Its presence switches the child\n * from \"send the route snapshot\" to \"build the OpenAPI document in this process\".\n */\nexport type OpenApiChildRequestOptions = Readonly<{\n version: typeof OPENAPI_PROTOCOL_VERSION;\n /** Overrides `info.title` (default: the app `package.json` name). */\n title?: string;\n /** Overrides `servers[0].url` (default: built from the `http` config). */\n server?: string;\n includePages?: boolean;\n}>;\n\n/** What the child sends back: plain JSON only, never a Seal instance. */\nexport type OpenApiChildResult = Readonly<{\n version: typeof OPENAPI_PROTOCOL_VERSION;\n document: OpenApiDocument;\n warnings: readonly string[];\n}>;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * Validate the `openapi` request field inside the child. Throws on anything unexpected.\n */\nexport function parseOpenApiChildOptions(value: unknown): OpenApiChildRequestOptions {\n if (\n !isRecord(value) ||\n value.version !== OPENAPI_PROTOCOL_VERSION ||\n !(value.title === undefined || typeof value.title === \"string\") ||\n !(value.server === undefined || typeof value.server === \"string\") ||\n !(value.includePages === undefined || typeof value.includePages === \"boolean\")\n ) {\n throw new Error(\"Route registration child received an invalid OpenAPI request.\");\n }\n\n return value as OpenApiChildRequestOptions;\n}\n\n/**\n * Validate the result message in the parent. Throws on anything unexpected.\n */\nexport function parseOpenApiChildResult(value: unknown): OpenApiChildResult {\n if (\n !isRecord(value) ||\n value.version !== OPENAPI_PROTOCOL_VERSION ||\n !isRecord(value.document) ||\n typeof value.document.openapi !== \"string\" ||\n !isRecord(value.document.paths) ||\n !Array.isArray(value.warnings) ||\n !value.warnings.every((warning) => typeof warning === \"string\")\n ) {\n throw new Error(\"Route registration child sent an invalid OpenAPI document payload.\");\n }\n\n return Object.freeze({\n version: OPENAPI_PROTOCOL_VERSION,\n document: value.document as unknown as OpenApiDocument,\n warnings: Object.freeze([...(value.warnings as string[])]),\n });\n}\n"],"mappings":";;AAGA,MAAa,2BAA2B;;AAGxC,MAAa,yBAAyB;AAsBtC,SAAS,SAAS,OAAkD;CAClE,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;;;;AAKA,SAAgB,yBAAyB,OAA4C;CACnF,IACE,CAAC,SAAS,KAAK,KACf,MAAM,iBACN,EAAE,MAAM,UAAU,UAAa,OAAO,MAAM,UAAU,aACtD,EAAE,MAAM,WAAW,UAAa,OAAO,MAAM,WAAW,aACxD,EAAE,MAAM,iBAAiB,UAAa,OAAO,MAAM,iBAAiB,YAEpE,MAAM,IAAI,MAAM,+DAA+D;CAGjF,OAAO;AACT;;;;AAKA,SAAgB,wBAAwB,OAAoC;CAC1E,IACE,CAAC,SAAS,KAAK,KACf,MAAM,iBACN,CAAC,SAAS,MAAM,QAAQ,KACxB,OAAO,MAAM,SAAS,YAAY,YAClC,CAAC,SAAS,MAAM,SAAS,KAAK,KAC9B,CAAC,MAAM,QAAQ,MAAM,QAAQ,KAC7B,CAAC,MAAM,SAAS,OAAO,YAAY,OAAO,YAAY,QAAQ,GAE9D,MAAM,IAAI,MAAM,oEAAoE;CAGtF,OAAO,OAAO,OAAO;EACnB;EACA,UAAU,MAAM;EAChB,UAAU,OAAO,OAAO,CAAC,GAAI,MAAM,QAAqB,CAAC;CAC3D,CAAC;AACH"}
@@ -0,0 +1,63 @@
1
+ //#region ../core/src/openapi/paths.ts
2
+ /**
3
+ * The concrete verbs an `all` route expands into. Mirrors `ALL_METHODS` in the route registry.
4
+ */
5
+ const ALL_METHODS = [
6
+ "get",
7
+ "post",
8
+ "put",
9
+ "patch",
10
+ "delete",
11
+ "options",
12
+ "head"
13
+ ];
14
+ const PATH_PARAM_PATTERN = /:([A-Za-z_][A-Za-z0-9_]*)(\([^)]*\))?/g;
15
+ /**
16
+ * Convert a Warlock path into an OpenAPI path template: `/users/:id` -> `/users/{id}`.
17
+ * A Fastify-style regex suffix (`:id(\d+)`) is dropped.
18
+ */
19
+ function toOpenApiPath(path) {
20
+ return path.replace(PATH_PARAM_PATTERN, (_match, name) => `{${name}}`);
21
+ }
22
+ /**
23
+ * Names of the path parameters of a Warlock path, in order of appearance.
24
+ */
25
+ function extractPathParamNames(path) {
26
+ return [...path.matchAll(PATH_PARAM_PATTERN)].flatMap((match) => match[1] ?? []);
27
+ }
28
+ /**
29
+ * Lowercase operation verbs of a route method; `all` expands to every verb.
30
+ */
31
+ function expandMethods(method) {
32
+ const lowered = String(method).toLowerCase();
33
+ if (lowered === "all") return [...ALL_METHODS];
34
+ return ALL_METHODS.includes(lowered) ? [lowered] : [];
35
+ }
36
+ /**
37
+ * `operationId` for an operation. A named route keeps its name (with `.<verb>` appended when
38
+ * an `all` route expands into several operations); an unnamed route gets a `method_path_slug`
39
+ * id such as `get_users_id`.
40
+ */
41
+ function buildOperationId(name, method, path, expanded) {
42
+ if (name) return expanded ? `${name}.${method}` : name;
43
+ const slug = path.replace(PATH_PARAM_PATTERN, "$1").replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
44
+ return slug ? `${method}_${slug}` : method;
45
+ }
46
+ /**
47
+ * Tag of a route: its first static path segment (`/users/:id` -> `users`).
48
+ */
49
+ function firstSegmentTag(path) {
50
+ const segment = path.split("/").find((part) => part.length > 0);
51
+ if (!segment || segment.startsWith(":") || segment === "*") return;
52
+ return segment;
53
+ }
54
+ /**
55
+ * Verbs whose requests carry no body, so unscoped validation maps to query parameters.
56
+ */
57
+ function isBodylessMethod(method) {
58
+ return method === "get" || method === "head" || method === "delete" || method === "options";
59
+ }
60
+
61
+ //#endregion
62
+ export { buildOperationId, expandMethods, extractPathParamNames, firstSegmentTag, isBodylessMethod, toOpenApiPath };
63
+ //# sourceMappingURL=paths.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.mjs","names":[],"sources":["../../../../../../../core/src/openapi/paths.ts"],"sourcesContent":["import type { RequestMethod } from \"../router/types\";\n\n/**\n * The concrete verbs an `all` route expands into. Mirrors `ALL_METHODS` in the route registry.\n */\nconst ALL_METHODS = [\"get\", \"post\", \"put\", \"patch\", \"delete\", \"options\", \"head\"] as const;\n\nexport type OpenApiMethod = (typeof ALL_METHODS)[number];\n\nconst PATH_PARAM_PATTERN = /:([A-Za-z_][A-Za-z0-9_]*)(\\([^)]*\\))?/g;\n\n/**\n * Convert a Warlock path into an OpenAPI path template: `/users/:id` -> `/users/{id}`.\n * A Fastify-style regex suffix (`:id(\\d+)`) is dropped.\n */\nexport function toOpenApiPath(path: string): string {\n return path.replace(PATH_PARAM_PATTERN, (_match, name: string) => `{${name}}`);\n}\n\n/**\n * Names of the path parameters of a Warlock path, in order of appearance.\n */\nexport function extractPathParamNames(path: string): string[] {\n return [...path.matchAll(PATH_PARAM_PATTERN)].flatMap((match) => match[1] ?? []);\n}\n\n/**\n * Lowercase operation verbs of a route method; `all` expands to every verb.\n */\nexport function expandMethods(method: RequestMethod | string): OpenApiMethod[] {\n const lowered = String(method).toLowerCase();\n\n if (lowered === \"all\") {\n return [...ALL_METHODS];\n }\n\n return (ALL_METHODS as readonly string[]).includes(lowered) ? [lowered as OpenApiMethod] : [];\n}\n\n/**\n * `operationId` for an operation. A named route keeps its name (with `.<verb>` appended when\n * an `all` route expands into several operations); an unnamed route gets a `method_path_slug`\n * id such as `get_users_id`.\n */\nexport function buildOperationId(\n name: string | undefined,\n method: OpenApiMethod,\n path: string,\n expanded: boolean,\n): string {\n if (name) {\n return expanded ? `${name}.${method}` : name;\n }\n\n const slug = path\n .replace(PATH_PARAM_PATTERN, \"$1\")\n .replace(/[^A-Za-z0-9]+/g, \"_\")\n .replace(/^_+|_+$/g, \"\");\n\n return slug ? `${method}_${slug}` : method;\n}\n\n/**\n * Tag of a route: its first static path segment (`/users/:id` -> `users`).\n */\nexport function firstSegmentTag(path: string): string | undefined {\n const segment = path.split(\"/\").find((part) => part.length > 0);\n\n if (!segment || segment.startsWith(\":\") || segment === \"*\") {\n return undefined;\n }\n\n return segment;\n}\n\n/**\n * Verbs whose requests carry no body, so unscoped validation maps to query parameters.\n */\nexport function isBodylessMethod(method: OpenApiMethod): boolean {\n return method === \"get\" || method === \"head\" || method === \"delete\" || method === \"options\";\n}\n"],"mappings":";;;;AAKA,MAAM,cAAc;CAAC;CAAO;CAAQ;CAAO;CAAS;CAAU;CAAW;AAAM;AAI/E,MAAM,qBAAqB;;;;;AAM3B,SAAgB,cAAc,MAAsB;CAClD,OAAO,KAAK,QAAQ,qBAAqB,QAAQ,SAAiB,IAAI,KAAK,EAAE;AAC/E;;;;AAKA,SAAgB,sBAAsB,MAAwB;CAC5D,OAAO,CAAC,GAAG,KAAK,SAAS,kBAAkB,CAAC,CAAC,CAAC,SAAS,UAAU,MAAM,MAAM,CAAC,CAAC;AACjF;;;;AAKA,SAAgB,cAAc,QAAiD;CAC7E,MAAM,UAAU,OAAO,MAAM,CAAC,CAAC,YAAY;CAE3C,IAAI,YAAY,OACd,OAAO,CAAC,GAAG,WAAW;CAGxB,OAAQ,YAAkC,SAAS,OAAO,IAAI,CAAC,OAAwB,IAAI,CAAC;AAC9F;;;;;;AAOA,SAAgB,iBACd,MACA,QACA,MACA,UACQ;CACR,IAAI,MACF,OAAO,WAAW,GAAG,KAAK,GAAG,WAAW;CAG1C,MAAM,OAAO,KACV,QAAQ,oBAAoB,IAAI,CAAC,CACjC,QAAQ,kBAAkB,GAAG,CAAC,CAC9B,QAAQ,YAAY,EAAE;CAEzB,OAAO,OAAO,GAAG,OAAO,GAAG,SAAS;AACtC;;;;AAKA,SAAgB,gBAAgB,MAAkC;CAChE,MAAM,UAAU,KAAK,MAAM,GAAG,CAAC,CAAC,MAAM,SAAS,KAAK,SAAS,CAAC;CAE9D,IAAI,CAAC,WAAW,QAAQ,WAAW,GAAG,KAAK,YAAY,KACrD;CAGF,OAAO;AACT;;;;AAKA,SAAgB,iBAAiB,QAAgC;CAC/D,OAAO,WAAW,SAAS,WAAW,UAAU,WAAW,YAAY,WAAW;AACpF"}
@@ -0,0 +1,138 @@
1
+ import { extractPathParamNames, isBodylessMethod } from "./paths.mjs";
2
+
3
+ //#region ../core/src/openapi/request-schemas.ts
4
+ const VALIDATING_LOCATIONS = [
5
+ "body",
6
+ "query",
7
+ "params",
8
+ "headers"
9
+ ];
10
+ /**
11
+ * Convert a Seal validator to a JSON Schema object (`draft-2020-12`, the OpenAPI 3.1 dialect)
12
+ * and split it into properties and required keys. Never throws: a validator that cannot be
13
+ * converted is reported through `warn` and yields `undefined`.
14
+ */
15
+ function readObjectSchema(validator, what, warn) {
16
+ const convert = validator?.toJsonSchema;
17
+ if (typeof convert !== "function") return;
18
+ let schema;
19
+ try {
20
+ schema = convert.call(validator, "draft-2020-12");
21
+ } catch (error) {
22
+ warn(`${what} could not be converted to JSON Schema and is not documented: ${error instanceof Error ? error.message : String(error)}`);
23
+ return;
24
+ }
25
+ const properties = schema?.properties;
26
+ if (typeof schema !== "object" || schema === null || !isRecord(properties)) {
27
+ warn(`${what} is not an object schema and is not documented.`);
28
+ return;
29
+ }
30
+ const required = schema.required;
31
+ return {
32
+ schema,
33
+ properties,
34
+ required: new Set(Array.isArray(required) ? required.filter((key) => typeof key === "string") : [])
35
+ };
36
+ }
37
+ function isRecord(value) {
38
+ return typeof value === "object" && value !== null && !Array.isArray(value);
39
+ }
40
+ function describeParameter(name, location, schema, required) {
41
+ const parameter = {
42
+ name,
43
+ in: location,
44
+ required,
45
+ schema
46
+ };
47
+ if (typeof schema.description === "string") parameter.description = schema.description;
48
+ if (location === "query" && schema.type === "object") {
49
+ parameter.style = "deepObject";
50
+ parameter.explode = true;
51
+ }
52
+ return parameter;
53
+ }
54
+ /**
55
+ * Where an unscoped or scoped `validating` puts the non-path properties.
56
+ *
57
+ * `validateAll` merges body and query (unset) or exactly the listed sources. The OpenAPI
58
+ * placement follows: unset -> by method, a single source -> that source, `body` + `query`
59
+ * -> body for verbs that carry one, query for the rest (and a warning, it is ambiguous).
60
+ */
61
+ function resolvePrimaryLocation(method, validating, warn) {
62
+ if (validating.length === 0) return isBodylessMethod(method) ? "query" : "body";
63
+ const hasBody = validating.includes("body");
64
+ const hasQuery = validating.includes("query");
65
+ if (hasBody && hasQuery) {
66
+ warn(`validating lists both "body" and "query"; the properties are documented as ${isBodylessMethod(method) ? "query parameters" : "the request body"}.`);
67
+ return isBodylessMethod(method) ? "query" : "body";
68
+ }
69
+ if (hasBody) return "body";
70
+ if (hasQuery) return "query";
71
+ if (validating.includes("headers")) return "header";
72
+ }
73
+ /**
74
+ * Map a handler's `validation` to OpenAPI parameters and a request body.
75
+ *
76
+ * - `validation.params` -> one required path parameter per property.
77
+ * - Path segments without a declared schema -> required string path parameters.
78
+ * - `validation.schema` -> request body (verbs that carry one) or query parameters, placed by
79
+ * `validating` exactly as `validateAll` reads the request; a property named like a path
80
+ * segment is a path parameter whenever `params` is validated (unset counts).
81
+ */
82
+ function buildRequestParts(input) {
83
+ const { method, path, validation, warn } = input;
84
+ const validating = (validation?.validating ?? []).filter((location) => VALIDATING_LOCATIONS.includes(location));
85
+ const pathNames = extractPathParamNames(path);
86
+ const paramsSchema = validation?.params ? readObjectSchema(validation.params, "validation.params", warn) : void 0;
87
+ const mainSchema = validation?.schema ? readObjectSchema(validation.schema, "validation.schema", warn) : void 0;
88
+ const pathSchemas = /* @__PURE__ */ new Map();
89
+ const primary = resolvePrimaryLocation(method, validating, warn);
90
+ const takesPathNames = validating.length === 0 || validating.includes("params");
91
+ const located = {
92
+ body: [],
93
+ query: [],
94
+ header: []
95
+ };
96
+ if (paramsSchema) {
97
+ for (const name of pathNames) if (paramsSchema.properties[name]) pathSchemas.set(name, paramsSchema.properties[name]);
98
+ }
99
+ if (mainSchema) for (const [key, propertySchema] of Object.entries(mainSchema.properties)) {
100
+ if (takesPathNames && pathNames.includes(key)) {
101
+ if (!pathSchemas.has(key)) pathSchemas.set(key, propertySchema);
102
+ continue;
103
+ }
104
+ if (!primary) {
105
+ warn(`validation.schema property "${key}" is not a path parameter and is not documented.`);
106
+ continue;
107
+ }
108
+ located[primary].push({
109
+ key,
110
+ schema: propertySchema
111
+ });
112
+ }
113
+ const requiredKeys = mainSchema?.required ?? /* @__PURE__ */ new Set();
114
+ const parameters = pathNames.map((name) => describeParameter(name, "path", pathSchemas.get(name) ?? { type: "string" }, true));
115
+ for (const { key, schema } of located.query) parameters.push(describeParameter(key, "query", schema, requiredKeys.has(key)));
116
+ for (const { key, schema } of located.header) parameters.push(describeParameter(key.toLowerCase(), "header", schema, requiredKeys.has(key)));
117
+ if (located.body.length === 0 || !mainSchema) return { parameters };
118
+ const properties = {};
119
+ for (const { key, schema } of located.body) properties[key] = schema;
120
+ const required = located.body.filter(({ key }) => mainSchema.required.has(key)).map(({ key }) => key);
121
+ const bodySchema = {
122
+ ...mainSchema.schema,
123
+ properties
124
+ };
125
+ if (required.length > 0) bodySchema.required = required;
126
+ else delete bodySchema.required;
127
+ return {
128
+ parameters,
129
+ requestBody: {
130
+ required: required.length > 0,
131
+ content: { "application/json": { schema: bodySchema } }
132
+ }
133
+ };
134
+ }
135
+
136
+ //#endregion
137
+ export { buildRequestParts };
138
+ //# sourceMappingURL=request-schemas.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-schemas.mjs","names":[],"sources":["../../../../../../../core/src/openapi/request-schemas.ts"],"sourcesContent":["import type { RequestHandlerValidation } from \"../router/types\";\nimport { extractPathParamNames, isBodylessMethod, type OpenApiMethod } from \"./paths\";\nimport type {\n OpenApiParameter,\n OpenApiRequestBody,\n OpenApiSchema,\n WarningSink,\n} from \"./types\";\n\ntype ValidatingLocation = \"body\" | \"query\" | \"params\" | \"headers\";\n\ntype ObjectSchemaParts = {\n schema: OpenApiSchema;\n properties: Record<string, OpenApiSchema>;\n required: Set<string>;\n};\n\n/** A `validation.schema` property placed in a request location, with its JSON Schema. */\ntype LocatedProperty = {\n key: string;\n schema: OpenApiSchema;\n};\n\nexport type RequestParts = {\n parameters: OpenApiParameter[];\n requestBody?: OpenApiRequestBody;\n};\n\nexport type BuildRequestPartsInput = {\n method: OpenApiMethod;\n /** Warlock path, with `:param` segments. */\n path: string;\n validation: RequestHandlerValidation | undefined;\n warn: WarningSink;\n};\n\nconst VALIDATING_LOCATIONS: readonly string[] = [\"body\", \"query\", \"params\", \"headers\"];\n\n/**\n * Convert a Seal validator to a JSON Schema object (`draft-2020-12`, the OpenAPI 3.1 dialect)\n * and split it into properties and required keys. Never throws: a validator that cannot be\n * converted is reported through `warn` and yields `undefined`.\n */\nfunction readObjectSchema(\n validator: unknown,\n what: string,\n warn: WarningSink,\n): ObjectSchemaParts | undefined {\n const convert = (validator as { toJsonSchema?: unknown } | undefined)?.toJsonSchema;\n\n if (typeof convert !== \"function\") {\n return undefined;\n }\n\n let schema: unknown;\n\n try {\n schema = convert.call(validator, \"draft-2020-12\");\n } catch (error) {\n warn(\n `${what} could not be converted to JSON Schema and is not documented: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n\n return undefined;\n }\n\n const properties = (schema as { properties?: unknown } | undefined)?.properties;\n\n if (typeof schema !== \"object\" || schema === null || !isRecord(properties)) {\n warn(`${what} is not an object schema and is not documented.`);\n\n return undefined;\n }\n\n const required = (schema as { required?: unknown }).required;\n\n return {\n schema: schema as OpenApiSchema,\n properties: properties as Record<string, OpenApiSchema>,\n required: new Set(Array.isArray(required) ? required.filter((key) => typeof key === \"string\") : []),\n };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction describeParameter(\n name: string,\n location: OpenApiParameter[\"in\"],\n schema: OpenApiSchema,\n required: boolean,\n): OpenApiParameter {\n const parameter: OpenApiParameter = { name, in: location, required, schema };\n\n if (typeof schema.description === \"string\") {\n parameter.description = schema.description;\n }\n\n if (location === \"query\" && schema.type === \"object\") {\n parameter.style = \"deepObject\";\n parameter.explode = true;\n }\n\n return parameter;\n}\n\n/**\n * Where an unscoped or scoped `validating` puts the non-path properties.\n *\n * `validateAll` merges body and query (unset) or exactly the listed sources. The OpenAPI\n * placement follows: unset -> by method, a single source -> that source, `body` + `query`\n * -> body for verbs that carry one, query for the rest (and a warning, it is ambiguous).\n */\nfunction resolvePrimaryLocation(\n method: OpenApiMethod,\n validating: ValidatingLocation[],\n warn: WarningSink,\n): \"body\" | \"query\" | \"header\" | undefined {\n if (validating.length === 0) {\n return isBodylessMethod(method) ? \"query\" : \"body\";\n }\n\n const hasBody = validating.includes(\"body\");\n const hasQuery = validating.includes(\"query\");\n\n if (hasBody && hasQuery) {\n warn(\n `validating lists both \"body\" and \"query\"; the properties are documented as ${\n isBodylessMethod(method) ? \"query parameters\" : \"the request body\"\n }.`,\n );\n\n return isBodylessMethod(method) ? \"query\" : \"body\";\n }\n\n if (hasBody) {\n return \"body\";\n }\n\n if (hasQuery) {\n return \"query\";\n }\n\n if (validating.includes(\"headers\")) {\n return \"header\";\n }\n\n return undefined;\n}\n\n/**\n * Map a handler's `validation` to OpenAPI parameters and a request body.\n *\n * - `validation.params` -> one required path parameter per property.\n * - Path segments without a declared schema -> required string path parameters.\n * - `validation.schema` -> request body (verbs that carry one) or query parameters, placed by\n * `validating` exactly as `validateAll` reads the request; a property named like a path\n * segment is a path parameter whenever `params` is validated (unset counts).\n */\nexport function buildRequestParts(input: BuildRequestPartsInput): RequestParts {\n const { method, path, validation, warn } = input;\n const validating = (validation?.validating ?? []).filter((location): location is ValidatingLocation =>\n VALIDATING_LOCATIONS.includes(location),\n );\n const pathNames = extractPathParamNames(path);\n\n const paramsSchema = validation?.params\n ? readObjectSchema(validation.params, \"validation.params\", warn)\n : undefined;\n const mainSchema = validation?.schema\n ? readObjectSchema(validation.schema, \"validation.schema\", warn)\n : undefined;\n\n const pathSchemas = new Map<string, OpenApiSchema>();\n const primary = resolvePrimaryLocation(method, validating, warn);\n const takesPathNames = validating.length === 0 || validating.includes(\"params\");\n\n const located: Record<\"body\" | \"query\" | \"header\", LocatedProperty[]> = {\n body: [],\n query: [],\n header: [],\n };\n\n if (paramsSchema) {\n for (const name of pathNames) {\n if (paramsSchema.properties[name]) {\n pathSchemas.set(name, paramsSchema.properties[name]);\n }\n }\n }\n\n if (mainSchema) {\n for (const [key, propertySchema] of Object.entries(mainSchema.properties)) {\n if (takesPathNames && pathNames.includes(key)) {\n if (!pathSchemas.has(key)) {\n pathSchemas.set(key, propertySchema);\n }\n\n continue;\n }\n\n if (!primary) {\n warn(`validation.schema property \"${key}\" is not a path parameter and is not documented.`);\n\n continue;\n }\n\n located[primary].push({ key, schema: propertySchema });\n }\n }\n\n const requiredKeys = mainSchema?.required ?? new Set<string>();\n\n const parameters: OpenApiParameter[] = pathNames.map((name) =>\n describeParameter(name, \"path\", pathSchemas.get(name) ?? { type: \"string\" }, true),\n );\n\n for (const { key, schema } of located.query) {\n parameters.push(describeParameter(key, \"query\", schema, requiredKeys.has(key)));\n }\n\n for (const { key, schema } of located.header) {\n parameters.push(describeParameter(key.toLowerCase(), \"header\", schema, requiredKeys.has(key)));\n }\n\n if (located.body.length === 0 || !mainSchema) {\n return { parameters };\n }\n\n const properties: Record<string, OpenApiSchema> = {};\n\n for (const { key, schema } of located.body) {\n properties[key] = schema;\n }\n\n const required = located.body.filter(({ key }) => mainSchema.required.has(key)).map(({ key }) => key);\n const bodySchema: OpenApiSchema = { ...mainSchema.schema, properties };\n\n if (required.length > 0) {\n bodySchema.required = required;\n } else {\n delete bodySchema.required;\n }\n\n return {\n parameters,\n requestBody: {\n required: required.length > 0,\n content: { \"application/json\": { schema: bodySchema } },\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,85 @@
1
+ //#region ../core/src/openapi/response-schemas.ts
2
+ const STATUS_TEXT = {
3
+ "200": "Successful response",
4
+ "201": "Created",
5
+ "202": "Accepted",
6
+ "204": "No content",
7
+ "301": "Moved permanently",
8
+ "302": "Found",
9
+ "304": "Not modified",
10
+ "400": "Bad request",
11
+ "401": "Unauthorized",
12
+ "403": "Forbidden",
13
+ "404": "Not found",
14
+ "405": "Method not allowed",
15
+ "409": "Conflict",
16
+ "410": "Gone",
17
+ "422": "Validation failed",
18
+ "429": "Too many requests",
19
+ "500": "Internal server error",
20
+ "503": "Service unavailable"
21
+ };
22
+ function isRecord(value) {
23
+ return typeof value === "object" && value !== null && !Array.isArray(value);
24
+ }
25
+ /**
26
+ * Human description of a status code for the required `description` of a response.
27
+ */
28
+ function describeStatus(status) {
29
+ return STATUS_TEXT[status] ?? `Status ${status}`;
30
+ }
31
+ /**
32
+ * JSON Schema of one response body value: a cast string, a resource class, `[Resource]`
33
+ * (array of it) or a nested plain object of those. Anything else (a resolver function, a
34
+ * builder) is `{}` plus a warning.
35
+ */
36
+ function bodyValueSchema(value, path, registry, warn) {
37
+ if (typeof value === "string") return registry.cast(value, path, warn);
38
+ if (typeof value === "function") {
39
+ if (registry.isResourceClass(value)) return registry.resourceRef(value);
40
+ warn(`${path}: a function that is not a resource class has no static shape and is documented as {}.`);
41
+ return {};
42
+ }
43
+ if (Array.isArray(value)) return {
44
+ type: "array",
45
+ items: bodyValueSchema(value[0], `${path}[]`, registry, warn)
46
+ };
47
+ if (isRecord(value)) return objectSchema(value, path, registry, warn);
48
+ warn(`${path}: unsupported response value is documented as {}.`);
49
+ return {};
50
+ }
51
+ function objectSchema(body, path, registry, warn) {
52
+ const properties = {};
53
+ for (const [key, member] of Object.entries(body)) properties[key] = bodyValueSchema(member, `${path}.${key}`, registry, warn);
54
+ return {
55
+ type: "object",
56
+ properties,
57
+ required: Object.keys(properties)
58
+ };
59
+ }
60
+ /**
61
+ * Map a handler's `responseSchema` to OpenAPI responses, one per declared status code.
62
+ *
63
+ * `routeLabel` prefixes warnings (for example `POST /login`). A status without a usable
64
+ * `body` becomes a description-only response.
65
+ */
66
+ function buildDeclaredResponses(responseSchema, registry, warn) {
67
+ const responses = {};
68
+ if (!isRecord(responseSchema)) return responses;
69
+ for (const [status, entry] of Object.entries(responseSchema)) {
70
+ const body = isRecord(entry) ? entry.body : void 0;
71
+ if (!isRecord(body)) {
72
+ responses[status] = { description: describeStatus(status) };
73
+ continue;
74
+ }
75
+ responses[status] = {
76
+ description: describeStatus(status),
77
+ content: { "application/json": { schema: objectSchema(body, `response ${status}`, registry, warn) } }
78
+ };
79
+ }
80
+ return responses;
81
+ }
82
+
83
+ //#endregion
84
+ export { buildDeclaredResponses, describeStatus };
85
+ //# sourceMappingURL=response-schemas.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"response-schemas.mjs","names":[],"sources":["../../../../../../../core/src/openapi/response-schemas.ts"],"sourcesContent":["import type { SchemaRegistry } from \"./schema-registry\";\nimport type { OpenApiResponse, OpenApiSchema, WarningSink } from \"./types\";\n\nconst STATUS_TEXT: Record<string, string> = {\n \"200\": \"Successful response\",\n \"201\": \"Created\",\n \"202\": \"Accepted\",\n \"204\": \"No content\",\n \"301\": \"Moved permanently\",\n \"302\": \"Found\",\n \"304\": \"Not modified\",\n \"400\": \"Bad request\",\n \"401\": \"Unauthorized\",\n \"403\": \"Forbidden\",\n \"404\": \"Not found\",\n \"405\": \"Method not allowed\",\n \"409\": \"Conflict\",\n \"410\": \"Gone\",\n \"422\": \"Validation failed\",\n \"429\": \"Too many requests\",\n \"500\": \"Internal server error\",\n \"503\": \"Service unavailable\",\n};\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * Human description of a status code for the required `description` of a response.\n */\nexport function describeStatus(status: string): string {\n return STATUS_TEXT[status] ?? `Status ${status}`;\n}\n\n/**\n * JSON Schema of one response body value: a cast string, a resource class, `[Resource]`\n * (array of it) or a nested plain object of those. Anything else (a resolver function, a\n * builder) is `{}` plus a warning.\n */\nfunction bodyValueSchema(value: unknown, path: string, registry: SchemaRegistry, warn: WarningSink): OpenApiSchema {\n if (typeof value === \"string\") {\n return registry.cast(value, path, warn);\n }\n\n if (typeof value === \"function\") {\n if (registry.isResourceClass(value)) {\n return registry.resourceRef(value);\n }\n\n warn(`${path}: a function that is not a resource class has no static shape and is documented as {}.`);\n\n return {};\n }\n\n if (Array.isArray(value)) {\n return { type: \"array\", items: bodyValueSchema(value[0], `${path}[]`, registry, warn) };\n }\n\n if (isRecord(value)) {\n return objectSchema(value, path, registry, warn);\n }\n\n warn(`${path}: unsupported response value is documented as {}.`);\n\n return {};\n}\n\nfunction objectSchema(\n body: Record<string, unknown>,\n path: string,\n registry: SchemaRegistry,\n warn: WarningSink,\n): OpenApiSchema {\n const properties: Record<string, OpenApiSchema> = {};\n\n for (const [key, member] of Object.entries(body)) {\n properties[key] = bodyValueSchema(member, `${path}.${key}`, registry, warn);\n }\n\n return { type: \"object\", properties, required: Object.keys(properties) };\n}\n\n/**\n * Map a handler's `responseSchema` to OpenAPI responses, one per declared status code.\n *\n * `routeLabel` prefixes warnings (for example `POST /login`). A status without a usable\n * `body` becomes a description-only response.\n */\nexport function buildDeclaredResponses(\n responseSchema: unknown,\n registry: SchemaRegistry,\n warn: WarningSink,\n): Record<string, OpenApiResponse> {\n const responses: Record<string, OpenApiResponse> = {};\n\n if (!isRecord(responseSchema)) {\n return responses;\n }\n\n for (const [status, entry] of Object.entries(responseSchema)) {\n const body = isRecord(entry) ? entry.body : undefined;\n\n if (!isRecord(body)) {\n responses[status] = { description: describeStatus(status) };\n\n continue;\n }\n\n responses[status] = {\n description: describeStatus(status),\n content: {\n \"application/json\": { schema: objectSchema(body, `response ${status}`, registry, warn) },\n },\n };\n }\n\n return responses;\n}\n"],"mappings":";AAGA,MAAM,cAAsC;CAC1C,OAAO;CACP,OAAO;CACP,OAAO;CACP,OAAO;CACP,OAAO;CACP,OAAO;CACP,OAAO;CACP,OAAO;CACP,OAAO;CACP,OAAO;CACP,OAAO;CACP,OAAO;CACP,OAAO;CACP,OAAO;CACP,OAAO;CACP,OAAO;CACP,OAAO;CACP,OAAO;AACT;AAEA,SAAS,SAAS,OAAkD;CAClE,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;;;;AAKA,SAAgB,eAAe,QAAwB;CACrD,OAAO,YAAY,WAAW,UAAU;AAC1C;;;;;;AAOA,SAAS,gBAAgB,OAAgB,MAAc,UAA0B,MAAkC;CACjH,IAAI,OAAO,UAAU,UACnB,OAAO,SAAS,KAAK,OAAO,MAAM,IAAI;CAGxC,IAAI,OAAO,UAAU,YAAY;EAC/B,IAAI,SAAS,gBAAgB,KAAK,GAChC,OAAO,SAAS,YAAY,KAAK;EAGnC,KAAK,GAAG,KAAK,uFAAuF;EAEpG,OAAO,CAAC;CACV;CAEA,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO;EAAE,MAAM;EAAS,OAAO,gBAAgB,MAAM,IAAI,GAAG,KAAK,KAAK,UAAU,IAAI;CAAE;CAGxF,IAAI,SAAS,KAAK,GAChB,OAAO,aAAa,OAAO,MAAM,UAAU,IAAI;CAGjD,KAAK,GAAG,KAAK,kDAAkD;CAE/D,OAAO,CAAC;AACV;AAEA,SAAS,aACP,MACA,MACA,UACA,MACe;CACf,MAAM,aAA4C,CAAC;CAEnD,KAAK,MAAM,CAAC,KAAK,WAAW,OAAO,QAAQ,IAAI,GAC7C,WAAW,OAAO,gBAAgB,QAAQ,GAAG,KAAK,GAAG,OAAO,UAAU,IAAI;CAG5E,OAAO;EAAE,MAAM;EAAU;EAAY,UAAU,OAAO,KAAK,UAAU;CAAE;AACzE;;;;;;;AAQA,SAAgB,uBACd,gBACA,UACA,MACiC;CACjC,MAAM,YAA6C,CAAC;CAEpD,IAAI,CAAC,SAAS,cAAc,GAC1B,OAAO;CAGT,KAAK,MAAM,CAAC,QAAQ,UAAU,OAAO,QAAQ,cAAc,GAAG;EAC5D,MAAM,OAAO,SAAS,KAAK,IAAI,MAAM,OAAO;EAE5C,IAAI,CAAC,SAAS,IAAI,GAAG;GACnB,UAAU,UAAU,EAAE,aAAa,eAAe,MAAM,EAAE;GAE1D;EACF;EAEA,UAAU,UAAU;GAClB,aAAa,eAAe,MAAM;GAClC,SAAS,EACP,oBAAoB,EAAE,QAAQ,aAAa,MAAM,YAAY,UAAU,UAAU,IAAI,EAAE,EACzF;EACF;CACF;CAEA,OAAO;AACT"}