@warlock.js/core 5.25.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.
- package/CHANGELOG.md +53 -0
- package/esm/cli/commands/doctor/checks/index.mjs +2 -0
- package/esm/cli/commands/doctor/checks/index.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/locales-ts-deprecated.check.mjs +170 -0
- package/esm/cli/commands/doctor/checks/locales-ts-deprecated.check.mjs.map +1 -0
- package/esm/cli/commands/doctor/doctor-command.action.mjs +7 -2
- package/esm/cli/commands/doctor/doctor-command.action.mjs.map +1 -1
- package/esm/cli/commands/doctor.command.mjs +5 -0
- package/esm/cli/commands/doctor.command.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/module.generator.mjs +1 -12
- package/esm/cli/commands/generate/generators/module.generator.mjs.map +1 -1
- package/esm/cli/commands/openapi-generator.action.mjs +43 -0
- package/esm/cli/commands/openapi-generator.action.mjs.map +1 -0
- package/esm/cli/commands/openapi-generator.command.mjs +44 -0
- package/esm/cli/commands/openapi-generator.command.mjs.map +1 -0
- package/esm/cli/commands/typings-generator.command.mjs +4 -1
- package/esm/cli/commands/typings-generator.command.mjs.map +1 -1
- package/esm/cli/framework-cli-commands.mjs +2 -0
- package/esm/cli/framework-cli-commands.mjs.map +1 -1
- package/esm/database/migrate-action.mjs +2 -2
- package/esm/database/migrate-action.mjs.map +1 -1
- package/esm/dev-server/build-development-openapi-document.mjs +36 -0
- package/esm/dev-server/build-development-openapi-document.mjs.map +1 -0
- package/esm/dev-server/collect-development-api-routes.mjs +27 -0
- package/esm/dev-server/collect-development-api-routes.mjs.map +1 -0
- package/esm/dev-server/dev-logger.mjs +9 -1
- package/esm/dev-server/dev-logger.mjs.map +1 -1
- package/esm/dev-server/development-server.mjs +6 -3
- package/esm/dev-server/development-server.mjs.map +1 -1
- package/esm/dev-server/files-orchestrator.mjs +17 -1
- package/esm/dev-server/files-orchestrator.mjs.map +1 -1
- package/esm/dev-server/legacy-locales-warning.mjs +16 -0
- package/esm/dev-server/legacy-locales-warning.mjs.map +1 -0
- package/esm/dev-server/model-resource-typings.mjs +246 -0
- package/esm/dev-server/model-resource-typings.mjs.map +1 -0
- package/esm/dev-server/module-loader.mjs +16 -0
- package/esm/dev-server/module-loader.mjs.map +1 -1
- package/esm/dev-server/special-file-patterns.mjs +2 -2
- package/esm/dev-server/special-file-patterns.mjs.map +1 -1
- package/esm/dev-server/translation-keys-sources.mjs +7 -3
- package/esm/dev-server/translation-keys-sources.mjs.map +1 -1
- package/esm/dev-server/type-generator.mjs +73 -6
- package/esm/dev-server/type-generator.mjs.map +1 -1
- package/esm/generations/features/access.feature.mjs +38 -18
- package/esm/generations/features/access.feature.mjs.map +1 -1
- package/esm/generations/features/bull-board.feature.mjs +2 -2
- package/esm/generations/features/bull-board.feature.mjs.map +1 -1
- package/esm/http/build-rate-limit-options.d.mts +8 -2
- package/esm/http/build-rate-limit-options.d.mts.map +1 -1
- package/esm/http/build-rate-limit-options.mjs +1 -0
- package/esm/http/build-rate-limit-options.mjs.map +1 -1
- package/esm/http/errors/errors.d.mts +1 -17
- package/esm/http/errors/errors.d.mts.map +1 -1
- package/esm/http/errors/errors.mjs +1 -20
- package/esm/http/errors/errors.mjs.map +1 -1
- package/esm/http/index.d.mts +4 -3
- package/esm/http/index.mjs +1 -1
- package/esm/http/middleware/inject-request-context.d.mts +1 -1
- package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
- package/esm/http/middleware/inject-request-context.mjs.map +1 -1
- package/esm/http/middleware/rate-limit.middleware.d.mts.map +1 -1
- package/esm/http/middleware/rate-limit.middleware.mjs +5 -1
- package/esm/http/middleware/rate-limit.middleware.mjs.map +1 -1
- package/esm/http/request.d.mts +9 -26
- package/esm/http/request.d.mts.map +1 -1
- package/esm/http/request.mjs +1 -22
- package/esm/http/request.mjs.map +1 -1
- package/esm/http/response.d.mts.map +1 -1
- package/esm/http/response.mjs +4 -2
- package/esm/http/response.mjs.map +1 -1
- package/esm/http/serialized.d.mts +114 -0
- package/esm/http/serialized.d.mts.map +1 -0
- package/esm/index.d.mts +12 -5
- package/esm/index.mjs +8 -2
- package/esm/localization/index.mjs +4 -0
- package/esm/localization/parse-locale-dictionary.d.mts +23 -0
- package/esm/localization/parse-locale-dictionary.d.mts.map +1 -0
- package/esm/localization/parse-locale-dictionary.mjs +215 -0
- package/esm/localization/parse-locale-dictionary.mjs.map +1 -0
- package/esm/localization/register-locale-dictionary.d.mts +12 -0
- package/esm/localization/register-locale-dictionary.d.mts.map +1 -0
- package/esm/localization/register-locale-dictionary.mjs +58 -0
- package/esm/localization/register-locale-dictionary.mjs.map +1 -0
- package/esm/openapi/build-openapi-document.d.mts +17 -0
- package/esm/openapi/build-openapi-document.d.mts.map +1 -0
- package/esm/openapi/build-openapi-document.mjs +124 -0
- package/esm/openapi/build-openapi-document.mjs.map +1 -0
- package/esm/openapi/cast-schema.mjs +89 -0
- package/esm/openapi/cast-schema.mjs.map +1 -0
- package/esm/openapi/collect-openapi-document.mjs +40 -0
- package/esm/openapi/collect-openapi-document.mjs.map +1 -0
- package/esm/openapi/development-openapi-provider.d.mts +17 -0
- package/esm/openapi/development-openapi-provider.d.mts.map +1 -0
- package/esm/openapi/development-openapi-provider.mjs +21 -0
- package/esm/openapi/development-openapi-provider.mjs.map +1 -0
- package/esm/openapi/error-responses.mjs +67 -0
- package/esm/openapi/error-responses.mjs.map +1 -0
- package/esm/openapi/index.d.mts +3 -0
- package/esm/openapi/index.mjs +4 -0
- package/esm/openapi/openapi-child-output.mjs +57 -0
- package/esm/openapi/openapi-child-output.mjs.map +1 -0
- package/esm/openapi/openapi-protocol.mjs +30 -0
- package/esm/openapi/openapi-protocol.mjs.map +1 -0
- package/esm/openapi/paths.mjs +63 -0
- package/esm/openapi/paths.mjs.map +1 -0
- package/esm/openapi/request-schemas.mjs +138 -0
- package/esm/openapi/request-schemas.mjs.map +1 -0
- package/esm/openapi/response-schemas.mjs +85 -0
- package/esm/openapi/response-schemas.mjs.map +1 -0
- package/esm/openapi/schema-registry.mjs +143 -0
- package/esm/openapi/schema-registry.mjs.map +1 -0
- package/esm/openapi/security.mjs +98 -0
- package/esm/openapi/security.mjs.map +1 -0
- package/esm/openapi/types.d.mts +119 -0
- package/esm/openapi/types.d.mts.map +1 -0
- package/esm/production/production-builder.mjs +11 -2
- package/esm/production/production-builder.mjs.map +1 -1
- package/esm/production/registration-child-runner.mjs +139 -0
- package/esm/production/registration-child-runner.mjs.map +1 -0
- package/esm/production/route-registration-child.mjs +30 -2
- package/esm/production/route-registration-child.mjs.map +1 -1
- package/esm/production/route-registration-snapshot.mjs +22 -119
- package/esm/production/route-registration-snapshot.mjs.map +1 -1
- package/esm/resource/define-resource.d.mts +44 -14
- package/esm/resource/define-resource.d.mts.map +1 -1
- package/esm/resource/define-resource.mjs +0 -40
- package/esm/resource/define-resource.mjs.map +1 -1
- package/esm/resource/index.d.mts +1 -1
- package/esm/resource/resource.d.mts +4 -4
- package/esm/resource/resource.d.mts.map +1 -1
- package/esm/resource/resource.mjs.map +1 -1
- package/esm/resource/types.d.mts +89 -4
- package/esm/resource/types.d.mts.map +1 -1
- package/esm/restful/restful.d.mts +5 -5
- package/esm/restful/restful.d.mts.map +1 -1
- package/esm/router/index.d.mts +1 -1
- package/esm/router/named-api-routes-with-responses.mjs +59 -0
- package/esm/router/named-api-routes-with-responses.mjs.map +1 -0
- package/esm/router/response-schema-types.mjs +100 -0
- package/esm/router/response-schema-types.mjs.map +1 -0
- package/esm/router/router.d.mts +2 -2
- package/esm/router/router.d.mts.map +1 -1
- package/esm/router/router.mjs +38 -11
- package/esm/router/router.mjs.map +1 -1
- package/esm/router/types.d.mts +24 -1
- package/esm/router/types.d.mts.map +1 -1
- package/esm/tests/start-http-development-server.d.mts +10 -0
- package/esm/tests/start-http-development-server.d.mts.map +1 -1
- package/esm/tests/start-http-development-server.mjs +16 -2
- package/esm/tests/start-http-development-server.mjs.map +1 -1
- package/esm/validation/validateAll.mjs +48 -1
- package/esm/validation/validateAll.mjs.map +1 -1
- package/llms-full.txt +383 -80
- package/llms.txt +4 -3
- package/package.json +21 -21
- package/skills/README.md +4 -3
- package/skills/create-controller/SKILL.md +43 -2
- package/skills/define-resource/SKILL.md +100 -1
- package/skills/generate-openapi/SKILL.md +120 -0
- package/skills/index.md +3 -3
- package/skills/test-http/SKILL.md +9 -1
- package/skills/use-localization/SKILL.md +39 -26
- package/skills/validate-input/SKILL.md +2 -2
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
//#region ../core/src/openapi/cast-schema.ts
|
|
2
|
+
/**
|
|
3
|
+
* JSON Schema of the DEFAULT date output of a resource field (`iso`, `format`, `timestamp`
|
|
4
|
+
* and `humanTime` on; `timezone`, `locale` and `offset` off), matching `BaseCastOutput<"date">`.
|
|
5
|
+
*/
|
|
6
|
+
const DEFAULT_DATE_SCHEMA = {
|
|
7
|
+
type: "object",
|
|
8
|
+
properties: {
|
|
9
|
+
iso: {
|
|
10
|
+
type: "string",
|
|
11
|
+
format: "date-time"
|
|
12
|
+
},
|
|
13
|
+
format: { type: "string" },
|
|
14
|
+
timestamp: { type: "number" },
|
|
15
|
+
humanTime: { type: "string" }
|
|
16
|
+
},
|
|
17
|
+
required: [
|
|
18
|
+
"iso",
|
|
19
|
+
"format",
|
|
20
|
+
"timestamp",
|
|
21
|
+
"humanTime"
|
|
22
|
+
]
|
|
23
|
+
};
|
|
24
|
+
function baseCastSchema(base) {
|
|
25
|
+
switch (base) {
|
|
26
|
+
case "string":
|
|
27
|
+
case "localized": return { type: "string" };
|
|
28
|
+
case "url":
|
|
29
|
+
case "uploadsUrl":
|
|
30
|
+
case "storageUrl": return {
|
|
31
|
+
type: "string",
|
|
32
|
+
format: "uri"
|
|
33
|
+
};
|
|
34
|
+
case "number":
|
|
35
|
+
case "float": return { type: "number" };
|
|
36
|
+
case "int": return { type: "integer" };
|
|
37
|
+
case "boolean": return { type: "boolean" };
|
|
38
|
+
case "object": return { type: "object" };
|
|
39
|
+
case "array": return { type: "array" };
|
|
40
|
+
case "date": return structuredClone(DEFAULT_DATE_SCHEMA);
|
|
41
|
+
default: return;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Make a schema accept `null` too, the 3.1 way: `type: [T, "null"]`. A schema without a plain
|
|
46
|
+
* `type` (a `$ref`, `{}`) is wrapped in `anyOf` with `{ type: "null" }`.
|
|
47
|
+
*/
|
|
48
|
+
function makeNullable(schema) {
|
|
49
|
+
if (typeof schema.type === "string") return {
|
|
50
|
+
...schema,
|
|
51
|
+
type: [schema.type, "null"]
|
|
52
|
+
};
|
|
53
|
+
if (Array.isArray(schema.type)) return schema.type.includes("null") ? schema : {
|
|
54
|
+
...schema,
|
|
55
|
+
type: [...schema.type, "null"]
|
|
56
|
+
};
|
|
57
|
+
return { anyOf: [schema, { type: "null" }] };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* JSON Schema of a resource cast string, suffixes included (`"string"`, `"number?"`,
|
|
61
|
+
* `"string[]"`, `"date[]?"`). `?` means "always present, value or null"; `[]` wraps the base
|
|
62
|
+
* in an array, and with `?` it is the array that may be null.
|
|
63
|
+
*
|
|
64
|
+
* @returns `undefined` for a cast this mapper does not know.
|
|
65
|
+
*/
|
|
66
|
+
function castToJsonSchema(cast) {
|
|
67
|
+
let base = cast;
|
|
68
|
+
let nullable = false;
|
|
69
|
+
let isArray = false;
|
|
70
|
+
if (base.endsWith("?")) {
|
|
71
|
+
nullable = true;
|
|
72
|
+
base = base.slice(0, -1);
|
|
73
|
+
}
|
|
74
|
+
if (base.endsWith("[]")) {
|
|
75
|
+
isArray = true;
|
|
76
|
+
base = base.slice(0, -2);
|
|
77
|
+
}
|
|
78
|
+
const baseSchema = baseCastSchema(base);
|
|
79
|
+
if (!baseSchema) return;
|
|
80
|
+
const shaped = isArray ? {
|
|
81
|
+
type: "array",
|
|
82
|
+
items: baseSchema
|
|
83
|
+
} : baseSchema;
|
|
84
|
+
return nullable ? makeNullable(shaped) : shaped;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
//#endregion
|
|
88
|
+
export { castToJsonSchema };
|
|
89
|
+
//# sourceMappingURL=cast-schema.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cast-schema.mjs","names":[],"sources":["../../../../../../../core/src/openapi/cast-schema.ts"],"sourcesContent":["import type { OpenApiSchema } from \"./types\";\n\n/**\n * JSON Schema of the DEFAULT date output of a resource field (`iso`, `format`, `timestamp`\n * and `humanTime` on; `timezone`, `locale` and `offset` off), matching `BaseCastOutput<\"date\">`.\n */\nconst DEFAULT_DATE_SCHEMA: OpenApiSchema = {\n type: \"object\",\n properties: {\n iso: { type: \"string\", format: \"date-time\" },\n format: { type: \"string\" },\n timestamp: { type: \"number\" },\n humanTime: { type: \"string\" },\n },\n required: [\"iso\", \"format\", \"timestamp\", \"humanTime\"],\n};\n\nfunction baseCastSchema(base: string): OpenApiSchema | undefined {\n switch (base) {\n case \"string\":\n case \"localized\":\n return { type: \"string\" };\n case \"url\":\n case \"uploadsUrl\":\n case \"storageUrl\":\n return { type: \"string\", format: \"uri\" };\n case \"number\":\n case \"float\":\n return { type: \"number\" };\n case \"int\":\n return { type: \"integer\" };\n case \"boolean\":\n return { type: \"boolean\" };\n case \"object\":\n return { type: \"object\" };\n case \"array\":\n return { type: \"array\" };\n case \"date\":\n return structuredClone(DEFAULT_DATE_SCHEMA);\n default:\n return undefined;\n }\n}\n\n/**\n * Make a schema accept `null` too, the 3.1 way: `type: [T, \"null\"]`. A schema without a plain\n * `type` (a `$ref`, `{}`) is wrapped in `anyOf` with `{ type: \"null\" }`.\n */\nexport function makeNullable(schema: OpenApiSchema): OpenApiSchema {\n if (typeof schema.type === \"string\") {\n return { ...schema, type: [schema.type, \"null\"] };\n }\n\n if (Array.isArray(schema.type)) {\n return schema.type.includes(\"null\") ? schema : { ...schema, type: [...schema.type, \"null\"] };\n }\n\n return { anyOf: [schema, { type: \"null\" }] };\n}\n\n/**\n * JSON Schema of a resource cast string, suffixes included (`\"string\"`, `\"number?\"`,\n * `\"string[]\"`, `\"date[]?\"`). `?` means \"always present, value or null\"; `[]` wraps the base\n * in an array, and with `?` it is the array that may be null.\n *\n * @returns `undefined` for a cast this mapper does not know.\n */\nexport function castToJsonSchema(cast: string): OpenApiSchema | undefined {\n let base = cast;\n let nullable = false;\n let isArray = false;\n\n if (base.endsWith(\"?\")) {\n nullable = true;\n base = base.slice(0, -1);\n }\n\n if (base.endsWith(\"[]\")) {\n isArray = true;\n base = base.slice(0, -2);\n }\n\n const baseSchema = baseCastSchema(base);\n\n if (!baseSchema) {\n return undefined;\n }\n\n const shaped: OpenApiSchema = isArray ? { type: \"array\", items: baseSchema } : baseSchema;\n\n return nullable ? makeNullable(shaped) : shaped;\n}\n"],"mappings":";;;;;AAMA,MAAM,sBAAqC;CACzC,MAAM;CACN,YAAY;EACV,KAAK;GAAE,MAAM;GAAU,QAAQ;EAAY;EAC3C,QAAQ,EAAE,MAAM,SAAS;EACzB,WAAW,EAAE,MAAM,SAAS;EAC5B,WAAW,EAAE,MAAM,SAAS;CAC9B;CACA,UAAU;EAAC;EAAO;EAAU;EAAa;CAAW;AACtD;AAEA,SAAS,eAAe,MAAyC;CAC/D,QAAQ,MAAR;EACE,KAAK;EACL,KAAK,aACH,OAAO,EAAE,MAAM,SAAS;EAC1B,KAAK;EACL,KAAK;EACL,KAAK,cACH,OAAO;GAAE,MAAM;GAAU,QAAQ;EAAM;EACzC,KAAK;EACL,KAAK,SACH,OAAO,EAAE,MAAM,SAAS;EAC1B,KAAK,OACH,OAAO,EAAE,MAAM,UAAU;EAC3B,KAAK,WACH,OAAO,EAAE,MAAM,UAAU;EAC3B,KAAK,UACH,OAAO,EAAE,MAAM,SAAS;EAC1B,KAAK,SACH,OAAO,EAAE,MAAM,QAAQ;EACzB,KAAK,QACH,OAAO,gBAAgB,mBAAmB;EAC5C,SACE;CACJ;AACF;;;;;AAMA,SAAgB,aAAa,QAAsC;CACjE,IAAI,OAAO,OAAO,SAAS,UACzB,OAAO;EAAE,GAAG;EAAQ,MAAM,CAAC,OAAO,MAAM,MAAM;CAAE;CAGlD,IAAI,MAAM,QAAQ,OAAO,IAAI,GAC3B,OAAO,OAAO,KAAK,SAAS,MAAM,IAAI,SAAS;EAAE,GAAG;EAAQ,MAAM,CAAC,GAAG,OAAO,MAAM,MAAM;CAAE;CAG7F,OAAO,EAAE,OAAO,CAAC,QAAQ,EAAE,MAAM,OAAO,CAAC,EAAE;AAC7C;;;;;;;;AASA,SAAgB,iBAAiB,MAAyC;CACxE,IAAI,OAAO;CACX,IAAI,WAAW;CACf,IAAI,UAAU;CAEd,IAAI,KAAK,SAAS,GAAG,GAAG;EACtB,WAAW;EACX,OAAO,KAAK,MAAM,GAAG,EAAE;CACzB;CAEA,IAAI,KAAK,SAAS,IAAI,GAAG;EACvB,UAAU;EACV,OAAO,KAAK,MAAM,GAAG,EAAE;CACzB;CAEA,MAAM,aAAa,eAAe,IAAI;CAEtC,IAAI,CAAC,YACH;CAGF,MAAM,SAAwB,UAAU;EAAE,MAAM;EAAS,OAAO;CAAW,IAAI;CAE/E,OAAO,WAAW,aAAa,MAAM,IAAI;AAC3C"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { runRegistrationChild } from "../production/registration-child-runner.mjs";
|
|
2
|
+
import { OPENAPI_RESULT_MESSAGE, parseOpenApiChildResult } from "./openapi-protocol.mjs";
|
|
3
|
+
|
|
4
|
+
//#region ../core/src/openapi/collect-openapi-document.ts
|
|
5
|
+
/**
|
|
6
|
+
* Build the app's OpenAPI document in a fresh process.
|
|
7
|
+
*
|
|
8
|
+
* Uses the same registration child as `warlock build` (it imports the route modules without
|
|
9
|
+
* booting connectors) in OpenAPI mode: the document is built inside the child, where the Seal
|
|
10
|
+
* schemas and resource classes live, and only the finished JSON crosses IPC.
|
|
11
|
+
*/
|
|
12
|
+
function collectOpenApiDocument(options = {}) {
|
|
13
|
+
const cwd = options.cwd ?? process.cwd();
|
|
14
|
+
const openapi = {
|
|
15
|
+
version: 1,
|
|
16
|
+
...options.title !== void 0 ? { title: options.title } : {},
|
|
17
|
+
...options.server !== void 0 ? { server: options.server } : {},
|
|
18
|
+
...options.includePages !== void 0 ? { includePages: options.includePages } : {}
|
|
19
|
+
};
|
|
20
|
+
return runRegistrationChild({
|
|
21
|
+
cwd,
|
|
22
|
+
timeoutMs: options.timeoutMs,
|
|
23
|
+
childEntry: options.childEntry,
|
|
24
|
+
forkProcess: options.forkProcess,
|
|
25
|
+
request: {
|
|
26
|
+
version: 1,
|
|
27
|
+
cwd,
|
|
28
|
+
environment: options.environment,
|
|
29
|
+
runtimeStrategy: options.runtimeStrategy,
|
|
30
|
+
openapi
|
|
31
|
+
},
|
|
32
|
+
resultMessageType: OPENAPI_RESULT_MESSAGE,
|
|
33
|
+
resultNoun: "document",
|
|
34
|
+
readResult: (message) => parseOpenApiChildResult(message.result)
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
//#endregion
|
|
39
|
+
export { collectOpenApiDocument };
|
|
40
|
+
//# sourceMappingURL=collect-openapi-document.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collect-openapi-document.mjs","names":[],"sources":["../../../../../../../core/src/openapi/collect-openapi-document.ts"],"sourcesContent":["import {\n runRegistrationChild,\n type RegistrationChildForker,\n} from \"../production/registration-child-runner\";\nimport type { Environment, RuntimeStrategy } from \"../utils/environment\";\nimport {\n OPENAPI_PROTOCOL_VERSION,\n OPENAPI_RESULT_MESSAGE,\n parseOpenApiChildResult,\n type OpenApiChildRequestOptions,\n type OpenApiChildResult,\n} from \"./openapi-protocol\";\n\nexport type CollectOpenApiDocumentOptions = Readonly<{\n cwd?: string;\n environment?: Environment;\n runtimeStrategy?: RuntimeStrategy;\n timeoutMs?: number;\n childEntry?: string;\n forkProcess?: RegistrationChildForker;\n title?: string;\n server?: string;\n includePages?: boolean;\n}>;\n\n/**\n * Build the app's OpenAPI document in a fresh process.\n *\n * Uses the same registration child as `warlock build` (it imports the route modules without\n * booting connectors) in OpenAPI mode: the document is built inside the child, where the Seal\n * schemas and resource classes live, and only the finished JSON crosses IPC.\n */\nexport function collectOpenApiDocument(\n options: CollectOpenApiDocumentOptions = {},\n): Promise<OpenApiChildResult> {\n const cwd = options.cwd ?? process.cwd();\n const openapi: OpenApiChildRequestOptions = {\n version: OPENAPI_PROTOCOL_VERSION,\n ...(options.title !== undefined ? { title: options.title } : {}),\n ...(options.server !== undefined ? { server: options.server } : {}),\n ...(options.includePages !== undefined ? { includePages: options.includePages } : {}),\n };\n\n return runRegistrationChild<OpenApiChildResult>({\n cwd,\n timeoutMs: options.timeoutMs,\n childEntry: options.childEntry,\n forkProcess: options.forkProcess,\n request: {\n version: 1,\n cwd,\n environment: options.environment,\n runtimeStrategy: options.runtimeStrategy,\n openapi,\n },\n resultMessageType: OPENAPI_RESULT_MESSAGE,\n resultNoun: \"document\",\n readResult: (message) => parseOpenApiChildResult(message.result),\n });\n}\n"],"mappings":";;;;;;;;;;;AAgCA,SAAgB,uBACd,UAAyC,CAAC,GACb;CAC7B,MAAM,MAAM,QAAQ,OAAO,QAAQ,IAAI;CACvC,MAAM,UAAsC;EAC1C;EACA,GAAI,QAAQ,UAAU,SAAY,EAAE,OAAO,QAAQ,MAAM,IAAI,CAAC;EAC9D,GAAI,QAAQ,WAAW,SAAY,EAAE,QAAQ,QAAQ,OAAO,IAAI,CAAC;EACjE,GAAI,QAAQ,iBAAiB,SAAY,EAAE,cAAc,QAAQ,aAAa,IAAI,CAAC;CACrF;CAEA,OAAO,qBAAyC;EAC9C;EACA,WAAW,QAAQ;EACnB,YAAY,QAAQ;EACpB,aAAa,QAAQ;EACrB,SAAS;GACP,SAAS;GACT;GACA,aAAa,QAAQ;GACrB,iBAAiB,QAAQ;GACzB;EACF;EACA,mBAAmB;EACnB,YAAY;EACZ,aAAa,YAAY,wBAAwB,QAAQ,MAAM;CACjE,CAAC;AACH"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { OpenApiBuildResult } from "./types.mjs";
|
|
2
|
+
|
|
3
|
+
//#region ../core/src/openapi/development-openapi-provider.d.ts
|
|
4
|
+
type DevelopmentOpenApiProvider = () => Promise<OpenApiBuildResult>;
|
|
5
|
+
/**
|
|
6
|
+
* Called by the dev server at startup. The builder lives under `dev-server/`, so the
|
|
7
|
+
* production entry never reaches it; this registry is the only thing the root exports.
|
|
8
|
+
*/
|
|
9
|
+
declare function registerDevelopmentOpenApiProvider(build: DevelopmentOpenApiProvider): void;
|
|
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
|
+
declare function getDevelopmentOpenApiDocument(): Promise<OpenApiBuildResult>;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { getDevelopmentOpenApiDocument, registerDevelopmentOpenApiProvider };
|
|
17
|
+
//# sourceMappingURL=development-openapi-provider.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"development-openapi-provider.d.mts","names":[],"sources":["../../../../../../../core/src/openapi/development-openapi-provider.ts"],"mappings":";;;KAEK,0BAAA,SAAmC,OAAO,CAAC,kBAAA;;AAFE;;;iBAUlC,kCAAA,CAAmC,KAAiC,EAA1B,0BAA0B;AARlB;AAQlE;;;AARkE,iBAgB5C,6BAAA,IAAiC,OAAO,CAAC,kBAAA"}
|
|
@@ -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,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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"}
|