@moostjs/swagger 0.5.32 → 0.6.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/README.md +79 -0
- package/dist/index.cjs +767 -333
- package/dist/index.d.ts +289 -20
- package/dist/index.mjs +761 -335
- package/package.json +32 -32
package/dist/index.mjs
CHANGED
|
@@ -1,41 +1,55 @@
|
|
|
1
|
-
import { Const, Controller, Moost, getMoostMate, useControllerContext,
|
|
1
|
+
import { Const, Controller, Moost, current, getMoostMate, useControllerContext, useLogger } from "moost";
|
|
2
2
|
import { Get, HeaderHook, SetHeader, StatusHook, Url } from "@moostjs/event-http";
|
|
3
|
-
import {
|
|
4
|
-
import { THeaderHook, useSetHeaders } from "@wooksjs/event-http";
|
|
3
|
+
import { useResponse } from "@wooksjs/event-http";
|
|
5
4
|
import { serveFile } from "@wooksjs/http-static";
|
|
6
5
|
import Path from "path";
|
|
7
6
|
import { getAbsoluteFSPath } from "swagger-ui-dist";
|
|
8
|
-
import { parseZodType } from "zod-parser";
|
|
9
7
|
|
|
10
8
|
//#region packages/swagger/src/swagger.mate.ts
|
|
11
|
-
function getSwaggerMate() {
|
|
9
|
+
/** Returns the shared `Mate` instance extended with Swagger/OpenAPI metadata fields. */ function getSwaggerMate() {
|
|
12
10
|
return getMoostMate();
|
|
13
11
|
}
|
|
14
12
|
|
|
15
13
|
//#endregion
|
|
16
14
|
//#region packages/swagger/src/decorators.ts
|
|
17
|
-
const SwaggerTag = (tag) => getSwaggerMate().decorate("swaggerTags", tag, true);
|
|
18
|
-
const SwaggerExclude = () => getSwaggerMate().decorate("swaggerExclude", true);
|
|
19
|
-
const SwaggerDescription = (descr) => getSwaggerMate().decorate("swaggerDescription", descr);
|
|
15
|
+
/** Adds an OpenAPI tag to a controller or handler for grouping in the Swagger UI. */ const SwaggerTag = (tag) => getSwaggerMate().decorate("swaggerTags", tag, true);
|
|
16
|
+
/** Excludes a controller or handler from the generated OpenAPI spec. */ const SwaggerExclude = () => getSwaggerMate().decorate("swaggerExclude", true);
|
|
17
|
+
/** Sets the OpenAPI description for a handler. */ const SwaggerDescription = (descr) => getSwaggerMate().decorate("swaggerDescription", descr);
|
|
20
18
|
function SwaggerResponse(code, opts, example) {
|
|
21
19
|
return getSwaggerMate().decorate((meta) => {
|
|
22
20
|
let ex;
|
|
23
21
|
if (example) ex = example;
|
|
24
22
|
if (typeof code !== "number" && opts) ex = opts;
|
|
23
|
+
if (ex === void 0) {
|
|
24
|
+
ex = typeof code === "number" ? opts : code;
|
|
25
|
+
ex = ex?.example;
|
|
26
|
+
}
|
|
25
27
|
meta.swaggerResponses = meta.swaggerResponses || {};
|
|
26
28
|
const keyCode = typeof code === "number" ? code : 0;
|
|
27
29
|
const opt = typeof code === "number" ? opts : code;
|
|
28
30
|
const contentType = typeof opt.contentType === "string" ? opt.contentType : "*/*";
|
|
31
|
+
const description = typeof opt.description === "string" ? opt.description : void 0;
|
|
32
|
+
const headers = opt.headers;
|
|
29
33
|
const response = ["object", "function"].includes(typeof opt.response) ? opt.response : opt;
|
|
30
|
-
meta.swaggerResponses[keyCode] = meta.swaggerResponses[keyCode] || {};
|
|
31
|
-
meta.swaggerResponses[keyCode][contentType] = {
|
|
34
|
+
meta.swaggerResponses[keyCode] = meta.swaggerResponses[keyCode] || { content: {} };
|
|
35
|
+
meta.swaggerResponses[keyCode].content[contentType] = {
|
|
32
36
|
response,
|
|
33
37
|
example: ex
|
|
34
38
|
};
|
|
39
|
+
if (description) meta.swaggerResponses[keyCode].description = description;
|
|
40
|
+
if (headers) meta.swaggerResponses[keyCode].headers = {
|
|
41
|
+
...meta.swaggerResponses[keyCode].headers,
|
|
42
|
+
...headers
|
|
43
|
+
};
|
|
35
44
|
return meta;
|
|
36
45
|
});
|
|
37
46
|
}
|
|
38
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Defines the request body schema in the OpenAPI spec.
|
|
49
|
+
*
|
|
50
|
+
* @param opt - Schema definition. Use `{ response: MyDto }` for a typed schema,
|
|
51
|
+
* or `{ response: MyDto, contentType: 'multipart/form-data' }` for a specific content type.
|
|
52
|
+
*/ function SwaggerRequestBody(opt) {
|
|
39
53
|
return getSwaggerMate().decorate((meta) => {
|
|
40
54
|
meta.swaggerRequestBody = meta.swaggerRequestBody || {};
|
|
41
55
|
const contentType = typeof opt.contentType === "string" ? opt.contentType : "application/json";
|
|
@@ -44,26 +58,155 @@ function SwaggerRequestBody(opt) {
|
|
|
44
58
|
return meta;
|
|
45
59
|
});
|
|
46
60
|
}
|
|
47
|
-
function SwaggerParam(opts) {
|
|
61
|
+
/** Defines a parameter (query, path, header, cookie) in the OpenAPI spec. */ function SwaggerParam(opts) {
|
|
48
62
|
return getSwaggerMate().decorate("swaggerParams", opts, true);
|
|
49
63
|
}
|
|
50
|
-
function SwaggerExample(example) {
|
|
64
|
+
/** Attaches an example value to a handler's OpenAPI documentation. */ function SwaggerExample(example) {
|
|
51
65
|
return getSwaggerMate().decorate("swaggerExample", example);
|
|
52
66
|
}
|
|
67
|
+
/** Marks a handler or controller as public, opting out of inherited security requirements. */ const SwaggerPublic = () => getSwaggerMate().decorate("swaggerPublic", true);
|
|
68
|
+
/** Marks a handler or controller as deprecated in the OpenAPI spec. */ const SwaggerDeprecated = () => getSwaggerMate().decorate("swaggerDeprecated", true);
|
|
69
|
+
/** Overrides the auto-generated operationId for an endpoint. */ const SwaggerOperationId = (id) => getSwaggerMate().decorate("swaggerOperationId", id);
|
|
70
|
+
/** Links an operation to external documentation. */ const SwaggerExternalDocs = (url, description) => getSwaggerMate().decorate("swaggerExternalDocs", {
|
|
71
|
+
url,
|
|
72
|
+
...description ? { description } : {}
|
|
73
|
+
});
|
|
74
|
+
/**
|
|
75
|
+
* Attaches a security requirement to a handler or controller (OR semantics).
|
|
76
|
+
* Multiple calls add alternative requirements — any one suffices.
|
|
77
|
+
*
|
|
78
|
+
* @param schemeName - The name of the security scheme (must match a key in securitySchemes)
|
|
79
|
+
* @param scopes - OAuth2/OIDC scopes required (default: [])
|
|
80
|
+
*/ function SwaggerSecurity(schemeName, scopes = []) {
|
|
81
|
+
return getSwaggerMate().decorate("swaggerSecurity", { [schemeName]: scopes }, true);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Attaches a combined security requirement (AND semantics).
|
|
85
|
+
* All schemes in the requirement must be satisfied simultaneously.
|
|
86
|
+
*/ function SwaggerSecurityAll(requirement) {
|
|
87
|
+
return getSwaggerMate().decorate("swaggerSecurity", requirement, true);
|
|
88
|
+
}
|
|
89
|
+
function SwaggerLink(codeOrName, nameOrOptions, maybeOptions) {
|
|
90
|
+
const statusCode = typeof codeOrName === "number" ? codeOrName : 0;
|
|
91
|
+
const name = typeof codeOrName === "string" ? codeOrName : nameOrOptions;
|
|
92
|
+
const options = typeof codeOrName === "string" ? nameOrOptions : maybeOptions;
|
|
93
|
+
const config = {
|
|
94
|
+
statusCode,
|
|
95
|
+
name,
|
|
96
|
+
..."operationId" in options && options.operationId ? { operationId: options.operationId } : {},
|
|
97
|
+
..."operationRef" in options && options.operationRef ? { operationRef: options.operationRef } : {},
|
|
98
|
+
..."handler" in options && options.handler ? { handler: options.handler } : {},
|
|
99
|
+
...options.parameters ? { parameters: options.parameters } : {},
|
|
100
|
+
...options.requestBody ? { requestBody: options.requestBody } : {},
|
|
101
|
+
...options.description ? { description: options.description } : {},
|
|
102
|
+
...options.server ? { server: options.server } : {}
|
|
103
|
+
};
|
|
104
|
+
return getSwaggerMate().decorate("swaggerLinks", config, true);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Documents an OpenAPI callback (webhook) on an operation.
|
|
108
|
+
* Describes a request your server sends to a client-provided URL.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```ts
|
|
112
|
+
* @SwaggerCallback('onEvent', {
|
|
113
|
+
* expression: '{$request.body#/callbackUrl}',
|
|
114
|
+
* requestBody: EventPayloadDto,
|
|
115
|
+
* description: 'Event notification sent to subscriber',
|
|
116
|
+
* })
|
|
117
|
+
* @Post('subscribe')
|
|
118
|
+
* subscribe() { ... }
|
|
119
|
+
* ```
|
|
120
|
+
*/ function SwaggerCallback(name, options) {
|
|
121
|
+
const config = {
|
|
122
|
+
name,
|
|
123
|
+
expression: options.expression,
|
|
124
|
+
...options.method ? { method: options.method } : {},
|
|
125
|
+
...options.requestBody ? { requestBody: options.requestBody } : {},
|
|
126
|
+
...options.contentType ? { contentType: options.contentType } : {},
|
|
127
|
+
...options.description ? { description: options.description } : {},
|
|
128
|
+
...options.responseStatus ? { responseStatus: options.responseStatus } : {},
|
|
129
|
+
...options.responseDescription ? { responseDescription: options.responseDescription } : {}
|
|
130
|
+
};
|
|
131
|
+
return getSwaggerMate().decorate("swaggerCallbacks", config, true);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
//#endregion
|
|
135
|
+
//#region packages/swagger/src/json-to-yaml.ts
|
|
136
|
+
const YAML_SPECIAL = /^[\s#!&*|>'{}[\],?:@`-]|[:#]\s|[\n\r]|\s$/;
|
|
137
|
+
function quoteString(str) {
|
|
138
|
+
if (str === "") return "''";
|
|
139
|
+
if (YAML_SPECIAL.test(str) || str === "true" || str === "false" || str === "null") return JSON.stringify(str);
|
|
140
|
+
const num = Number(str);
|
|
141
|
+
if (str.length > 0 && !Number.isNaN(num) && String(num) === str) return JSON.stringify(str);
|
|
142
|
+
return str;
|
|
143
|
+
}
|
|
144
|
+
function serializeValue(value, indent) {
|
|
145
|
+
if (value === null || value === void 0) return "null";
|
|
146
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
147
|
+
if (typeof value === "number") return Number.isFinite(value) ? String(value) : "null";
|
|
148
|
+
if (typeof value === "string") return quoteString(value);
|
|
149
|
+
const pad = " ".repeat(indent);
|
|
150
|
+
const childPad = " ".repeat(indent + 1);
|
|
151
|
+
if (Array.isArray(value)) {
|
|
152
|
+
if (value.length === 0) return "[]";
|
|
153
|
+
const lines = [];
|
|
154
|
+
for (const item of value) if (isObject(item) || Array.isArray(item)) {
|
|
155
|
+
const nested = serializeValue(item, indent + 1);
|
|
156
|
+
lines.push(`${pad}- ${nested.slice(childPad.length)}`);
|
|
157
|
+
} else lines.push(`${pad}- ${serializeValue(item, 0)}`);
|
|
158
|
+
return lines.join("\n");
|
|
159
|
+
}
|
|
160
|
+
if (isObject(value)) {
|
|
161
|
+
const entries = Object.entries(value);
|
|
162
|
+
if (entries.length === 0) return "{}";
|
|
163
|
+
const lines = [];
|
|
164
|
+
for (const [key, val] of entries) {
|
|
165
|
+
const yamlKey = quoteString(key);
|
|
166
|
+
if (isObject(val) || Array.isArray(val)) {
|
|
167
|
+
const nested = serializeValue(val, indent + 1);
|
|
168
|
+
if (nested === "[]" || nested === "{}") lines.push(`${pad}${yamlKey}: ${nested}`);
|
|
169
|
+
else lines.push(`${pad}${yamlKey}:\n${nested}`);
|
|
170
|
+
} else lines.push(`${pad}${yamlKey}: ${serializeValue(val, 0)}`);
|
|
171
|
+
}
|
|
172
|
+
return lines.join("\n");
|
|
173
|
+
}
|
|
174
|
+
return String(value);
|
|
175
|
+
}
|
|
176
|
+
function isObject(value) {
|
|
177
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
178
|
+
}
|
|
179
|
+
function jsonToYaml(value) {
|
|
180
|
+
return `${serializeValue(value, 0)}\n`;
|
|
181
|
+
}
|
|
53
182
|
|
|
54
183
|
//#endregion
|
|
55
184
|
//#region packages/swagger/src/mapping.ts
|
|
185
|
+
const globalSchemas = {};
|
|
186
|
+
let schemaRefs = /* @__PURE__ */ new WeakMap();
|
|
187
|
+
const nameToType = /* @__PURE__ */ new Map();
|
|
56
188
|
function mapToSwaggerSpec(metadata, options, logger) {
|
|
189
|
+
resetSchemaRegistry();
|
|
190
|
+
const is31 = options?.openapiVersion === "3.1";
|
|
191
|
+
const collectedSecuritySchemes = { ...options?.securitySchemes };
|
|
57
192
|
const swaggerSpec = {
|
|
58
|
-
openapi: "3.0.0",
|
|
193
|
+
openapi: is31 ? "3.1.0" : "3.0.0",
|
|
59
194
|
info: {
|
|
60
195
|
title: options?.title || "API Documentation",
|
|
61
|
-
|
|
196
|
+
...options?.description ? { description: options.description } : {},
|
|
197
|
+
version: options?.version || "1.0.0",
|
|
198
|
+
...options?.contact ? { contact: options.contact } : {},
|
|
199
|
+
...options?.license ? { license: options.license } : {},
|
|
200
|
+
...options?.termsOfService ? { termsOfService: options.termsOfService } : {}
|
|
62
201
|
},
|
|
63
202
|
paths: {},
|
|
64
203
|
tags: [],
|
|
204
|
+
...options?.servers?.length ? { servers: options.servers } : {},
|
|
205
|
+
...options?.externalDocs ? { externalDocs: options.externalDocs } : {},
|
|
206
|
+
...options?.security ? { security: options.security } : {},
|
|
65
207
|
components: { schemas: globalSchemas }
|
|
66
208
|
};
|
|
209
|
+
const deferredLinks = [];
|
|
67
210
|
for (const controller of metadata) {
|
|
68
211
|
const cmeta = controller.meta;
|
|
69
212
|
if (cmeta?.swaggerExclude) continue;
|
|
@@ -75,67 +218,93 @@ function mapToSwaggerSpec(metadata, options, logger) {
|
|
|
75
218
|
const uniqueParams = {};
|
|
76
219
|
const handlerPath = handler.registeredAs[0].path;
|
|
77
220
|
const handlerMethod = hh.method?.toLowerCase() || "get";
|
|
78
|
-
const
|
|
221
|
+
const handlerSummary = hmeta?.label;
|
|
222
|
+
const handlerDescription = hmeta?.swaggerDescription || hmeta?.description;
|
|
79
223
|
const handlerTags = [...controllerTags, ...hmeta?.swaggerTags || []];
|
|
80
224
|
if (!swaggerSpec.paths[handlerPath]) swaggerSpec.paths[handlerPath] = {};
|
|
81
225
|
let responses;
|
|
82
|
-
if (hmeta?.swaggerResponses) for (const [code,
|
|
226
|
+
if (hmeta?.swaggerResponses) for (const [code, responseEntry] of Object.entries(hmeta.swaggerResponses)) {
|
|
83
227
|
const newCode = code === "0" ? getDefaultStatusCode(handlerMethod) : code;
|
|
84
|
-
for (const [contentType, conf] of Object.entries(
|
|
228
|
+
for (const [contentType, conf] of Object.entries(responseEntry.content)) {
|
|
85
229
|
const { response, example } = conf;
|
|
86
|
-
const schema =
|
|
230
|
+
const schema = resolveSwaggerSchemaFromConfig(response);
|
|
87
231
|
if (schema) {
|
|
88
232
|
responses = responses || {};
|
|
89
|
-
|
|
233
|
+
const schemaWithExample = example !== void 0 ? {
|
|
90
234
|
...schema,
|
|
91
|
-
example
|
|
92
|
-
}
|
|
235
|
+
example
|
|
236
|
+
} : schema;
|
|
237
|
+
if (!responses[newCode]) responses[newCode] = {
|
|
238
|
+
description: responseEntry.description || defaultStatusDescription(Number(newCode)),
|
|
239
|
+
content: {}
|
|
240
|
+
};
|
|
241
|
+
responses[newCode].content[contentType] = { schema: schemaWithExample };
|
|
93
242
|
}
|
|
94
243
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if ([
|
|
99
|
-
"ZodString",
|
|
100
|
-
"ZodNumber",
|
|
101
|
-
"ZodObject",
|
|
102
|
-
"ZodArray",
|
|
103
|
-
"ZodBoolean"
|
|
104
|
-
].includes(parsed.$type)) {
|
|
105
|
-
const schema = getSwaggerSchema(parsed);
|
|
106
|
-
if (schema) {
|
|
244
|
+
if (responseEntry.headers) {
|
|
245
|
+
const resolvedHeaders = resolveResponseHeaders(responseEntry.headers);
|
|
246
|
+
if (resolvedHeaders) {
|
|
107
247
|
responses = responses || {};
|
|
108
|
-
responses[
|
|
248
|
+
if (!responses[newCode]) responses[newCode] = {
|
|
249
|
+
description: defaultStatusDescription(Number(newCode)),
|
|
250
|
+
content: {}
|
|
251
|
+
};
|
|
252
|
+
responses[newCode].headers = resolvedHeaders;
|
|
109
253
|
}
|
|
110
254
|
}
|
|
111
255
|
}
|
|
256
|
+
const defaultCode = getDefaultStatusCode(handlerMethod);
|
|
257
|
+
if (!responses?.[defaultCode] && hmeta?.returnType) {
|
|
258
|
+
const ensured = ensureSchema(hmeta.returnType);
|
|
259
|
+
const schema = toSchemaOrRef(ensured);
|
|
260
|
+
if (schema) {
|
|
261
|
+
responses = responses || {};
|
|
262
|
+
responses[defaultCode] = {
|
|
263
|
+
description: defaultStatusDescription(Number(defaultCode)),
|
|
264
|
+
content: { "*/*": { schema } }
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
112
268
|
let reqBodyRequired = true;
|
|
113
|
-
const
|
|
269
|
+
const bodyContent = {};
|
|
114
270
|
if (hmeta?.swaggerRequestBody) for (const [contentType, type] of Object.entries(hmeta.swaggerRequestBody)) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (type instanceof z.ZodType) zt = type;
|
|
118
|
-
else if (typeof type === "function") zt = getZodType({ type });
|
|
119
|
-
if (zt) {
|
|
120
|
-
const parsed = myParseZod(zt);
|
|
121
|
-
if ([
|
|
122
|
-
"ZodString",
|
|
123
|
-
"ZodNumber",
|
|
124
|
-
"ZodObject",
|
|
125
|
-
"ZodArray",
|
|
126
|
-
"ZodBoolean"
|
|
127
|
-
].includes(parsed.$type)) schema = getSwaggerSchema(parsed);
|
|
128
|
-
}
|
|
129
|
-
bodyConfig[contentType] = { schema };
|
|
271
|
+
const schema = resolveSwaggerSchemaFromConfig(type);
|
|
272
|
+
if (schema) bodyContent[contentType] = { schema };
|
|
130
273
|
}
|
|
131
274
|
swaggerSpec.paths[handlerPath][handlerMethod] = {
|
|
132
|
-
summary:
|
|
133
|
-
|
|
275
|
+
summary: handlerSummary,
|
|
276
|
+
description: handlerDescription,
|
|
277
|
+
operationId: hmeta?.swaggerOperationId || hmeta?.id || `${handlerMethod.toUpperCase()}_${handlerPath.replaceAll(/\//g, "_").replaceAll(/[{}]/g, "__").replaceAll(/[^\dA-Za-z]/g, "_")}`,
|
|
134
278
|
tags: handlerTags,
|
|
135
279
|
parameters: [],
|
|
136
280
|
responses
|
|
137
281
|
};
|
|
138
282
|
const endpointSpec = swaggerSpec.paths[handlerPath][handlerMethod];
|
|
283
|
+
if (hmeta?.swaggerDeprecated || cmeta?.swaggerDeprecated) endpointSpec.deprecated = true;
|
|
284
|
+
if (hmeta?.swaggerExternalDocs) endpointSpec.externalDocs = hmeta.swaggerExternalDocs;
|
|
285
|
+
const opSecurity = resolveOperationSecurity(cmeta, hmeta, collectedSecuritySchemes);
|
|
286
|
+
if (opSecurity !== void 0) endpointSpec.security = opSecurity;
|
|
287
|
+
if (hmeta?.swaggerLinks?.length) for (const linkConfig of hmeta.swaggerLinks) deferredLinks.push({
|
|
288
|
+
endpointSpec,
|
|
289
|
+
httpMethod: handlerMethod,
|
|
290
|
+
config: linkConfig
|
|
291
|
+
});
|
|
292
|
+
if (hmeta?.swaggerCallbacks?.length) {
|
|
293
|
+
endpointSpec.callbacks = endpointSpec.callbacks || {};
|
|
294
|
+
for (const cb of hmeta.swaggerCallbacks) {
|
|
295
|
+
const method = (cb.method || "post").toLowerCase();
|
|
296
|
+
const contentType = cb.contentType || "application/json";
|
|
297
|
+
const status = String(cb.responseStatus || 200);
|
|
298
|
+
const responseDesc = cb.responseDescription || "OK";
|
|
299
|
+
const pathItem = { responses: { [status]: { description: responseDesc } } };
|
|
300
|
+
if (cb.description) pathItem.description = cb.description;
|
|
301
|
+
if (cb.requestBody) {
|
|
302
|
+
const schema = resolveSwaggerSchemaFromConfig(cb.requestBody);
|
|
303
|
+
if (schema) pathItem.requestBody = { content: { [contentType]: { schema } } };
|
|
304
|
+
}
|
|
305
|
+
endpointSpec.callbacks[cb.name] = { [cb.expression]: { [method]: pathItem } };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
139
308
|
function addParam(param) {
|
|
140
309
|
const key = `${param.in}//${param.name}`;
|
|
141
310
|
if (uniqueParams[key]) {
|
|
@@ -152,298 +321,459 @@ function mapToSwaggerSpec(metadata, options, logger) {
|
|
|
152
321
|
in: param.in,
|
|
153
322
|
description: param.description,
|
|
154
323
|
required: !!param.required,
|
|
155
|
-
schema:
|
|
324
|
+
schema: resolveSwaggerSchemaFromConfig(param.type) || { type: "string" }
|
|
156
325
|
});
|
|
157
326
|
for (const param of hmeta?.swaggerParams || []) addParam({
|
|
158
327
|
name: param.name,
|
|
159
328
|
in: param.in,
|
|
160
329
|
description: param.description,
|
|
161
330
|
required: !!param.required,
|
|
162
|
-
schema:
|
|
331
|
+
schema: resolveSwaggerSchemaFromConfig(param.type) || { type: "string" }
|
|
163
332
|
});
|
|
164
333
|
for (const paramName of handler.registeredAs[0].args) {
|
|
165
334
|
const paramIndex = handler.meta.params.findIndex((param) => param.paramSource === "ROUTE" && param.paramName === paramName);
|
|
166
335
|
const paramMeta = handler.meta.params[paramIndex];
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (paramMeta) {
|
|
170
|
-
const zodType = getZodTypeForProp({
|
|
171
|
-
type: controller.type,
|
|
172
|
-
key: handler.method,
|
|
173
|
-
index: paramIndex
|
|
174
|
-
}, {
|
|
175
|
-
type: paramMeta.type,
|
|
176
|
-
additionalMeta: paramMeta
|
|
177
|
-
}, void 0, logger);
|
|
178
|
-
parsed = myParseZod(zodType);
|
|
179
|
-
schema = getSwaggerSchema(parsed, true);
|
|
180
|
-
}
|
|
336
|
+
const ensured = ensureSchema(paramMeta?.type);
|
|
337
|
+
const schema = toSchemaOrRef(ensured) || { type: "string" };
|
|
181
338
|
addParam({
|
|
182
339
|
name: paramName,
|
|
183
340
|
in: "path",
|
|
184
341
|
description: paramMeta ? paramMeta.description : void 0,
|
|
185
|
-
required: !paramMeta
|
|
186
|
-
schema
|
|
342
|
+
required: !paramMeta?.optional,
|
|
343
|
+
schema
|
|
187
344
|
});
|
|
188
345
|
}
|
|
189
|
-
for (
|
|
190
|
-
const paramMeta = handler.meta.params[i];
|
|
346
|
+
for (const paramMeta of handler.meta.params) {
|
|
191
347
|
if (paramMeta.paramSource && ["QUERY_ITEM", "QUERY"].includes(paramMeta.paramSource)) {
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
348
|
+
const ensured = ensureSchema(paramMeta.type);
|
|
349
|
+
if (paramMeta.paramSource === "QUERY_ITEM") {
|
|
350
|
+
const schema = toSchemaOrRef(ensured);
|
|
351
|
+
const normalized = schema ? normalizeQueryParamSchema(schema) : void 0;
|
|
352
|
+
endpointSpec.parameters.push({
|
|
353
|
+
name: paramMeta.paramName || "",
|
|
354
|
+
in: "query",
|
|
355
|
+
description: paramMeta.description,
|
|
356
|
+
required: !paramMeta.optional,
|
|
357
|
+
schema: normalized || { type: "string" }
|
|
358
|
+
});
|
|
359
|
+
} else if (paramMeta.paramSource === "QUERY") {
|
|
360
|
+
const schema = ensured?.schema;
|
|
361
|
+
if (schema?.type === "object" && schema.properties) {
|
|
362
|
+
const requiredProps = new Set(schema.required || []);
|
|
363
|
+
for (const [key, value] of Object.entries(schema.properties)) {
|
|
364
|
+
const propertySchema = cloneSchema(value);
|
|
365
|
+
const normalizedProperty = normalizeQueryParamSchema(propertySchema);
|
|
366
|
+
if (normalizedProperty) endpointSpec.parameters.push({
|
|
367
|
+
name: key,
|
|
368
|
+
in: "query",
|
|
369
|
+
description: normalizedProperty.description,
|
|
370
|
+
required: !paramMeta.optional && requiredProps.has(key),
|
|
371
|
+
schema: normalizedProperty
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
} else if (ensured) {
|
|
375
|
+
const schema$1 = toSchemaOrRef(ensured);
|
|
376
|
+
const normalized = schema$1 ? normalizeQueryParamSchema(schema$1) : void 0;
|
|
377
|
+
endpointSpec.parameters.push({
|
|
378
|
+
name: paramMeta.paramName || "",
|
|
214
379
|
in: "query",
|
|
215
|
-
description:
|
|
216
|
-
required: !
|
|
217
|
-
schema:
|
|
218
|
-
};
|
|
219
|
-
endpointSpec.parameters.push(swaggerSchema);
|
|
380
|
+
description: paramMeta.description,
|
|
381
|
+
required: !paramMeta.optional,
|
|
382
|
+
schema: normalized || { type: "string" }
|
|
383
|
+
});
|
|
220
384
|
}
|
|
221
385
|
}
|
|
222
386
|
}
|
|
223
387
|
if (paramMeta.paramSource === "BODY") {
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
additionalMeta: paramMeta
|
|
231
|
-
}, void 0, logger);
|
|
232
|
-
const parsed = myParseZod(zodType);
|
|
233
|
-
let contentType = "";
|
|
234
|
-
switch (parsed.$type) {
|
|
235
|
-
case "ZodString":
|
|
236
|
-
case "ZodNumber":
|
|
237
|
-
case "ZodBigInt":
|
|
238
|
-
case "ZodBoolean":
|
|
239
|
-
case "ZodDate":
|
|
240
|
-
case "ZodEnum":
|
|
241
|
-
case "ZodNativeEnum":
|
|
242
|
-
case "ZodLiteral": {
|
|
243
|
-
contentType = "text/plan";
|
|
244
|
-
break;
|
|
245
|
-
}
|
|
246
|
-
default: contentType = "application/json";
|
|
388
|
+
const ensured = ensureSchema(paramMeta.type);
|
|
389
|
+
const schema = toSchemaOrRef(ensured);
|
|
390
|
+
if (schema) {
|
|
391
|
+
const contentType = inferBodyContentType(schema, ensured?.schema);
|
|
392
|
+
if (!bodyContent[contentType]) bodyContent[contentType] = { schema };
|
|
393
|
+
reqBodyRequired = !paramMeta.optional;
|
|
247
394
|
}
|
|
248
|
-
if (!bodyConfig[contentType]) bodyConfig[contentType] = { schema: getSwaggerSchema(parsed) };
|
|
249
|
-
reqBodyRequired = !zodType.isOptional() && !paramMeta.optional;
|
|
250
395
|
}
|
|
251
396
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
397
|
+
const bodyEntries = Object.entries(bodyContent).filter((entry) => entry[1] && entry[1].schema !== void 0);
|
|
398
|
+
if (bodyEntries.length > 0) {
|
|
399
|
+
const content = {};
|
|
400
|
+
for (const [contentType, { schema }] of bodyEntries) content[contentType] = { schema };
|
|
401
|
+
endpointSpec.requestBody = {
|
|
402
|
+
content,
|
|
403
|
+
required: reqBodyRequired
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (deferredLinks.length > 0) {
|
|
409
|
+
const handlerOpIds = /* @__PURE__ */ new Map();
|
|
410
|
+
for (const controller of metadata) for (const handler of controller.handlers) {
|
|
411
|
+
const hh = handler.handler;
|
|
412
|
+
if (hh.type !== "HTTP" || handler.registeredAs.length === 0) continue;
|
|
413
|
+
const path = handler.registeredAs[0].path;
|
|
414
|
+
const method = hh.method?.toLowerCase() || "get";
|
|
415
|
+
const opId = swaggerSpec.paths[path]?.[method]?.operationId;
|
|
416
|
+
if (opId) {
|
|
417
|
+
if (!handlerOpIds.has(controller.type)) handlerOpIds.set(controller.type, /* @__PURE__ */ new Map());
|
|
418
|
+
const methodMap = handlerOpIds.get(controller.type);
|
|
419
|
+
methodMap?.set(handler.method, opId);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
for (const { endpointSpec, httpMethod, config } of deferredLinks) {
|
|
423
|
+
const statusCode = config.statusCode === 0 ? String(getDefaultStatusCode(httpMethod)) : String(config.statusCode);
|
|
424
|
+
const link = {};
|
|
425
|
+
if (config.handler) {
|
|
426
|
+
const [ctrlClass, methodName] = config.handler;
|
|
427
|
+
const resolvedId = handlerOpIds.get(ctrlClass)?.get(methodName);
|
|
428
|
+
if (!resolvedId) continue;
|
|
429
|
+
link.operationId = resolvedId;
|
|
430
|
+
} else if (config.operationId) link.operationId = config.operationId;
|
|
431
|
+
else if (config.operationRef) link.operationRef = config.operationRef;
|
|
432
|
+
if (config.parameters) link.parameters = config.parameters;
|
|
433
|
+
if (config.requestBody) link.requestBody = config.requestBody;
|
|
434
|
+
if (config.description) link.description = config.description;
|
|
435
|
+
if (config.server) link.server = config.server;
|
|
436
|
+
if (!endpointSpec.responses) endpointSpec.responses = {};
|
|
437
|
+
if (!endpointSpec.responses[statusCode]) endpointSpec.responses[statusCode] = {
|
|
438
|
+
description: defaultStatusDescription(Number(statusCode)),
|
|
439
|
+
content: {}
|
|
255
440
|
};
|
|
441
|
+
const responseEntry = endpointSpec.responses[statusCode];
|
|
442
|
+
if (!responseEntry.links) responseEntry.links = {};
|
|
443
|
+
responseEntry.links[config.name] = link;
|
|
256
444
|
}
|
|
257
445
|
}
|
|
446
|
+
const manualTags = new Map((options?.tags || []).map((t) => [t.name, t]));
|
|
447
|
+
const discoveredNames = /* @__PURE__ */ new Set();
|
|
448
|
+
for (const methods of Object.values(swaggerSpec.paths)) for (const endpoint of Object.values(methods)) for (const tag of endpoint.tags) discoveredNames.add(tag);
|
|
449
|
+
for (const tag of manualTags.values()) swaggerSpec.tags.push(tag);
|
|
450
|
+
for (const name of discoveredNames) if (!manualTags.has(name)) swaggerSpec.tags.push({ name });
|
|
451
|
+
const ownedSchemas = {};
|
|
452
|
+
for (const [key, value] of Object.entries(globalSchemas)) ownedSchemas[key] = cloneSchema(value);
|
|
453
|
+
swaggerSpec.components.schemas = ownedSchemas;
|
|
454
|
+
if (Object.keys(collectedSecuritySchemes).length > 0) swaggerSpec.components.securitySchemes = collectedSecuritySchemes;
|
|
455
|
+
if (is31) transformSpecTo31(swaggerSpec);
|
|
258
456
|
return swaggerSpec;
|
|
259
457
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const { regex } = parsed.$checks;
|
|
277
|
-
if (regex) schema.pattern = regex.source;
|
|
278
|
-
}
|
|
458
|
+
function resolveSwaggerSchemaFromConfig(type) {
|
|
459
|
+
if (type === void 0) return void 0;
|
|
460
|
+
const ensured = ensureSchema(type);
|
|
461
|
+
return toSchemaOrRef(ensured);
|
|
462
|
+
}
|
|
463
|
+
function resolveResponseHeaders(headers) {
|
|
464
|
+
const resolved = {};
|
|
465
|
+
let hasAny = false;
|
|
466
|
+
for (const [name, header] of Object.entries(headers)) {
|
|
467
|
+
const schema = resolveSwaggerSchemaFromConfig(header.type) || { type: "string" };
|
|
468
|
+
const entry = { schema };
|
|
469
|
+
if (header.description) entry.description = header.description;
|
|
470
|
+
if (header.required !== void 0) entry.required = header.required;
|
|
471
|
+
if (header.example !== void 0) entry.example = header.example;
|
|
472
|
+
resolved[name] = entry;
|
|
473
|
+
hasAny = true;
|
|
279
474
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
475
|
+
return hasAny ? resolved : void 0;
|
|
476
|
+
}
|
|
477
|
+
function toSchemaOrRef(result) {
|
|
478
|
+
if (!result) return void 0;
|
|
479
|
+
if (result.ref) return { $ref: result.ref };
|
|
480
|
+
return cloneSchema(result.schema);
|
|
481
|
+
}
|
|
482
|
+
function inferBodyContentType(schema, resolved) {
|
|
483
|
+
const target = resolved ?? resolveSchemaFromRef(schema);
|
|
484
|
+
const schemaType = target?.type ?? schema.type;
|
|
485
|
+
if (typeof schemaType === "string" && [
|
|
486
|
+
"string",
|
|
487
|
+
"number",
|
|
488
|
+
"integer",
|
|
489
|
+
"boolean"
|
|
490
|
+
].includes(schemaType)) return "text/plain";
|
|
491
|
+
return "application/json";
|
|
492
|
+
}
|
|
493
|
+
const SIMPLE_QUERY_TYPES = new Set([
|
|
494
|
+
"string",
|
|
495
|
+
"number",
|
|
496
|
+
"integer",
|
|
497
|
+
"boolean"
|
|
498
|
+
]);
|
|
499
|
+
function normalizeQueryParamSchema(schema) {
|
|
500
|
+
const target = resolveSchemaFromRef(schema) || schema;
|
|
501
|
+
if (!target) return void 0;
|
|
502
|
+
if (target.type === "array") return isArrayOfSimpleItems(target.items) ? schema : void 0;
|
|
503
|
+
return isSimpleSchema(schema) ? schema : void 0;
|
|
504
|
+
}
|
|
505
|
+
function isArrayOfSimpleItems(items) {
|
|
506
|
+
if (!items) return false;
|
|
507
|
+
if (Array.isArray(items)) {
|
|
508
|
+
if (items.length === 0) return false;
|
|
509
|
+
return items.every((entry) => isSimpleSchema(entry));
|
|
285
510
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
511
|
+
return isSimpleSchema(items);
|
|
512
|
+
}
|
|
513
|
+
function isSimpleSchema(schema, seen = /* @__PURE__ */ new Set()) {
|
|
514
|
+
if (!schema) return false;
|
|
515
|
+
if (seen.has(schema)) return false;
|
|
516
|
+
seen.add(schema);
|
|
517
|
+
if (schema.$ref) {
|
|
518
|
+
const resolved = resolveSchemaFromRef(schema);
|
|
519
|
+
if (!resolved) return false;
|
|
520
|
+
return isSimpleSchema(resolved, seen);
|
|
291
521
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
522
|
+
if (typeof schema.type === "string" && SIMPLE_QUERY_TYPES.has(schema.type)) return true;
|
|
523
|
+
if (Array.isArray(schema.enum) && schema.enum.length > 0) return true;
|
|
524
|
+
if (schema.const !== void 0) return true;
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
527
|
+
function resetSchemaRegistry() {
|
|
528
|
+
schemaRefs = /* @__PURE__ */ new WeakMap();
|
|
529
|
+
nameToType.clear();
|
|
530
|
+
for (const key of Object.keys(globalSchemas)) delete globalSchemas[key];
|
|
531
|
+
}
|
|
532
|
+
function ensureSchema(type) {
|
|
533
|
+
if (type === void 0 || type === null) return void 0;
|
|
534
|
+
const resolution = createSchemaResolution(type);
|
|
535
|
+
if (!resolution) return void 0;
|
|
536
|
+
if (resolution.kind === "inline") return {
|
|
537
|
+
schema: cloneSchema(resolution.schema),
|
|
538
|
+
isComponent: false
|
|
539
|
+
};
|
|
540
|
+
const schemaClone = cloneSchema(resolution.schema);
|
|
541
|
+
const componentName = ensureComponentName(resolution.typeRef, schemaClone, resolution.suggestedName);
|
|
542
|
+
return {
|
|
543
|
+
schema: cloneSchema(globalSchemas[componentName]),
|
|
544
|
+
ref: `#/components/schemas/${componentName}`,
|
|
545
|
+
componentName,
|
|
546
|
+
isComponent: true
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
function createSchemaResolution(type) {
|
|
550
|
+
if (type === void 0 || type === null) return void 0;
|
|
551
|
+
if (isSwaggerSchema(type)) return {
|
|
552
|
+
kind: "inline",
|
|
553
|
+
schema: cloneSchema(type)
|
|
554
|
+
};
|
|
555
|
+
if (Array.isArray(type)) {
|
|
556
|
+
if (type.length === 1) {
|
|
557
|
+
const itemEnsured = ensureSchema(type[0]);
|
|
558
|
+
const itemsSchema = toSchemaOrRef(itemEnsured);
|
|
559
|
+
return {
|
|
560
|
+
kind: "inline",
|
|
561
|
+
schema: {
|
|
562
|
+
type: "array",
|
|
563
|
+
items: itemsSchema
|
|
564
|
+
}
|
|
565
|
+
};
|
|
296
566
|
}
|
|
567
|
+
return {
|
|
568
|
+
kind: "inline",
|
|
569
|
+
schema: { type: "array" }
|
|
570
|
+
};
|
|
297
571
|
}
|
|
298
|
-
if (
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
case "ZodBigInt": {
|
|
310
|
-
schema.type = "integer";
|
|
311
|
-
break;
|
|
312
|
-
}
|
|
313
|
-
case "ZodBoolean": {
|
|
314
|
-
schema.type = "boolean";
|
|
315
|
-
break;
|
|
316
|
-
}
|
|
317
|
-
case "ZodLiteral": {
|
|
318
|
-
asLiteral();
|
|
319
|
-
break;
|
|
320
|
-
}
|
|
321
|
-
case "ZodEnum": {
|
|
322
|
-
asEnum();
|
|
323
|
-
break;
|
|
324
|
-
}
|
|
325
|
-
case "ZodNativeEnum": {
|
|
326
|
-
asNativeEnum();
|
|
327
|
-
break;
|
|
328
|
-
}
|
|
329
|
-
case "ZodDate": {
|
|
330
|
-
schema.type = "string";
|
|
331
|
-
break;
|
|
332
|
-
}
|
|
333
|
-
case "ZodNull": {
|
|
334
|
-
schema.type = "null";
|
|
335
|
-
break;
|
|
336
|
-
}
|
|
337
|
-
default: return void 0;
|
|
572
|
+
if (isLiteralValue(type)) return {
|
|
573
|
+
kind: "inline",
|
|
574
|
+
schema: schemaFromLiteral(type)
|
|
575
|
+
};
|
|
576
|
+
if (isPrimitiveConstructor(type)) return {
|
|
577
|
+
kind: "inline",
|
|
578
|
+
schema: schemaFromPrimitiveCtor(type)
|
|
579
|
+
};
|
|
580
|
+
if (typeof type === "function") {
|
|
581
|
+
const resolution = schemaFromFunction(type);
|
|
582
|
+
if (resolution) return resolution;
|
|
338
583
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
584
|
+
if (typeof type === "object") {
|
|
585
|
+
const resolution = schemaFromInstance(type);
|
|
586
|
+
if (resolution) return resolution;
|
|
587
|
+
}
|
|
588
|
+
return void 0;
|
|
589
|
+
}
|
|
590
|
+
function schemaFromFunction(fn) {
|
|
591
|
+
const ctor = fn;
|
|
592
|
+
if (typeof ctor.toJsonSchema === "function") {
|
|
593
|
+
const schema = asSwaggerSchema(ctor.toJsonSchema());
|
|
594
|
+
return {
|
|
595
|
+
kind: "component",
|
|
596
|
+
schema,
|
|
597
|
+
typeRef: ctor,
|
|
598
|
+
suggestedName: ctor.name
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
if (fn.length === 0) try {
|
|
602
|
+
const result = fn();
|
|
603
|
+
if (result && result !== fn) return createSchemaResolution(result);
|
|
604
|
+
} catch {}
|
|
605
|
+
return void 0;
|
|
606
|
+
}
|
|
607
|
+
function schemaFromInstance(obj) {
|
|
608
|
+
if (isSwaggerSchema(obj)) return {
|
|
609
|
+
kind: "inline",
|
|
610
|
+
schema: cloneSchema(obj)
|
|
611
|
+
};
|
|
612
|
+
const ctor = obj.constructor;
|
|
613
|
+
if (ctor && typeof ctor.toJsonSchema === "function") {
|
|
614
|
+
const schema = asSwaggerSchema(ctor.toJsonSchema());
|
|
615
|
+
return {
|
|
616
|
+
kind: "component",
|
|
617
|
+
schema,
|
|
618
|
+
typeRef: ctor,
|
|
619
|
+
suggestedName: ctor.name
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
if (typeof obj.toJsonSchema === "function") {
|
|
623
|
+
const schema = asSwaggerSchema(obj.toJsonSchema());
|
|
624
|
+
return {
|
|
625
|
+
kind: "component",
|
|
626
|
+
schema,
|
|
627
|
+
typeRef: obj,
|
|
628
|
+
suggestedName: getTypeName(obj)
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
return void 0;
|
|
632
|
+
}
|
|
633
|
+
function asSwaggerSchema(schema) {
|
|
634
|
+
if (!schema || typeof schema !== "object") return {};
|
|
635
|
+
return cloneSchema(schema);
|
|
636
|
+
}
|
|
637
|
+
function ensureComponentName(typeRef, schema, suggestedName) {
|
|
638
|
+
const existing = schemaRefs.get(typeRef);
|
|
639
|
+
if (existing) {
|
|
640
|
+
if (!globalSchemas[existing]) globalSchemas[existing] = cloneSchema(schema);
|
|
641
|
+
return existing;
|
|
642
|
+
}
|
|
643
|
+
const baseName = sanitizeComponentName(suggestedName || schema.title || getTypeName(typeRef) || "Schema");
|
|
644
|
+
let candidate = baseName || "Schema";
|
|
645
|
+
let counter = 1;
|
|
646
|
+
while (nameToType.has(candidate)) candidate = `${baseName}_${counter++}`;
|
|
647
|
+
nameToType.set(candidate, typeRef);
|
|
648
|
+
schemaRefs.set(typeRef, candidate);
|
|
649
|
+
applySwaggerMetadata(typeRef, schema);
|
|
650
|
+
globalSchemas[candidate] = cloneSchema(schema);
|
|
651
|
+
hoistDefs(globalSchemas[candidate]);
|
|
652
|
+
return candidate;
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* When a schema has `$defs`, hoist each definition into `globalSchemas`
|
|
656
|
+
* (i.e. `#/components/schemas/`) and rewrite all `#/$defs/X` references
|
|
657
|
+
* throughout the schema tree to `#/components/schemas/X`.
|
|
658
|
+
* Removes the `$defs` property from the schema after hoisting.
|
|
659
|
+
*/ function hoistDefs(schema) {
|
|
660
|
+
if (!schema.$defs) return;
|
|
661
|
+
for (const [name, def] of Object.entries(schema.$defs)) if (!globalSchemas[name]) {
|
|
662
|
+
globalSchemas[name] = cloneSchema(def);
|
|
663
|
+
hoistDefs(globalSchemas[name]);
|
|
664
|
+
}
|
|
665
|
+
delete schema.$defs;
|
|
666
|
+
rewriteDefsRefs(schema);
|
|
667
|
+
}
|
|
668
|
+
/** Recursively rewrite `$ref: '#/$defs/X'` → `$ref: '#/components/schemas/X'` */ function rewriteDefsRefs(schema) {
|
|
669
|
+
if (schema.$ref?.startsWith("#/$defs/")) schema.$ref = schema.$ref.replace("#/$defs/", "#/components/schemas/");
|
|
670
|
+
if (schema.discriminator?.mapping) {
|
|
671
|
+
for (const [key, value] of Object.entries(schema.discriminator.mapping)) if (value.startsWith("#/$defs/")) schema.discriminator.mapping[key] = value.replace("#/$defs/", "#/components/schemas/");
|
|
672
|
+
}
|
|
673
|
+
if (schema.items && !Array.isArray(schema.items)) rewriteDefsRefs(schema.items);
|
|
674
|
+
if (schema.properties) for (const prop of Object.values(schema.properties)) rewriteDefsRefs(prop);
|
|
675
|
+
for (const list of [
|
|
676
|
+
schema.allOf,
|
|
677
|
+
schema.anyOf,
|
|
678
|
+
schema.oneOf
|
|
679
|
+
]) if (list) for (const item of list) rewriteDefsRefs(item);
|
|
680
|
+
if (typeof schema.additionalProperties === "object" && schema.additionalProperties) rewriteDefsRefs(schema.additionalProperties);
|
|
681
|
+
if (schema.not) rewriteDefsRefs(schema.not);
|
|
682
|
+
}
|
|
683
|
+
function applySwaggerMetadata(typeRef, schema) {
|
|
684
|
+
try {
|
|
685
|
+
const mate = getSwaggerMate();
|
|
686
|
+
const meta = mate.read(typeRef);
|
|
687
|
+
if (meta) {
|
|
688
|
+
if (meta.swaggerExample !== void 0 && schema.example === void 0) schema.example = meta.swaggerExample;
|
|
689
|
+
const title = meta.label || meta.id;
|
|
690
|
+
if (title && !schema.title) schema.title = title;
|
|
691
|
+
if (meta.swaggerDescription && !schema.description) schema.description = meta.swaggerDescription;
|
|
692
|
+
else if (meta.description && !schema.description) schema.description = meta.description;
|
|
426
693
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
const checks = parsed.$checks;
|
|
434
|
-
if (parsed.$type === "ZodString") {
|
|
435
|
-
if (typeof checks.min === "number") schema.minLength = checks.min;
|
|
436
|
-
if (typeof checks.max === "number") schema.maxLength = checks.max;
|
|
437
|
-
} else {
|
|
438
|
-
if (typeof checks.min === "number") schema.minimum = checks.min;
|
|
439
|
-
if (typeof checks.max === "number") schema.maximum = checks.max;
|
|
694
|
+
} catch {}
|
|
695
|
+
if (schema.example === void 0) {
|
|
696
|
+
const exampleFn = typeRef.toExampleData;
|
|
697
|
+
if (typeof exampleFn === "function") {
|
|
698
|
+
const example = exampleFn.call(typeRef);
|
|
699
|
+
if (example !== void 0) schema.example = example;
|
|
440
700
|
}
|
|
441
701
|
}
|
|
442
|
-
if (!forParam && zodType.__type_ref) return { $ref: `#/components/schemas/${zodType.__type_ref.name}` };
|
|
443
|
-
return schema;
|
|
444
702
|
}
|
|
445
|
-
function
|
|
446
|
-
|
|
703
|
+
function sanitizeComponentName(name) {
|
|
704
|
+
const sanitized = name.replaceAll(/[^A-Za-z0-9_.-]/g, "_");
|
|
705
|
+
return sanitized || "Schema";
|
|
706
|
+
}
|
|
707
|
+
function getTypeName(typeRef) {
|
|
708
|
+
if (typeof typeRef === "function" && typeRef.name) return typeRef.name;
|
|
709
|
+
const ctor = typeRef.constructor;
|
|
710
|
+
if (ctor && ctor !== Object && ctor.name) return ctor.name;
|
|
711
|
+
return void 0;
|
|
712
|
+
}
|
|
713
|
+
function isSwaggerSchema(candidate) {
|
|
714
|
+
if (!candidate || typeof candidate !== "object") return false;
|
|
715
|
+
const obj = candidate;
|
|
716
|
+
return "$ref" in obj || "type" in obj || "properties" in obj || "items" in obj || "allOf" in obj || "anyOf" in obj || "oneOf" in obj;
|
|
717
|
+
}
|
|
718
|
+
function isLiteralValue(value) {
|
|
719
|
+
const type = typeof value;
|
|
720
|
+
return type === "string" || type === "number" || type === "boolean" || type === "bigint";
|
|
721
|
+
}
|
|
722
|
+
function schemaFromLiteral(value) {
|
|
723
|
+
if (typeof value === "string") {
|
|
724
|
+
if ([
|
|
725
|
+
"string",
|
|
726
|
+
"number",
|
|
727
|
+
"boolean",
|
|
728
|
+
"integer",
|
|
729
|
+
"object",
|
|
730
|
+
"array"
|
|
731
|
+
].includes(value)) return { type: value };
|
|
732
|
+
return {
|
|
733
|
+
const: value,
|
|
734
|
+
type: "string"
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
if (typeof value === "number") return {
|
|
738
|
+
const: value,
|
|
739
|
+
type: Number.isInteger(value) ? "integer" : "number"
|
|
740
|
+
};
|
|
741
|
+
if (typeof value === "boolean") return {
|
|
742
|
+
const: value,
|
|
743
|
+
type: "boolean"
|
|
744
|
+
};
|
|
745
|
+
return {
|
|
746
|
+
const: value.toString(),
|
|
747
|
+
type: "integer"
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
function isPrimitiveConstructor(value) {
|
|
751
|
+
if (typeof value !== "function") return false;
|
|
752
|
+
return value === String || value === Number || value === Boolean || value === BigInt || value === Date || value === Array || value === Object || value === Symbol;
|
|
753
|
+
}
|
|
754
|
+
function schemaFromPrimitiveCtor(fn) {
|
|
755
|
+
switch (fn) {
|
|
756
|
+
case String: return { type: "string" };
|
|
757
|
+
case Number: return { type: "number" };
|
|
758
|
+
case Boolean: return { type: "boolean" };
|
|
759
|
+
case BigInt: return { type: "integer" };
|
|
760
|
+
case Date: return {
|
|
761
|
+
type: "string",
|
|
762
|
+
format: "date-time"
|
|
763
|
+
};
|
|
764
|
+
case Array: return { type: "array" };
|
|
765
|
+
case Object: return { type: "object" };
|
|
766
|
+
case Symbol: return { type: "string" };
|
|
767
|
+
default: return { type: "object" };
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
function resolveSchemaFromRef(schema) {
|
|
771
|
+
if (!schema.$ref) return schema;
|
|
772
|
+
const refName = schema.$ref.replace("#/components/schemas/", "");
|
|
773
|
+
return globalSchemas[refName];
|
|
774
|
+
}
|
|
775
|
+
function cloneSchema(schema) {
|
|
776
|
+
return JSON.parse(JSON.stringify(schema));
|
|
447
777
|
}
|
|
448
778
|
function getDefaultStatusCode(httpMethod) {
|
|
449
779
|
const defaultStatusCodes = {
|
|
@@ -454,22 +784,106 @@ function getDefaultStatusCode(httpMethod) {
|
|
|
454
784
|
};
|
|
455
785
|
return defaultStatusCodes[httpMethod.toUpperCase()] || 200;
|
|
456
786
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
787
|
+
const STATUS_DESCRIPTIONS = {
|
|
788
|
+
200: "OK",
|
|
789
|
+
201: "Created",
|
|
790
|
+
202: "Accepted",
|
|
791
|
+
204: "No Content",
|
|
792
|
+
301: "Moved Permanently",
|
|
793
|
+
302: "Found",
|
|
794
|
+
304: "Not Modified",
|
|
795
|
+
400: "Bad Request",
|
|
796
|
+
401: "Unauthorized",
|
|
797
|
+
403: "Forbidden",
|
|
798
|
+
404: "Not Found",
|
|
799
|
+
405: "Method Not Allowed",
|
|
800
|
+
409: "Conflict",
|
|
801
|
+
410: "Gone",
|
|
802
|
+
415: "Unsupported Media Type",
|
|
803
|
+
422: "Unprocessable Entity",
|
|
804
|
+
429: "Too Many Requests",
|
|
805
|
+
500: "Internal Server Error",
|
|
806
|
+
502: "Bad Gateway",
|
|
807
|
+
503: "Service Unavailable"
|
|
808
|
+
};
|
|
809
|
+
function defaultStatusDescription(code) {
|
|
810
|
+
return STATUS_DESCRIPTIONS[code] || "Response";
|
|
811
|
+
}
|
|
812
|
+
function transformSpecTo31(spec) {
|
|
813
|
+
for (const schema of Object.values(spec.components.schemas)) convertSchemaTo31(schema);
|
|
814
|
+
for (const methods of Object.values(spec.paths)) for (const endpoint of Object.values(methods)) {
|
|
815
|
+
for (const param of endpoint.parameters) convertSchemaTo31(param.schema);
|
|
816
|
+
if (endpoint.responses) for (const response of Object.values(endpoint.responses)) {
|
|
817
|
+
for (const media of Object.values(response.content)) convertSchemaTo31(media.schema);
|
|
818
|
+
if (response.headers) for (const header of Object.values(response.headers)) convertSchemaTo31(header.schema);
|
|
819
|
+
}
|
|
820
|
+
if (endpoint.requestBody) for (const media of Object.values(endpoint.requestBody.content)) convertSchemaTo31(media.schema);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
function convertSchemaTo31(schema) {
|
|
824
|
+
if (schema.nullable) {
|
|
825
|
+
delete schema.nullable;
|
|
826
|
+
if (typeof schema.type === "string") schema.type = [schema.type, "null"];
|
|
827
|
+
else if (Array.isArray(schema.type)) {
|
|
828
|
+
if (!schema.type.includes("null")) schema.type.push("null");
|
|
829
|
+
} else schema.type = "null";
|
|
830
|
+
}
|
|
831
|
+
if (schema.properties) for (const prop of Object.values(schema.properties)) convertSchemaTo31(prop);
|
|
832
|
+
if (schema.items) if (Array.isArray(schema.items)) for (const item of schema.items) convertSchemaTo31(item);
|
|
833
|
+
else convertSchemaTo31(schema.items);
|
|
834
|
+
if (schema.allOf) for (const sub of schema.allOf) convertSchemaTo31(sub);
|
|
835
|
+
if (schema.anyOf) for (const sub of schema.anyOf) convertSchemaTo31(sub);
|
|
836
|
+
if (schema.oneOf) for (const sub of schema.oneOf) convertSchemaTo31(sub);
|
|
837
|
+
if (schema.not) convertSchemaTo31(schema.not);
|
|
838
|
+
if (typeof schema.additionalProperties === "object" && schema.additionalProperties) convertSchemaTo31(schema.additionalProperties);
|
|
839
|
+
}
|
|
840
|
+
function collectSchemesFromTransports(transports, schemes) {
|
|
841
|
+
if (transports.bearer) schemes.bearerAuth = {
|
|
842
|
+
type: "http",
|
|
843
|
+
scheme: "bearer",
|
|
844
|
+
...transports.bearer.format ? { bearerFormat: transports.bearer.format } : {},
|
|
845
|
+
...transports.bearer.description ? { description: transports.bearer.description } : {}
|
|
846
|
+
};
|
|
847
|
+
if (transports.basic) schemes.basicAuth = {
|
|
848
|
+
type: "http",
|
|
849
|
+
scheme: "basic",
|
|
850
|
+
...transports.basic.description ? { description: transports.basic.description } : {}
|
|
851
|
+
};
|
|
852
|
+
if (transports.apiKey) schemes.apiKeyAuth = {
|
|
853
|
+
type: "apiKey",
|
|
854
|
+
name: transports.apiKey.name,
|
|
855
|
+
in: transports.apiKey.in,
|
|
856
|
+
...transports.apiKey.description ? { description: transports.apiKey.description } : {}
|
|
857
|
+
};
|
|
858
|
+
if (transports.cookie) schemes.cookieAuth = {
|
|
859
|
+
type: "apiKey",
|
|
860
|
+
name: transports.cookie.name,
|
|
861
|
+
in: "cookie",
|
|
862
|
+
...transports.cookie.description ? { description: transports.cookie.description } : {}
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
function transportsToSecurityRequirement(transports) {
|
|
866
|
+
const requirements = [];
|
|
867
|
+
if (transports.bearer) requirements.push({ bearerAuth: [] });
|
|
868
|
+
if (transports.basic) requirements.push({ basicAuth: [] });
|
|
869
|
+
if (transports.apiKey) requirements.push({ apiKeyAuth: [] });
|
|
870
|
+
if (transports.cookie) requirements.push({ cookieAuth: [] });
|
|
871
|
+
return requirements;
|
|
872
|
+
}
|
|
873
|
+
function resolveOperationSecurity(cmeta, hmeta, schemes) {
|
|
874
|
+
if (hmeta?.swaggerPublic) return [];
|
|
875
|
+
if (hmeta?.swaggerSecurity?.length) return hmeta.swaggerSecurity;
|
|
876
|
+
if (hmeta?.authTransports) {
|
|
877
|
+
collectSchemesFromTransports(hmeta.authTransports, schemes);
|
|
878
|
+
return transportsToSecurityRequirement(hmeta.authTransports);
|
|
879
|
+
}
|
|
880
|
+
if (cmeta?.swaggerPublic) return [];
|
|
881
|
+
if (cmeta?.swaggerSecurity?.length) return cmeta.swaggerSecurity;
|
|
882
|
+
if (cmeta?.authTransports) {
|
|
883
|
+
collectSchemesFromTransports(cmeta.authTransports, schemes);
|
|
884
|
+
return transportsToSecurityRequirement(cmeta.authTransports);
|
|
885
|
+
}
|
|
886
|
+
return void 0;
|
|
473
887
|
}
|
|
474
888
|
|
|
475
889
|
//#endregion
|
|
@@ -499,13 +913,10 @@ function _ts_param(paramIndex, decorator) {
|
|
|
499
913
|
};
|
|
500
914
|
}
|
|
501
915
|
var SwaggerController = class {
|
|
502
|
-
|
|
503
|
-
if (this.opts.cors)
|
|
504
|
-
const { enableCors } = useSetHeaders();
|
|
505
|
-
enableCors(this.opts.cors === true ? void 0 : this.opts.cors);
|
|
506
|
-
}
|
|
916
|
+
processCors() {
|
|
917
|
+
if (this.opts.cors) useResponse().enableCors(this.opts.cors === true ? void 0 : this.opts.cors);
|
|
507
918
|
}
|
|
508
|
-
|
|
919
|
+
serveIndex(url, location, status) {
|
|
509
920
|
this.processCors();
|
|
510
921
|
if (!url.endsWith("index.html") && !url.endsWith("/")) {
|
|
511
922
|
status.value = 302;
|
|
@@ -549,21 +960,30 @@ var SwaggerController = class {
|
|
|
549
960
|
});
|
|
550
961
|
};`;
|
|
551
962
|
}
|
|
552
|
-
async
|
|
553
|
-
this.processCors();
|
|
554
|
-
const logger = useEventLogger("@moostjs/zod");
|
|
963
|
+
async resolveSpec() {
|
|
555
964
|
if (!this.spec) {
|
|
556
|
-
const
|
|
965
|
+
const ctx = current();
|
|
966
|
+
const l = useLogger(ctx);
|
|
967
|
+
const logger = typeof l.topic === "function" ? l.topic("@moostjs/swagger") : l;
|
|
968
|
+
const { instantiate } = useControllerContext(ctx);
|
|
557
969
|
const moost = await instantiate(Moost);
|
|
558
970
|
this.spec = mapToSwaggerSpec(moost.getControllersOverview(), this.opts, logger);
|
|
559
971
|
}
|
|
560
972
|
return this.spec;
|
|
561
973
|
}
|
|
562
|
-
"
|
|
974
|
+
async "spec.json"() {
|
|
975
|
+
this.processCors();
|
|
976
|
+
return this.resolveSpec();
|
|
977
|
+
}
|
|
978
|
+
async "spec.yaml"() {
|
|
979
|
+
this.processCors();
|
|
980
|
+
return jsonToYaml(await this.resolveSpec());
|
|
981
|
+
}
|
|
982
|
+
files(url) {
|
|
563
983
|
this.processCors();
|
|
564
984
|
return this.serve(url.split("/").pop());
|
|
565
985
|
}
|
|
566
|
-
|
|
986
|
+
serve(path) {
|
|
567
987
|
return serveFile(path, {
|
|
568
988
|
baseDir: this.assetPath,
|
|
569
989
|
cacheControl: {
|
|
@@ -609,6 +1029,13 @@ _ts_decorate([
|
|
|
609
1029
|
_ts_metadata("design:paramtypes", []),
|
|
610
1030
|
_ts_metadata("design:returntype", Promise)
|
|
611
1031
|
], SwaggerController.prototype, "spec.json", null);
|
|
1032
|
+
_ts_decorate([
|
|
1033
|
+
Get(),
|
|
1034
|
+
SetHeader("content-type", "text/yaml"),
|
|
1035
|
+
_ts_metadata("design:type", Function),
|
|
1036
|
+
_ts_metadata("design:paramtypes", []),
|
|
1037
|
+
_ts_metadata("design:returntype", Promise)
|
|
1038
|
+
], SwaggerController.prototype, "spec.yaml", null);
|
|
612
1039
|
_ts_decorate([
|
|
613
1040
|
Get("swagger-ui-bundle.*(js|js\\.map)"),
|
|
614
1041
|
Get("swagger-ui-standalone-preset.*(js|js\\.map)"),
|
|
@@ -621,7 +1048,6 @@ _ts_decorate([
|
|
|
621
1048
|
], SwaggerController.prototype, "files", null);
|
|
622
1049
|
SwaggerController = _ts_decorate([
|
|
623
1050
|
SwaggerExclude(),
|
|
624
|
-
ZodSkip(),
|
|
625
1051
|
Controller("api-docs"),
|
|
626
1052
|
_ts_param(0, Const({ title: "Moost API" })),
|
|
627
1053
|
_ts_metadata("design:type", Function),
|
|
@@ -629,4 +1055,4 @@ SwaggerController = _ts_decorate([
|
|
|
629
1055
|
], SwaggerController);
|
|
630
1056
|
|
|
631
1057
|
//#endregion
|
|
632
|
-
export { SwaggerController, SwaggerDescription, SwaggerExample, SwaggerExclude, SwaggerParam, SwaggerRequestBody, SwaggerResponse, SwaggerTag, getSwaggerMate };
|
|
1058
|
+
export { SwaggerCallback, SwaggerController, SwaggerDeprecated, SwaggerDescription, SwaggerExample, SwaggerExclude, SwaggerExternalDocs, SwaggerLink, SwaggerOperationId, SwaggerParam, SwaggerPublic, SwaggerRequestBody, SwaggerResponse, SwaggerSecurity, SwaggerSecurityAll, SwaggerTag, getSwaggerMate };
|