@dangao/bun-server 1.0.0 → 1.0.3
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/package.json +4 -2
- package/readme.md +163 -2
- package/src/auth/controller.ts +148 -0
- package/src/auth/decorators.ts +81 -0
- package/src/auth/index.ts +12 -0
- package/src/auth/jwt.ts +169 -0
- package/src/auth/oauth2.ts +244 -0
- package/src/auth/types.ts +248 -0
- package/src/cache/cache-module.ts +67 -0
- package/src/cache/decorators.ts +202 -0
- package/src/cache/index.ts +27 -0
- package/src/cache/service.ts +151 -0
- package/src/cache/types.ts +420 -0
- package/src/config/config-module.ts +76 -0
- package/src/config/index.ts +8 -0
- package/src/config/service.ts +93 -0
- package/src/config/types.ts +27 -0
- package/src/controller/controller.ts +251 -0
- package/src/controller/decorators.ts +84 -0
- package/src/controller/index.ts +7 -0
- package/src/controller/metadata.ts +27 -0
- package/src/controller/param-binder.ts +157 -0
- package/src/core/application.ts +233 -0
- package/src/core/context.ts +228 -0
- package/src/core/index.ts +4 -0
- package/src/core/server.ts +128 -0
- package/src/core/types.ts +2 -0
- package/src/database/connection-manager.ts +239 -0
- package/src/database/connection-pool.ts +322 -0
- package/src/database/database-extension.ts +62 -0
- package/src/database/database-module.ts +115 -0
- package/src/database/health-indicator.ts +51 -0
- package/src/database/index.ts +47 -0
- package/src/database/orm/decorators.ts +155 -0
- package/src/database/orm/drizzle-repository.ts +39 -0
- package/src/database/orm/index.ts +23 -0
- package/src/database/orm/repository-decorator.ts +39 -0
- package/src/database/orm/repository.ts +103 -0
- package/src/database/orm/service.ts +49 -0
- package/src/database/orm/transaction-decorator.ts +45 -0
- package/src/database/orm/transaction-interceptor.ts +243 -0
- package/src/database/orm/transaction-manager.ts +276 -0
- package/src/database/orm/transaction-types.ts +140 -0
- package/src/database/orm/types.ts +99 -0
- package/src/database/service.ts +221 -0
- package/src/database/types.ts +171 -0
- package/src/di/container.ts +398 -0
- package/src/di/decorators.ts +228 -0
- package/src/di/index.ts +4 -0
- package/src/di/module-registry.ts +188 -0
- package/src/di/module.ts +65 -0
- package/src/di/types.ts +67 -0
- package/src/error/error-codes.ts +222 -0
- package/src/error/filter.ts +43 -0
- package/src/error/handler.ts +66 -0
- package/src/error/http-exception.ts +115 -0
- package/src/error/i18n.ts +217 -0
- package/src/error/index.ts +16 -0
- package/src/extensions/index.ts +5 -0
- package/src/extensions/logger-extension.ts +31 -0
- package/src/extensions/logger-module.ts +69 -0
- package/src/extensions/types.ts +14 -0
- package/src/files/index.ts +5 -0
- package/src/files/static-middleware.ts +53 -0
- package/src/files/storage.ts +67 -0
- package/src/files/types.ts +33 -0
- package/src/files/upload-middleware.ts +45 -0
- package/src/health/controller.ts +76 -0
- package/src/health/health-module.ts +51 -0
- package/src/health/index.ts +12 -0
- package/src/health/types.ts +28 -0
- package/src/index.ts +270 -0
- package/src/metrics/collector.ts +209 -0
- package/src/metrics/controller.ts +40 -0
- package/src/metrics/index.ts +15 -0
- package/src/metrics/metrics-module.ts +58 -0
- package/src/metrics/middleware.ts +46 -0
- package/src/metrics/prometheus.ts +79 -0
- package/src/metrics/types.ts +103 -0
- package/src/middleware/builtin/cors.ts +60 -0
- package/src/middleware/builtin/error-handler.ts +90 -0
- package/src/middleware/builtin/file-upload.ts +42 -0
- package/src/middleware/builtin/index.ts +14 -0
- package/src/middleware/builtin/logger.ts +91 -0
- package/src/middleware/builtin/rate-limit.ts +252 -0
- package/src/middleware/builtin/static-file.ts +88 -0
- package/src/middleware/decorators.ts +91 -0
- package/src/middleware/index.ts +11 -0
- package/src/middleware/middleware.ts +13 -0
- package/src/middleware/pipeline.ts +93 -0
- package/src/queue/decorators.ts +110 -0
- package/src/queue/index.ts +26 -0
- package/src/queue/queue-module.ts +64 -0
- package/src/queue/service.ts +302 -0
- package/src/queue/types.ts +341 -0
- package/src/request/body-parser.ts +133 -0
- package/src/request/file-handler.ts +46 -0
- package/src/request/index.ts +5 -0
- package/src/request/request.ts +107 -0
- package/src/request/response.ts +150 -0
- package/src/router/decorators.ts +122 -0
- package/src/router/index.ts +6 -0
- package/src/router/registry.ts +98 -0
- package/src/router/route.ts +140 -0
- package/src/router/router.ts +241 -0
- package/src/router/types.ts +27 -0
- package/src/security/access-decision-manager.ts +34 -0
- package/src/security/authentication-manager.ts +47 -0
- package/src/security/context.ts +92 -0
- package/src/security/filter.ts +162 -0
- package/src/security/index.ts +8 -0
- package/src/security/providers/index.ts +3 -0
- package/src/security/providers/jwt-provider.ts +60 -0
- package/src/security/providers/oauth2-provider.ts +70 -0
- package/src/security/security-module.ts +145 -0
- package/src/security/types.ts +165 -0
- package/src/session/decorators.ts +45 -0
- package/src/session/index.ts +19 -0
- package/src/session/middleware.ts +143 -0
- package/src/session/service.ts +218 -0
- package/src/session/session-module.ts +69 -0
- package/src/session/types.ts +373 -0
- package/src/swagger/decorators.ts +133 -0
- package/src/swagger/generator.ts +234 -0
- package/src/swagger/index.ts +7 -0
- package/src/swagger/swagger-extension.ts +41 -0
- package/src/swagger/swagger-module.ts +83 -0
- package/src/swagger/types.ts +188 -0
- package/src/swagger/ui.ts +98 -0
- package/src/testing/harness.ts +96 -0
- package/src/validation/decorators.ts +95 -0
- package/src/validation/errors.ts +28 -0
- package/src/validation/index.ts +14 -0
- package/src/validation/types.ts +35 -0
- package/src/validation/validator.ts +63 -0
- package/src/websocket/decorators.ts +51 -0
- package/src/websocket/index.ts +12 -0
- package/src/websocket/registry.ts +133 -0
- package/tests/cache/cache-module.test.ts +212 -0
- package/tests/config/config-module.test.ts +151 -0
- package/tests/controller/controller.test.ts +189 -0
- package/tests/core/application.test.ts +57 -0
- package/tests/core/context-body.test.ts +44 -0
- package/tests/core/context.test.ts +86 -0
- package/tests/core/edge-cases.test.ts +432 -0
- package/tests/database/database-module.test.ts +385 -0
- package/tests/database/orm.test.ts +164 -0
- package/tests/database/postgres-mysql-integration.test.ts +395 -0
- package/tests/database/transaction.test.ts +238 -0
- package/tests/di/container.test.ts +264 -0
- package/tests/di/module.test.ts +128 -0
- package/tests/error/error-codes.test.ts +121 -0
- package/tests/error/error-handler.test.ts +68 -0
- package/tests/error/error-handling.test.ts +254 -0
- package/tests/error/http-exception.test.ts +37 -0
- package/tests/error/i18n-integration.test.ts +175 -0
- package/tests/extensions/logger-extension.test.ts +40 -0
- package/tests/files/static-middleware.test.ts +67 -0
- package/tests/files/upload-middleware.test.ts +43 -0
- package/tests/health/health-module.test.ts +116 -0
- package/tests/integration/application-router.test.ts +85 -0
- package/tests/integration/body-parsing.test.ts +88 -0
- package/tests/integration/cache-e2e.test.ts +114 -0
- package/tests/integration/oauth2-e2e.test.ts +615 -0
- package/tests/integration/session-e2e.test.ts +207 -0
- package/tests/metrics/metrics-module.test.ts +178 -0
- package/tests/middleware/builtin.test.ts +206 -0
- package/tests/middleware/file-upload.test.ts +41 -0
- package/tests/middleware/middleware.test.ts +120 -0
- package/tests/middleware/pipeline.test.ts +72 -0
- package/tests/middleware/rate-limit.test.ts +314 -0
- package/tests/middleware/static-file.test.ts +62 -0
- package/tests/perf/harness.test.ts +48 -0
- package/tests/perf/optimization.test.ts +183 -0
- package/tests/perf/regression.test.ts +120 -0
- package/tests/queue/queue-module.test.ts +217 -0
- package/tests/request/body-parser.test.ts +96 -0
- package/tests/request/response.test.ts +99 -0
- package/tests/router/decorators.test.ts +48 -0
- package/tests/router/registry.test.ts +51 -0
- package/tests/router/route.test.ts +71 -0
- package/tests/router/router-normalization.test.ts +106 -0
- package/tests/router/router.test.ts +133 -0
- package/tests/security/access-decision-manager.test.ts +84 -0
- package/tests/security/authentication-manager.test.ts +81 -0
- package/tests/security/context.test.ts +302 -0
- package/tests/security/filter.test.ts +225 -0
- package/tests/security/jwt-provider.test.ts +106 -0
- package/tests/security/oauth2-provider.test.ts +269 -0
- package/tests/security/security-module.test.ts +143 -0
- package/tests/session/session-module.test.ts +307 -0
- package/tests/stress/di-stress.test.ts +30 -0
- package/tests/swagger/decorators.test.ts +153 -0
- package/tests/swagger/generator.test.ts +202 -0
- package/tests/swagger/swagger-extension.test.ts +72 -0
- package/tests/swagger/swagger-module.test.ts +79 -0
- package/tests/utils/test-port.ts +10 -0
- package/tests/validation/controller-validation.test.ts +64 -0
- package/tests/validation/validation.test.ts +42 -0
- package/tests/websocket/gateway.test.ts +68 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import type { SwaggerDocument, SwaggerOptions, SwaggerPathItem } from './types';
|
|
2
|
+
import { ControllerRegistry } from '../controller/controller';
|
|
3
|
+
import { getControllerMetadata, getRouteMetadata } from '../controller/metadata';
|
|
4
|
+
import {
|
|
5
|
+
getApiTags,
|
|
6
|
+
getApiOperation,
|
|
7
|
+
getApiParams,
|
|
8
|
+
getApiBody,
|
|
9
|
+
getApiResponses,
|
|
10
|
+
} from './decorators';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Swagger 文档生成器
|
|
14
|
+
*/
|
|
15
|
+
export class SwaggerGenerator {
|
|
16
|
+
private readonly options: SwaggerOptions;
|
|
17
|
+
|
|
18
|
+
public constructor(options: SwaggerOptions) {
|
|
19
|
+
this.options = options;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 生成 Swagger 文档
|
|
24
|
+
*/
|
|
25
|
+
public generate(): SwaggerDocument {
|
|
26
|
+
const document: SwaggerDocument = {
|
|
27
|
+
openapi: '3.0.0',
|
|
28
|
+
info: {
|
|
29
|
+
title: this.options.info.title,
|
|
30
|
+
version: this.options.info.version,
|
|
31
|
+
description: this.options.info.description,
|
|
32
|
+
contact: this.options.info.contact,
|
|
33
|
+
license: this.options.info.license,
|
|
34
|
+
},
|
|
35
|
+
servers: this.options.servers,
|
|
36
|
+
tags: this.options.tags,
|
|
37
|
+
paths: {},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// 从 ControllerRegistry 获取所有控制器
|
|
41
|
+
const controllerRegistry = ControllerRegistry.getInstance();
|
|
42
|
+
const controllers = controllerRegistry.getRegisteredControllers();
|
|
43
|
+
|
|
44
|
+
if (controllers.length === 0) {
|
|
45
|
+
return document;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const controllerClass of controllers) {
|
|
49
|
+
const controllerMetadata = getControllerMetadata(controllerClass);
|
|
50
|
+
if (!controllerMetadata) {
|
|
51
|
+
console.log(`[SwaggerGenerator] No metadata for controller: ${controllerClass.name}`);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const basePath = controllerMetadata.path;
|
|
56
|
+
const prototype = controllerClass.prototype;
|
|
57
|
+
|
|
58
|
+
// 获取路由元数据 - 从原型获取
|
|
59
|
+
const routes = getRouteMetadata(prototype);
|
|
60
|
+
|
|
61
|
+
// 如果没有路由,跳过这个控制器
|
|
62
|
+
if (!routes || routes.length === 0) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 获取控制器级别的标签
|
|
67
|
+
const controllerTags = getApiTags(controllerClass);
|
|
68
|
+
|
|
69
|
+
for (const route of routes) {
|
|
70
|
+
// 如果没有 propertyKey,尝试从 handler 函数名或原型中查找
|
|
71
|
+
let propertyKey = route.propertyKey;
|
|
72
|
+
if (!propertyKey && route.handler) {
|
|
73
|
+
// 尝试从 handler 函数名获取
|
|
74
|
+
propertyKey = route.handler.name;
|
|
75
|
+
// 如果还是没有,从原型中查找
|
|
76
|
+
if (!propertyKey || propertyKey === '') {
|
|
77
|
+
const propertyNames = Object.getOwnPropertyNames(prototype);
|
|
78
|
+
for (const key of propertyNames) {
|
|
79
|
+
if (key === 'constructor') continue;
|
|
80
|
+
const descriptor = Object.getOwnPropertyDescriptor(prototype, key);
|
|
81
|
+
if (descriptor && descriptor.value === route.handler) {
|
|
82
|
+
propertyKey = key;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!propertyKey) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const method = route.method.toLowerCase() as 'get' | 'post' | 'put' | 'delete' | 'patch';
|
|
94
|
+
// 组合基础路径和方法路径
|
|
95
|
+
let methodPath = route.path;
|
|
96
|
+
// 如果方法路径不是以 / 开头,需要添加
|
|
97
|
+
if (methodPath && !methodPath.startsWith('/')) {
|
|
98
|
+
methodPath = '/' + methodPath;
|
|
99
|
+
}
|
|
100
|
+
// 将路径参数格式从 :param 转换为 {param}(OpenAPI 格式)
|
|
101
|
+
const swaggerPath = (basePath + methodPath).replace(/:([^/]+)/g, '{$1}');
|
|
102
|
+
const fullPath = this.normalizePath(swaggerPath);
|
|
103
|
+
|
|
104
|
+
// 获取方法级别的元数据
|
|
105
|
+
const operationMetadata = getApiOperation(prototype, propertyKey);
|
|
106
|
+
const methodTags = getApiTags(prototype, propertyKey);
|
|
107
|
+
const params = getApiParams(prototype, propertyKey);
|
|
108
|
+
const body = getApiBody(prototype, propertyKey);
|
|
109
|
+
const responses = getApiResponses(prototype, propertyKey);
|
|
110
|
+
|
|
111
|
+
// 合并标签
|
|
112
|
+
const tags = [...new Set([...controllerTags, ...methodTags])];
|
|
113
|
+
|
|
114
|
+
const pathItem: SwaggerPathItem = {
|
|
115
|
+
summary: operationMetadata?.summary,
|
|
116
|
+
description: operationMetadata?.description,
|
|
117
|
+
operationId: operationMetadata?.operationId || propertyKey,
|
|
118
|
+
tags: tags.length > 0 ? tags : undefined,
|
|
119
|
+
deprecated: operationMetadata?.deprecated,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// 处理参数
|
|
123
|
+
const pathParams: Array<{
|
|
124
|
+
name: string;
|
|
125
|
+
in: 'query' | 'path' | 'header' | 'cookie';
|
|
126
|
+
description?: string;
|
|
127
|
+
required?: boolean;
|
|
128
|
+
schema?: {
|
|
129
|
+
type?: string;
|
|
130
|
+
format?: string;
|
|
131
|
+
enum?: unknown[];
|
|
132
|
+
default?: unknown;
|
|
133
|
+
};
|
|
134
|
+
}> = [];
|
|
135
|
+
|
|
136
|
+
// 自动从路径中提取路径参数
|
|
137
|
+
const pathParamMatches = fullPath.matchAll(/\{([^}]+)\}/g);
|
|
138
|
+
for (const match of pathParamMatches) {
|
|
139
|
+
const paramName = match[1];
|
|
140
|
+
// 检查是否已经有手动定义的参数
|
|
141
|
+
const existingParam = params.find((p) => p.metadata.name === paramName && p.metadata.in === 'path');
|
|
142
|
+
if (!existingParam) {
|
|
143
|
+
// 自动添加路径参数
|
|
144
|
+
pathParams.push({
|
|
145
|
+
name: paramName,
|
|
146
|
+
in: 'path',
|
|
147
|
+
required: true,
|
|
148
|
+
schema: { type: 'string' },
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 添加手动定义的参数
|
|
154
|
+
for (const param of params) {
|
|
155
|
+
pathParams.push({
|
|
156
|
+
name: param.metadata.name,
|
|
157
|
+
in: param.metadata.in,
|
|
158
|
+
description: param.metadata.description,
|
|
159
|
+
required: param.metadata.required,
|
|
160
|
+
schema: param.metadata.schema,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (pathParams.length > 0) {
|
|
165
|
+
pathItem.parameters = pathParams;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 处理请求体
|
|
169
|
+
if (body) {
|
|
170
|
+
pathItem.requestBody = {
|
|
171
|
+
description: body.description,
|
|
172
|
+
required: body.required,
|
|
173
|
+
content: {
|
|
174
|
+
'application/json': {
|
|
175
|
+
schema: body.schema,
|
|
176
|
+
examples: body.examples,
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 处理响应
|
|
183
|
+
if (responses.length > 0) {
|
|
184
|
+
pathItem.responses = {};
|
|
185
|
+
for (const response of responses) {
|
|
186
|
+
pathItem.responses[String(response.status)] = {
|
|
187
|
+
description: response.description,
|
|
188
|
+
content: {
|
|
189
|
+
'application/json': {
|
|
190
|
+
schema: response.schema,
|
|
191
|
+
examples: response.examples,
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
// 默认响应
|
|
198
|
+
pathItem.responses = {
|
|
199
|
+
'200': {
|
|
200
|
+
description: 'Success',
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 初始化路径对象
|
|
206
|
+
if (!document.paths[fullPath]) {
|
|
207
|
+
document.paths[fullPath] = {};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
document.paths[fullPath][method] = pathItem;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return document;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* 规范化路径
|
|
219
|
+
*/
|
|
220
|
+
private normalizePath(path: string): string {
|
|
221
|
+
// 移除重复的斜杠
|
|
222
|
+
path = path.replace(/\/+/g, '/');
|
|
223
|
+
// 确保以 / 开头
|
|
224
|
+
if (!path.startsWith('/')) {
|
|
225
|
+
path = '/' + path;
|
|
226
|
+
}
|
|
227
|
+
// 移除末尾的斜杠(除非是根路径)
|
|
228
|
+
if (path.length > 1 && path.endsWith('/')) {
|
|
229
|
+
path = path.slice(0, -1);
|
|
230
|
+
}
|
|
231
|
+
return path;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Container } from '../di/container';
|
|
2
|
+
import type { ApplicationExtension } from '../extensions/types';
|
|
3
|
+
import { SwaggerGenerator } from './generator';
|
|
4
|
+
import type { SwaggerOptions } from './types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Swagger 扩展
|
|
8
|
+
*/
|
|
9
|
+
export class SwaggerExtension implements ApplicationExtension {
|
|
10
|
+
private readonly options: SwaggerOptions;
|
|
11
|
+
private generator?: SwaggerGenerator;
|
|
12
|
+
|
|
13
|
+
public constructor(options: SwaggerOptions) {
|
|
14
|
+
this.options = options;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 注册扩展
|
|
19
|
+
*/
|
|
20
|
+
public register(_container: Container): void {
|
|
21
|
+
this.generator = new SwaggerGenerator(this.options);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 获取 Swagger 文档生成器
|
|
26
|
+
*/
|
|
27
|
+
public getGenerator(): SwaggerGenerator {
|
|
28
|
+
if (!this.generator) {
|
|
29
|
+
this.generator = new SwaggerGenerator(this.options);
|
|
30
|
+
}
|
|
31
|
+
return this.generator;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 生成 Swagger JSON
|
|
36
|
+
*/
|
|
37
|
+
public generateJSON(): string {
|
|
38
|
+
return JSON.stringify(this.getGenerator().generate(), null, 2);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Module, MODULE_METADATA_KEY } from '../di/module';
|
|
2
|
+
import { SwaggerExtension } from './swagger-extension';
|
|
3
|
+
import { createSwaggerUIMiddleware } from './ui';
|
|
4
|
+
import type { SwaggerOptions } from './types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Swagger 模块配置
|
|
8
|
+
*/
|
|
9
|
+
export interface SwaggerModuleOptions extends SwaggerOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Swagger UI 路径
|
|
12
|
+
* @default '/swagger'
|
|
13
|
+
*/
|
|
14
|
+
uiPath?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Swagger JSON 路径
|
|
17
|
+
* @default '/swagger.json'
|
|
18
|
+
*/
|
|
19
|
+
jsonPath?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Swagger UI 标题
|
|
22
|
+
*/
|
|
23
|
+
uiTitle?: string;
|
|
24
|
+
/**
|
|
25
|
+
* 是否启用 Swagger UI
|
|
26
|
+
* @default true
|
|
27
|
+
*/
|
|
28
|
+
enableUI?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Swagger 模块
|
|
33
|
+
* 提供 API 文档生成和 Swagger UI
|
|
34
|
+
*/
|
|
35
|
+
@Module({
|
|
36
|
+
extensions: [
|
|
37
|
+
// 将在运行时根据配置创建
|
|
38
|
+
],
|
|
39
|
+
middlewares: [
|
|
40
|
+
// 将在运行时根据配置创建
|
|
41
|
+
],
|
|
42
|
+
})
|
|
43
|
+
export class SwaggerModule {
|
|
44
|
+
/**
|
|
45
|
+
* 创建 Swagger 模块
|
|
46
|
+
* @param options - 模块配置
|
|
47
|
+
*/
|
|
48
|
+
public static forRoot(options: SwaggerModuleOptions): typeof SwaggerModule {
|
|
49
|
+
const extensions: any[] = [];
|
|
50
|
+
const middlewares: any[] = [];
|
|
51
|
+
|
|
52
|
+
// 创建 Swagger 扩展
|
|
53
|
+
const swaggerExtension = new SwaggerExtension({
|
|
54
|
+
info: options.info,
|
|
55
|
+
servers: options.servers,
|
|
56
|
+
basePath: options.basePath,
|
|
57
|
+
tags: options.tags,
|
|
58
|
+
});
|
|
59
|
+
extensions.push(swaggerExtension);
|
|
60
|
+
|
|
61
|
+
// 如果启用 UI,添加中间件
|
|
62
|
+
if (options.enableUI !== false) {
|
|
63
|
+
const uiMiddleware = createSwaggerUIMiddleware(swaggerExtension, {
|
|
64
|
+
uiPath: options.uiPath || '/swagger',
|
|
65
|
+
jsonPath: options.jsonPath || '/swagger.json',
|
|
66
|
+
title: options.uiTitle || options.info.title || 'API Documentation',
|
|
67
|
+
});
|
|
68
|
+
middlewares.push(uiMiddleware);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 动态更新模块元数据
|
|
72
|
+
const existingMetadata = Reflect.getMetadata(MODULE_METADATA_KEY, SwaggerModule) || {};
|
|
73
|
+
const metadata = {
|
|
74
|
+
...existingMetadata,
|
|
75
|
+
extensions,
|
|
76
|
+
middlewares,
|
|
77
|
+
};
|
|
78
|
+
Reflect.defineMetadata(MODULE_METADATA_KEY, metadata, SwaggerModule);
|
|
79
|
+
|
|
80
|
+
return SwaggerModule;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swagger/OpenAPI 类型定义
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Swagger 信息配置
|
|
7
|
+
*/
|
|
8
|
+
export interface SwaggerInfo {
|
|
9
|
+
title: string;
|
|
10
|
+
version: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
contact?: {
|
|
13
|
+
name?: string;
|
|
14
|
+
email?: string;
|
|
15
|
+
url?: string;
|
|
16
|
+
};
|
|
17
|
+
license?: {
|
|
18
|
+
name: string;
|
|
19
|
+
url?: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Swagger 配置
|
|
25
|
+
*/
|
|
26
|
+
export interface SwaggerOptions {
|
|
27
|
+
info: SwaggerInfo;
|
|
28
|
+
servers?: Array<{
|
|
29
|
+
url: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
}>;
|
|
32
|
+
basePath?: string;
|
|
33
|
+
tags?: Array<{
|
|
34
|
+
name: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
}>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* API 操作元数据
|
|
41
|
+
*/
|
|
42
|
+
export interface ApiOperationMetadata {
|
|
43
|
+
summary?: string;
|
|
44
|
+
description?: string;
|
|
45
|
+
operationId?: string;
|
|
46
|
+
tags?: string[];
|
|
47
|
+
deprecated?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* API 参数元数据
|
|
52
|
+
*/
|
|
53
|
+
export interface ApiParamMetadata {
|
|
54
|
+
name: string;
|
|
55
|
+
description?: string;
|
|
56
|
+
required?: boolean;
|
|
57
|
+
schema?: {
|
|
58
|
+
type?: string;
|
|
59
|
+
format?: string;
|
|
60
|
+
enum?: unknown[];
|
|
61
|
+
default?: unknown;
|
|
62
|
+
};
|
|
63
|
+
in: 'query' | 'path' | 'header' | 'cookie';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* API 请求体元数据
|
|
68
|
+
*/
|
|
69
|
+
export interface ApiBodyMetadata {
|
|
70
|
+
description?: string;
|
|
71
|
+
required?: boolean;
|
|
72
|
+
schema?: {
|
|
73
|
+
type?: string;
|
|
74
|
+
properties?: Record<string, unknown>;
|
|
75
|
+
required?: string[];
|
|
76
|
+
};
|
|
77
|
+
examples?: Record<string, unknown>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* API 响应元数据
|
|
82
|
+
*/
|
|
83
|
+
export interface ApiResponseMetadata {
|
|
84
|
+
status: number;
|
|
85
|
+
description?: string;
|
|
86
|
+
schema?: {
|
|
87
|
+
type?: string;
|
|
88
|
+
properties?: Record<string, unknown>;
|
|
89
|
+
};
|
|
90
|
+
examples?: Record<string, unknown>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* API 标签元数据
|
|
95
|
+
*/
|
|
96
|
+
export interface ApiTagMetadata {
|
|
97
|
+
name: string;
|
|
98
|
+
description?: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Swagger 路径项
|
|
103
|
+
*/
|
|
104
|
+
export interface SwaggerPathItem {
|
|
105
|
+
summary?: string;
|
|
106
|
+
description?: string;
|
|
107
|
+
operationId?: string;
|
|
108
|
+
tags?: string[];
|
|
109
|
+
parameters?: Array<{
|
|
110
|
+
name: string;
|
|
111
|
+
in: 'query' | 'path' | 'header' | 'cookie';
|
|
112
|
+
description?: string;
|
|
113
|
+
required?: boolean;
|
|
114
|
+
schema?: {
|
|
115
|
+
type?: string;
|
|
116
|
+
format?: string;
|
|
117
|
+
enum?: unknown[];
|
|
118
|
+
default?: unknown;
|
|
119
|
+
};
|
|
120
|
+
}>;
|
|
121
|
+
requestBody?: {
|
|
122
|
+
description?: string;
|
|
123
|
+
required?: boolean;
|
|
124
|
+
content?: {
|
|
125
|
+
'application/json'?: {
|
|
126
|
+
schema?: {
|
|
127
|
+
type?: string;
|
|
128
|
+
properties?: Record<string, unknown>;
|
|
129
|
+
required?: string[];
|
|
130
|
+
};
|
|
131
|
+
examples?: Record<string, unknown>;
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
responses?: Record<
|
|
136
|
+
string,
|
|
137
|
+
{
|
|
138
|
+
description?: string;
|
|
139
|
+
content?: {
|
|
140
|
+
'application/json'?: {
|
|
141
|
+
schema?: {
|
|
142
|
+
type?: string;
|
|
143
|
+
properties?: Record<string, unknown>;
|
|
144
|
+
};
|
|
145
|
+
examples?: Record<string, unknown>;
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
>;
|
|
150
|
+
deprecated?: boolean;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Swagger 文档结构
|
|
155
|
+
*/
|
|
156
|
+
export interface SwaggerDocument {
|
|
157
|
+
openapi: string;
|
|
158
|
+
info: {
|
|
159
|
+
title: string;
|
|
160
|
+
version: string;
|
|
161
|
+
description?: string;
|
|
162
|
+
contact?: {
|
|
163
|
+
name?: string;
|
|
164
|
+
email?: string;
|
|
165
|
+
url?: string;
|
|
166
|
+
};
|
|
167
|
+
license?: {
|
|
168
|
+
name: string;
|
|
169
|
+
url?: string;
|
|
170
|
+
};
|
|
171
|
+
};
|
|
172
|
+
servers?: Array<{
|
|
173
|
+
url: string;
|
|
174
|
+
description?: string;
|
|
175
|
+
}>;
|
|
176
|
+
tags?: Array<{
|
|
177
|
+
name: string;
|
|
178
|
+
description?: string;
|
|
179
|
+
}>;
|
|
180
|
+
paths: Record<string, {
|
|
181
|
+
get?: SwaggerPathItem;
|
|
182
|
+
post?: SwaggerPathItem;
|
|
183
|
+
put?: SwaggerPathItem;
|
|
184
|
+
delete?: SwaggerPathItem;
|
|
185
|
+
patch?: SwaggerPathItem;
|
|
186
|
+
}>;
|
|
187
|
+
}
|
|
188
|
+
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { Context } from '../core/context';
|
|
2
|
+
import type { Middleware, NextFunction } from '../middleware';
|
|
3
|
+
import { SwaggerExtension } from './swagger-extension';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Swagger UI HTML 模板
|
|
7
|
+
*/
|
|
8
|
+
const SWAGGER_UI_HTML = (jsonUrl: string, title: string) => `<!DOCTYPE html>
|
|
9
|
+
<html lang="en">
|
|
10
|
+
<head>
|
|
11
|
+
<meta charset="UTF-8">
|
|
12
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
13
|
+
<title>${title} - Swagger UI</title>
|
|
14
|
+
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui.css" />
|
|
15
|
+
<style>
|
|
16
|
+
html {
|
|
17
|
+
box-sizing: border-box;
|
|
18
|
+
overflow: -moz-scrollbars-vertical;
|
|
19
|
+
overflow-y: scroll;
|
|
20
|
+
}
|
|
21
|
+
*, *:before, *:after {
|
|
22
|
+
box-sizing: inherit;
|
|
23
|
+
}
|
|
24
|
+
body {
|
|
25
|
+
margin:0;
|
|
26
|
+
background: #fafafa;
|
|
27
|
+
}
|
|
28
|
+
</style>
|
|
29
|
+
</head>
|
|
30
|
+
<body>
|
|
31
|
+
<div id="swagger-ui"></div>
|
|
32
|
+
<script src="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui-bundle.js"></script>
|
|
33
|
+
<script src="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui-standalone-preset.js"></script>
|
|
34
|
+
<script>
|
|
35
|
+
window.onload = function() {
|
|
36
|
+
const ui = SwaggerUIBundle({
|
|
37
|
+
url: "${jsonUrl}",
|
|
38
|
+
dom_id: '#swagger-ui',
|
|
39
|
+
deepLinking: true,
|
|
40
|
+
presets: [
|
|
41
|
+
SwaggerUIBundle.presets.apis,
|
|
42
|
+
SwaggerUIStandalonePreset
|
|
43
|
+
],
|
|
44
|
+
plugins: [
|
|
45
|
+
SwaggerUIBundle.plugins.DownloadUrl
|
|
46
|
+
],
|
|
47
|
+
layout: "StandaloneLayout"
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
</script>
|
|
51
|
+
</body>
|
|
52
|
+
</html>`;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 创建 Swagger UI 中间件
|
|
56
|
+
* @param extension - Swagger 扩展实例
|
|
57
|
+
* @param options - 配置选项
|
|
58
|
+
*/
|
|
59
|
+
export function createSwaggerUIMiddleware(
|
|
60
|
+
extension: SwaggerExtension,
|
|
61
|
+
options: {
|
|
62
|
+
uiPath?: string;
|
|
63
|
+
jsonPath?: string;
|
|
64
|
+
title?: string;
|
|
65
|
+
} = {},
|
|
66
|
+
): Middleware {
|
|
67
|
+
const uiPath = options.uiPath || '/swagger';
|
|
68
|
+
const jsonPath = options.jsonPath || '/swagger.json';
|
|
69
|
+
const title = options.title || 'API Documentation';
|
|
70
|
+
|
|
71
|
+
return async (ctx: Context, next: NextFunction): Promise<Response> => {
|
|
72
|
+
const url = new URL(ctx.request.url);
|
|
73
|
+
const pathname = url.pathname;
|
|
74
|
+
|
|
75
|
+
// Swagger UI 页面
|
|
76
|
+
if (pathname === uiPath || pathname === `${uiPath}/`) {
|
|
77
|
+
const html = SWAGGER_UI_HTML(jsonPath, title);
|
|
78
|
+
return new Response(html, {
|
|
79
|
+
headers: {
|
|
80
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Swagger JSON
|
|
86
|
+
if (pathname === jsonPath) {
|
|
87
|
+
const json = extension.generateJSON();
|
|
88
|
+
return new Response(json, {
|
|
89
|
+
headers: {
|
|
90
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return await next();
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|