@moostjs/swagger 0.5.33 → 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 +451 -43
- package/dist/index.d.ts +286 -19
- package/dist/index.mjs +445 -45
- package/package.json +32 -30
package/dist/index.mjs
CHANGED
|
@@ -1,39 +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 {
|
|
3
|
+
import { useResponse } from "@wooksjs/event-http";
|
|
4
4
|
import { serveFile } from "@wooksjs/http-static";
|
|
5
5
|
import Path from "path";
|
|
6
6
|
import { getAbsoluteFSPath } from "swagger-ui-dist";
|
|
7
7
|
|
|
8
8
|
//#region packages/swagger/src/swagger.mate.ts
|
|
9
|
-
function getSwaggerMate() {
|
|
9
|
+
/** Returns the shared `Mate` instance extended with Swagger/OpenAPI metadata fields. */ function getSwaggerMate() {
|
|
10
10
|
return getMoostMate();
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
//#endregion
|
|
14
14
|
//#region packages/swagger/src/decorators.ts
|
|
15
|
-
const SwaggerTag = (tag) => getSwaggerMate().decorate("swaggerTags", tag, true);
|
|
16
|
-
const SwaggerExclude = () => getSwaggerMate().decorate("swaggerExclude", true);
|
|
17
|
-
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);
|
|
18
18
|
function SwaggerResponse(code, opts, example) {
|
|
19
19
|
return getSwaggerMate().decorate((meta) => {
|
|
20
20
|
let ex;
|
|
21
21
|
if (example) ex = example;
|
|
22
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
|
+
}
|
|
23
27
|
meta.swaggerResponses = meta.swaggerResponses || {};
|
|
24
28
|
const keyCode = typeof code === "number" ? code : 0;
|
|
25
29
|
const opt = typeof code === "number" ? opts : code;
|
|
26
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;
|
|
27
33
|
const response = ["object", "function"].includes(typeof opt.response) ? opt.response : opt;
|
|
28
|
-
meta.swaggerResponses[keyCode] = meta.swaggerResponses[keyCode] || {};
|
|
29
|
-
meta.swaggerResponses[keyCode][contentType] = {
|
|
34
|
+
meta.swaggerResponses[keyCode] = meta.swaggerResponses[keyCode] || { content: {} };
|
|
35
|
+
meta.swaggerResponses[keyCode].content[contentType] = {
|
|
30
36
|
response,
|
|
31
37
|
example: ex
|
|
32
38
|
};
|
|
39
|
+
if (description) meta.swaggerResponses[keyCode].description = description;
|
|
40
|
+
if (headers) meta.swaggerResponses[keyCode].headers = {
|
|
41
|
+
...meta.swaggerResponses[keyCode].headers,
|
|
42
|
+
...headers
|
|
43
|
+
};
|
|
33
44
|
return meta;
|
|
34
45
|
});
|
|
35
46
|
}
|
|
36
|
-
|
|
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) {
|
|
37
53
|
return getSwaggerMate().decorate((meta) => {
|
|
38
54
|
meta.swaggerRequestBody = meta.swaggerRequestBody || {};
|
|
39
55
|
const contentType = typeof opt.contentType === "string" ? opt.contentType : "application/json";
|
|
@@ -42,12 +58,127 @@ function SwaggerRequestBody(opt) {
|
|
|
42
58
|
return meta;
|
|
43
59
|
});
|
|
44
60
|
}
|
|
45
|
-
function SwaggerParam(opts) {
|
|
61
|
+
/** Defines a parameter (query, path, header, cookie) in the OpenAPI spec. */ function SwaggerParam(opts) {
|
|
46
62
|
return getSwaggerMate().decorate("swaggerParams", opts, true);
|
|
47
63
|
}
|
|
48
|
-
function SwaggerExample(example) {
|
|
64
|
+
/** Attaches an example value to a handler's OpenAPI documentation. */ function SwaggerExample(example) {
|
|
49
65
|
return getSwaggerMate().decorate("swaggerExample", example);
|
|
50
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
|
+
}
|
|
51
182
|
|
|
52
183
|
//#endregion
|
|
53
184
|
//#region packages/swagger/src/mapping.ts
|
|
@@ -56,16 +187,26 @@ let schemaRefs = /* @__PURE__ */ new WeakMap();
|
|
|
56
187
|
const nameToType = /* @__PURE__ */ new Map();
|
|
57
188
|
function mapToSwaggerSpec(metadata, options, logger) {
|
|
58
189
|
resetSchemaRegistry();
|
|
190
|
+
const is31 = options?.openapiVersion === "3.1";
|
|
191
|
+
const collectedSecuritySchemes = { ...options?.securitySchemes };
|
|
59
192
|
const swaggerSpec = {
|
|
60
|
-
openapi: "3.0.0",
|
|
193
|
+
openapi: is31 ? "3.1.0" : "3.0.0",
|
|
61
194
|
info: {
|
|
62
195
|
title: options?.title || "API Documentation",
|
|
63
|
-
|
|
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 } : {}
|
|
64
201
|
},
|
|
65
202
|
paths: {},
|
|
66
203
|
tags: [],
|
|
204
|
+
...options?.servers?.length ? { servers: options.servers } : {},
|
|
205
|
+
...options?.externalDocs ? { externalDocs: options.externalDocs } : {},
|
|
206
|
+
...options?.security ? { security: options.security } : {},
|
|
67
207
|
components: { schemas: globalSchemas }
|
|
68
208
|
};
|
|
209
|
+
const deferredLinks = [];
|
|
69
210
|
for (const controller of metadata) {
|
|
70
211
|
const cmeta = controller.meta;
|
|
71
212
|
if (cmeta?.swaggerExclude) continue;
|
|
@@ -77,13 +218,14 @@ function mapToSwaggerSpec(metadata, options, logger) {
|
|
|
77
218
|
const uniqueParams = {};
|
|
78
219
|
const handlerPath = handler.registeredAs[0].path;
|
|
79
220
|
const handlerMethod = hh.method?.toLowerCase() || "get";
|
|
80
|
-
const
|
|
221
|
+
const handlerSummary = hmeta?.label;
|
|
222
|
+
const handlerDescription = hmeta?.swaggerDescription || hmeta?.description;
|
|
81
223
|
const handlerTags = [...controllerTags, ...hmeta?.swaggerTags || []];
|
|
82
224
|
if (!swaggerSpec.paths[handlerPath]) swaggerSpec.paths[handlerPath] = {};
|
|
83
225
|
let responses;
|
|
84
|
-
if (hmeta?.swaggerResponses) for (const [code,
|
|
226
|
+
if (hmeta?.swaggerResponses) for (const [code, responseEntry] of Object.entries(hmeta.swaggerResponses)) {
|
|
85
227
|
const newCode = code === "0" ? getDefaultStatusCode(handlerMethod) : code;
|
|
86
|
-
for (const [contentType, conf] of Object.entries(
|
|
228
|
+
for (const [contentType, conf] of Object.entries(responseEntry.content)) {
|
|
87
229
|
const { response, example } = conf;
|
|
88
230
|
const schema = resolveSwaggerSchemaFromConfig(response);
|
|
89
231
|
if (schema) {
|
|
@@ -92,16 +234,35 @@ function mapToSwaggerSpec(metadata, options, logger) {
|
|
|
92
234
|
...schema,
|
|
93
235
|
example
|
|
94
236
|
} : schema;
|
|
95
|
-
responses[newCode]
|
|
237
|
+
if (!responses[newCode]) responses[newCode] = {
|
|
238
|
+
description: responseEntry.description || defaultStatusDescription(Number(newCode)),
|
|
239
|
+
content: {}
|
|
240
|
+
};
|
|
241
|
+
responses[newCode].content[contentType] = { schema: schemaWithExample };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (responseEntry.headers) {
|
|
245
|
+
const resolvedHeaders = resolveResponseHeaders(responseEntry.headers);
|
|
246
|
+
if (resolvedHeaders) {
|
|
247
|
+
responses = responses || {};
|
|
248
|
+
if (!responses[newCode]) responses[newCode] = {
|
|
249
|
+
description: defaultStatusDescription(Number(newCode)),
|
|
250
|
+
content: {}
|
|
251
|
+
};
|
|
252
|
+
responses[newCode].headers = resolvedHeaders;
|
|
96
253
|
}
|
|
97
254
|
}
|
|
98
255
|
}
|
|
99
|
-
|
|
256
|
+
const defaultCode = getDefaultStatusCode(handlerMethod);
|
|
257
|
+
if (!responses?.[defaultCode] && hmeta?.returnType) {
|
|
100
258
|
const ensured = ensureSchema(hmeta.returnType);
|
|
101
259
|
const schema = toSchemaOrRef(ensured);
|
|
102
260
|
if (schema) {
|
|
103
261
|
responses = responses || {};
|
|
104
|
-
responses[
|
|
262
|
+
responses[defaultCode] = {
|
|
263
|
+
description: defaultStatusDescription(Number(defaultCode)),
|
|
264
|
+
content: { "*/*": { schema } }
|
|
265
|
+
};
|
|
105
266
|
}
|
|
106
267
|
}
|
|
107
268
|
let reqBodyRequired = true;
|
|
@@ -111,13 +272,39 @@ function mapToSwaggerSpec(metadata, options, logger) {
|
|
|
111
272
|
if (schema) bodyContent[contentType] = { schema };
|
|
112
273
|
}
|
|
113
274
|
swaggerSpec.paths[handlerPath][handlerMethod] = {
|
|
114
|
-
summary:
|
|
115
|
-
|
|
275
|
+
summary: handlerSummary,
|
|
276
|
+
description: handlerDescription,
|
|
277
|
+
operationId: hmeta?.swaggerOperationId || hmeta?.id || `${handlerMethod.toUpperCase()}_${handlerPath.replaceAll(/\//g, "_").replaceAll(/[{}]/g, "__").replaceAll(/[^\dA-Za-z]/g, "_")}`,
|
|
116
278
|
tags: handlerTags,
|
|
117
279
|
parameters: [],
|
|
118
280
|
responses
|
|
119
281
|
};
|
|
120
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
|
+
}
|
|
121
308
|
function addParam(param) {
|
|
122
309
|
const key = `${param.in}//${param.name}`;
|
|
123
310
|
if (uniqueParams[key]) {
|
|
@@ -156,8 +343,7 @@ function mapToSwaggerSpec(metadata, options, logger) {
|
|
|
156
343
|
schema
|
|
157
344
|
});
|
|
158
345
|
}
|
|
159
|
-
for (
|
|
160
|
-
const paramMeta = handler.meta.params[i];
|
|
346
|
+
for (const paramMeta of handler.meta.params) {
|
|
161
347
|
if (paramMeta.paramSource && ["QUERY_ITEM", "QUERY"].includes(paramMeta.paramSource)) {
|
|
162
348
|
const ensured = ensureSchema(paramMeta.type);
|
|
163
349
|
if (paramMeta.paramSource === "QUERY_ITEM") {
|
|
@@ -209,7 +395,7 @@ function mapToSwaggerSpec(metadata, options, logger) {
|
|
|
209
395
|
}
|
|
210
396
|
}
|
|
211
397
|
const bodyEntries = Object.entries(bodyContent).filter((entry) => entry[1] && entry[1].schema !== void 0);
|
|
212
|
-
if (bodyEntries.length) {
|
|
398
|
+
if (bodyEntries.length > 0) {
|
|
213
399
|
const content = {};
|
|
214
400
|
for (const [contentType, { schema }] of bodyEntries) content[contentType] = { schema };
|
|
215
401
|
endpointSpec.requestBody = {
|
|
@@ -219,6 +405,54 @@ function mapToSwaggerSpec(metadata, options, logger) {
|
|
|
219
405
|
}
|
|
220
406
|
}
|
|
221
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: {}
|
|
440
|
+
};
|
|
441
|
+
const responseEntry = endpointSpec.responses[statusCode];
|
|
442
|
+
if (!responseEntry.links) responseEntry.links = {};
|
|
443
|
+
responseEntry.links[config.name] = link;
|
|
444
|
+
}
|
|
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);
|
|
222
456
|
return swaggerSpec;
|
|
223
457
|
}
|
|
224
458
|
function resolveSwaggerSchemaFromConfig(type) {
|
|
@@ -226,6 +460,20 @@ function resolveSwaggerSchemaFromConfig(type) {
|
|
|
226
460
|
const ensured = ensureSchema(type);
|
|
227
461
|
return toSchemaOrRef(ensured);
|
|
228
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;
|
|
474
|
+
}
|
|
475
|
+
return hasAny ? resolved : void 0;
|
|
476
|
+
}
|
|
229
477
|
function toSchemaOrRef(result) {
|
|
230
478
|
if (!result) return void 0;
|
|
231
479
|
if (result.ref) return { $ref: result.ref };
|
|
@@ -234,7 +482,7 @@ function toSchemaOrRef(result) {
|
|
|
234
482
|
function inferBodyContentType(schema, resolved) {
|
|
235
483
|
const target = resolved ?? resolveSchemaFromRef(schema);
|
|
236
484
|
const schemaType = target?.type ?? schema.type;
|
|
237
|
-
if (schemaType && [
|
|
485
|
+
if (typeof schemaType === "string" && [
|
|
238
486
|
"string",
|
|
239
487
|
"number",
|
|
240
488
|
"integer",
|
|
@@ -400,22 +648,60 @@ function ensureComponentName(typeRef, schema, suggestedName) {
|
|
|
400
648
|
schemaRefs.set(typeRef, candidate);
|
|
401
649
|
applySwaggerMetadata(typeRef, schema);
|
|
402
650
|
globalSchemas[candidate] = cloneSchema(schema);
|
|
651
|
+
hoistDefs(globalSchemas[candidate]);
|
|
403
652
|
return candidate;
|
|
404
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
|
+
}
|
|
405
683
|
function applySwaggerMetadata(typeRef, schema) {
|
|
406
684
|
try {
|
|
407
685
|
const mate = getSwaggerMate();
|
|
408
686
|
const meta = mate.read(typeRef);
|
|
409
|
-
if (
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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;
|
|
693
|
+
}
|
|
415
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;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
416
702
|
}
|
|
417
703
|
function sanitizeComponentName(name) {
|
|
418
|
-
const sanitized = name.
|
|
704
|
+
const sanitized = name.replaceAll(/[^A-Za-z0-9_.-]/g, "_");
|
|
419
705
|
return sanitized || "Schema";
|
|
420
706
|
}
|
|
421
707
|
function getTypeName(typeRef) {
|
|
@@ -498,6 +784,107 @@ function getDefaultStatusCode(httpMethod) {
|
|
|
498
784
|
};
|
|
499
785
|
return defaultStatusCodes[httpMethod.toUpperCase()] || 200;
|
|
500
786
|
}
|
|
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;
|
|
887
|
+
}
|
|
501
888
|
|
|
502
889
|
//#endregion
|
|
503
890
|
//#region packages/swagger/src/swagger.controller.ts
|
|
@@ -526,13 +913,10 @@ function _ts_param(paramIndex, decorator) {
|
|
|
526
913
|
};
|
|
527
914
|
}
|
|
528
915
|
var SwaggerController = class {
|
|
529
|
-
|
|
530
|
-
if (this.opts.cors)
|
|
531
|
-
const { enableCors } = useSetHeaders();
|
|
532
|
-
enableCors(this.opts.cors === true ? void 0 : this.opts.cors);
|
|
533
|
-
}
|
|
916
|
+
processCors() {
|
|
917
|
+
if (this.opts.cors) useResponse().enableCors(this.opts.cors === true ? void 0 : this.opts.cors);
|
|
534
918
|
}
|
|
535
|
-
|
|
919
|
+
serveIndex(url, location, status) {
|
|
536
920
|
this.processCors();
|
|
537
921
|
if (!url.endsWith("index.html") && !url.endsWith("/")) {
|
|
538
922
|
status.value = 302;
|
|
@@ -576,21 +960,30 @@ var SwaggerController = class {
|
|
|
576
960
|
});
|
|
577
961
|
};`;
|
|
578
962
|
}
|
|
579
|
-
async
|
|
580
|
-
this.processCors();
|
|
581
|
-
const logger = useEventLogger("@moostjs/zod");
|
|
963
|
+
async resolveSpec() {
|
|
582
964
|
if (!this.spec) {
|
|
583
|
-
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);
|
|
584
969
|
const moost = await instantiate(Moost);
|
|
585
970
|
this.spec = mapToSwaggerSpec(moost.getControllersOverview(), this.opts, logger);
|
|
586
971
|
}
|
|
587
972
|
return this.spec;
|
|
588
973
|
}
|
|
589
|
-
"
|
|
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) {
|
|
590
983
|
this.processCors();
|
|
591
984
|
return this.serve(url.split("/").pop());
|
|
592
985
|
}
|
|
593
|
-
|
|
986
|
+
serve(path) {
|
|
594
987
|
return serveFile(path, {
|
|
595
988
|
baseDir: this.assetPath,
|
|
596
989
|
cacheControl: {
|
|
@@ -636,6 +1029,13 @@ _ts_decorate([
|
|
|
636
1029
|
_ts_metadata("design:paramtypes", []),
|
|
637
1030
|
_ts_metadata("design:returntype", Promise)
|
|
638
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);
|
|
639
1039
|
_ts_decorate([
|
|
640
1040
|
Get("swagger-ui-bundle.*(js|js\\.map)"),
|
|
641
1041
|
Get("swagger-ui-standalone-preset.*(js|js\\.map)"),
|
|
@@ -655,4 +1055,4 @@ SwaggerController = _ts_decorate([
|
|
|
655
1055
|
], SwaggerController);
|
|
656
1056
|
|
|
657
1057
|
//#endregion
|
|
658
|
-
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 };
|