@mindbase/express-common 1.0.6 → 1.0.8
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/commands/prepare.ts +62 -0
- package/core/app.ts +186 -0
- package/core/module/CreateModule.ts +38 -0
- package/core/module/FindPackageRoot.ts +58 -0
- package/core/module/GetModulePath.ts +58 -0
- package/core/state.ts +67 -0
- package/feature/cron/CronManager.ts +63 -0
- package/feature/scanner/FileScanner.ts +289 -0
- package/index.ts +32 -0
- package/middleware/Cors.ts +17 -0
- package/middleware/IpParser.ts +81 -0
- package/middleware/UaParser.ts +50 -0
- package/package.json +14 -10
- package/routes/Doc.route.ts +123 -0
- package/tsconfig.json +8 -0
- package/types/DocTypes.ts +111 -0
- package/types/express.d.ts +12 -0
- package/types/index.ts +19 -0
- package/utils/AppError.ts +21 -0
- package/utils/ComponentRegistry.ts +34 -0
- package/utils/DatabaseMigration.ts +121 -0
- package/utils/Dayjs.ts +16 -0
- package/utils/DocManager.ts +279 -0
- package/utils/HttpServer.ts +41 -0
- package/utils/InitDatabase.ts +133 -0
- package/utils/InitErrorHandler.ts +82 -0
- package/utils/InitExpress.ts +38 -0
- package/utils/Logger.ts +206 -0
- package/utils/MiddlewareRegistry.ts +14 -0
- package/utils/RouteParser.ts +655 -0
- package/utils/RouteRegistry.ts +92 -0
- package/utils/TSTypeParser.ts +456 -0
- package/utils/Validate.ts +25 -0
- package/utils/ZodSchemaParser.ts +420 -0
- package/zod/Doc.schema.ts +9 -0
- package/dist/index.d.mts +0 -363
- package/dist/index.mjs +0 -2468
- package/dist/index.mjs.map +0 -1
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Project,
|
|
3
|
+
SyntaxKind,
|
|
4
|
+
Node,
|
|
5
|
+
SourceFile,
|
|
6
|
+
StringLiteral,
|
|
7
|
+
ModuleKind,
|
|
8
|
+
ScriptTarget,
|
|
9
|
+
JSDoc,
|
|
10
|
+
} from "ts-morph";
|
|
11
|
+
import * as path from "path";
|
|
12
|
+
import { RouteInfo, RequestSchema, ResponseSchema, FieldValidation } from "../types/DocTypes";
|
|
13
|
+
import logger from "./Logger";
|
|
14
|
+
import { ZodSchemaParser } from "./ZodSchemaParser";
|
|
15
|
+
import { TSTypeParser } from "./TSTypeParser";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* apidoc 标签解析结果
|
|
19
|
+
*/
|
|
20
|
+
interface ApiDocTags {
|
|
21
|
+
params: Array<{
|
|
22
|
+
name: string;
|
|
23
|
+
type: string;
|
|
24
|
+
description: string;
|
|
25
|
+
required: boolean;
|
|
26
|
+
enumValues?: string[];
|
|
27
|
+
}>;
|
|
28
|
+
success: Array<{
|
|
29
|
+
name: string;
|
|
30
|
+
type: string;
|
|
31
|
+
description: string;
|
|
32
|
+
parent?: string;
|
|
33
|
+
}>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 路由解析器
|
|
38
|
+
* 使用 ts-morph 解析路由文件,提取路由信息
|
|
39
|
+
*/
|
|
40
|
+
export class RouteParser {
|
|
41
|
+
private project: Project;
|
|
42
|
+
private schemaParser: ZodSchemaParser;
|
|
43
|
+
private tsTypeParser: TSTypeParser;
|
|
44
|
+
|
|
45
|
+
constructor() {
|
|
46
|
+
this.project = new Project({
|
|
47
|
+
compilerOptions: {
|
|
48
|
+
module: ModuleKind.CommonJS,
|
|
49
|
+
target: ScriptTarget.ES2018,
|
|
50
|
+
allowJs: true,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
this.schemaParser = new ZodSchemaParser();
|
|
54
|
+
this.tsTypeParser = new TSTypeParser();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 解析单个路由文件
|
|
59
|
+
* @param filePath 路由文件路径
|
|
60
|
+
* @returns 路由信息数组
|
|
61
|
+
*/
|
|
62
|
+
public parseRouteFile(filePath: string): RouteInfo[] {
|
|
63
|
+
try {
|
|
64
|
+
const sourceFile = this.project.addSourceFileAtPath(filePath);
|
|
65
|
+
const routes: RouteInfo[] = [];
|
|
66
|
+
|
|
67
|
+
// 获取所有导入的 schema 文件映射
|
|
68
|
+
const schemaFiles = this.getImportedSchemaFiles(sourceFile, filePath);
|
|
69
|
+
|
|
70
|
+
// 获取所有类型导入映射(用于响应类型解析)
|
|
71
|
+
const typeImports = this.getTypeImports(sourceFile, filePath);
|
|
72
|
+
|
|
73
|
+
// 查找所有路由调用
|
|
74
|
+
sourceFile.forEachDescendant((node) => {
|
|
75
|
+
const callExpr = node as any;
|
|
76
|
+
if (callExpr.isKind && callExpr.isKind(SyntaxKind.CallExpression) && callExpr.getExpression && callExpr.getExpression().isKind(SyntaxKind.PropertyAccessExpression)) {
|
|
77
|
+
const propAccess = callExpr.getExpression();
|
|
78
|
+
const object = propAccess.getExpression && propAccess.getExpression().getText();
|
|
79
|
+
const method = propAccess.getName && propAccess.getName();
|
|
80
|
+
|
|
81
|
+
// 检查是否是 router 的 HTTP 方法调用
|
|
82
|
+
if (object === "router" && ["get", "post", "put", "delete", "patch"].includes(method)) {
|
|
83
|
+
// 提取路由路径
|
|
84
|
+
const args = callExpr.getArguments && callExpr.getArguments();
|
|
85
|
+
if (args && args.length > 0) {
|
|
86
|
+
const pathArg = args[0];
|
|
87
|
+
if (pathArg.isKind && pathArg.isKind(SyntaxKind.StringLiteral)) {
|
|
88
|
+
const routePath = pathArg.getText && pathArg.getText().replace(/['"]/g, "");
|
|
89
|
+
|
|
90
|
+
// 提取 JSDoc 注释
|
|
91
|
+
const jsDoc = callExpr.getPreviousSiblingIfKind && callExpr.getPreviousSiblingIfKind(SyntaxKind.JSDocComment);
|
|
92
|
+
let summary = "";
|
|
93
|
+
let description = "";
|
|
94
|
+
let requestSchema: RequestSchema | undefined;
|
|
95
|
+
let responseSchema: ResponseSchema | undefined;
|
|
96
|
+
|
|
97
|
+
if (jsDoc) {
|
|
98
|
+
const commentText = jsDoc.getCommentText && jsDoc.getCommentText();
|
|
99
|
+
if (commentText) {
|
|
100
|
+
const lines = commentText.split("\n").map((line) => line.trim());
|
|
101
|
+
summary = lines[0] || "";
|
|
102
|
+
description = lines.slice(1).join("\n").trim() || summary;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 解析 apidoc 格式的注释标签
|
|
106
|
+
try {
|
|
107
|
+
const fullCommentText = jsDoc.getFullText?.() || commentText || "";
|
|
108
|
+
const apiDocTags = this.parseApiDocTags(fullCommentText);
|
|
109
|
+
|
|
110
|
+
// 从 @apiParam 构建请求 Schema(优先级高于 validate 中间件)
|
|
111
|
+
if (apiDocTags.params.length > 0) {
|
|
112
|
+
requestSchema = this.buildRequestSchemaFromTags(apiDocTags.params);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 从 @apiSuccess 构建响应 Schema
|
|
116
|
+
if (apiDocTags.success.length > 0) {
|
|
117
|
+
responseSchema = this.buildResponseSchemaFromTags(apiDocTags.success);
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
logger.warn(`解析 apidoc 注释失败: ${filePath}:${routePath}`, error);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 如果没有 @apiSuccess,尝试提取 @returns 标签(降级方案)
|
|
124
|
+
if (!responseSchema) {
|
|
125
|
+
responseSchema = this.extractResponseSchema(jsDoc, sourceFile, typeImports);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 提取中间件信息
|
|
130
|
+
const middlewares: string[] = [];
|
|
131
|
+
for (let i = 1; i < args.length - 1; i++) {
|
|
132
|
+
const arg = args[i];
|
|
133
|
+
if (arg.isKind(SyntaxKind.CallExpression)) {
|
|
134
|
+
const mwCallExpr = arg as any;
|
|
135
|
+
const expr = mwCallExpr.getExpression();
|
|
136
|
+
if (expr.isKind(SyntaxKind.Identifier)) {
|
|
137
|
+
middlewares.push(expr.getText());
|
|
138
|
+
} else if (expr.isKind(SyntaxKind.PropertyAccessExpression)) {
|
|
139
|
+
middlewares.push(expr.getText());
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 如果没有从注释获取到请求 Schema,尝试从 validate 中间件获取(降级方案)
|
|
145
|
+
if (!requestSchema) {
|
|
146
|
+
requestSchema = this.extractRequestSchema(args, schemaFiles);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const routeInfo: RouteInfo = {
|
|
150
|
+
module: path.basename(path.dirname(filePath)).toLowerCase(),
|
|
151
|
+
method: method.toUpperCase(),
|
|
152
|
+
path: routePath,
|
|
153
|
+
fullPath: "", // 完整路径会在注册时填充
|
|
154
|
+
summary,
|
|
155
|
+
description,
|
|
156
|
+
requestSchema,
|
|
157
|
+
responseSchema,
|
|
158
|
+
middlewares,
|
|
159
|
+
filePath,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
routes.push(routeInfo);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
return routes;
|
|
170
|
+
} catch (error) {
|
|
171
|
+
logger.error(`解析路由文件失败: ${filePath}`, error);
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* 获取导入的 schema 文件映射
|
|
178
|
+
* @returns Map<变量名, 文件绝对路径>
|
|
179
|
+
*/
|
|
180
|
+
private getImportedSchemaFiles(
|
|
181
|
+
sourceFile: SourceFile,
|
|
182
|
+
currentFilePath: string
|
|
183
|
+
): Map<string, string> {
|
|
184
|
+
const schemaFiles = new Map<string, string>();
|
|
185
|
+
|
|
186
|
+
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
187
|
+
const source = importDecl.getModuleSpecifierValue();
|
|
188
|
+
|
|
189
|
+
// 检查是否是 schema 相关的导入
|
|
190
|
+
if (source.includes(".schema") || source.includes("/zod/") || source.includes("\\zod\\")) {
|
|
191
|
+
// 解析相对路径为绝对路径
|
|
192
|
+
const absolutePath = this.resolveImportPath(
|
|
193
|
+
currentFilePath,
|
|
194
|
+
source
|
|
195
|
+
);
|
|
196
|
+
if (absolutePath) {
|
|
197
|
+
// 记录导入的变量名 -> 文件路径映射
|
|
198
|
+
for (const specifier of importDecl.getNamedImports()) {
|
|
199
|
+
logger.debug(`发现 schema 导入: ${specifier.getName()} -> ${absolutePath}`);
|
|
200
|
+
schemaFiles.set(specifier.getName(), absolutePath);
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
logger.debug(`无法解析 schema 导入路径: ${source}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return schemaFiles;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 获取所有类型导入映射(用于响应类型解析)
|
|
213
|
+
* @returns Map<类型名, 文件绝对路径>
|
|
214
|
+
*/
|
|
215
|
+
private getTypeImports(
|
|
216
|
+
sourceFile: SourceFile,
|
|
217
|
+
currentFilePath: string
|
|
218
|
+
): Map<string, string> {
|
|
219
|
+
const typeImports = new Map<string, string>();
|
|
220
|
+
|
|
221
|
+
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
222
|
+
const source = importDecl.getModuleSpecifierValue();
|
|
223
|
+
|
|
224
|
+
// 解析相对路径为绝对路径
|
|
225
|
+
const absolutePath = this.resolveImportPath(currentFilePath, source);
|
|
226
|
+
if (absolutePath) {
|
|
227
|
+
// 记录所有命名导入
|
|
228
|
+
for (const specifier of importDecl.getNamedImports()) {
|
|
229
|
+
typeImports.set(specifier.getName(), absolutePath);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return typeImports;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* 从 JSDoc 中提取响应 Schema
|
|
239
|
+
*/
|
|
240
|
+
private extractResponseSchema(
|
|
241
|
+
jsDoc: JSDoc,
|
|
242
|
+
sourceFile: SourceFile,
|
|
243
|
+
typeImports: Map<string, string>
|
|
244
|
+
): ResponseSchema | undefined {
|
|
245
|
+
// 获取所有 JSDoc 标签
|
|
246
|
+
const tags = jsDoc.getTags();
|
|
247
|
+
|
|
248
|
+
for (const tag of tags) {
|
|
249
|
+
const tagName = tag.getTagName();
|
|
250
|
+
|
|
251
|
+
// 检查 @returns 或 @return 标签
|
|
252
|
+
if (tagName === "returns" || tagName === "return") {
|
|
253
|
+
// 获取标签的完整文本
|
|
254
|
+
const tagText = tag.getText?.() || "";
|
|
255
|
+
logger.debug(`@returns 标签文本: ${tagText}`);
|
|
256
|
+
|
|
257
|
+
// 尝试从标签文本中提取类型 {TypeName}
|
|
258
|
+
const typeMatch = tagText.match(/@\s*returns?\s*\{([^}]+)\}/i);
|
|
259
|
+
if (typeMatch) {
|
|
260
|
+
const typeString = typeMatch[1].trim();
|
|
261
|
+
logger.debug(`从标签文本提取类型: ${typeString}`);
|
|
262
|
+
|
|
263
|
+
const responseSchema = this.tsTypeParser.parseResponseType(
|
|
264
|
+
sourceFile,
|
|
265
|
+
typeString,
|
|
266
|
+
typeImports
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
if (responseSchema) {
|
|
270
|
+
return responseSchema;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// 尝试从注释中提取类型
|
|
275
|
+
const comment = tag.getCommentText?.();
|
|
276
|
+
if (comment) {
|
|
277
|
+
const commentMatch = comment.match(/^\{([^}]+)\}/);
|
|
278
|
+
if (commentMatch) {
|
|
279
|
+
const typeString = commentMatch[1].trim();
|
|
280
|
+
logger.debug(`从注释提取类型: ${typeString}`);
|
|
281
|
+
|
|
282
|
+
const responseSchema = this.tsTypeParser.parseResponseType(
|
|
283
|
+
sourceFile,
|
|
284
|
+
typeString,
|
|
285
|
+
typeImports
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
if (responseSchema) {
|
|
289
|
+
return responseSchema;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 尝试使用 ts-morph 的类型表达式
|
|
295
|
+
const typeExpression = (tag as any).getTypeExpression?.();
|
|
296
|
+
if (typeExpression) {
|
|
297
|
+
const typeNode = typeExpression.getType?.();
|
|
298
|
+
if (typeNode) {
|
|
299
|
+
const typeString = typeNode.getText();
|
|
300
|
+
logger.debug(`从类型表达式提取类型: ${typeString}`);
|
|
301
|
+
|
|
302
|
+
if (typeString && typeString !== "any") {
|
|
303
|
+
const responseSchema = this.tsTypeParser.parseResponseType(
|
|
304
|
+
sourceFile,
|
|
305
|
+
typeString,
|
|
306
|
+
typeImports
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
if (responseSchema) {
|
|
310
|
+
return responseSchema;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* 解析导入路径为绝对路径
|
|
323
|
+
*/
|
|
324
|
+
private resolveImportPath(
|
|
325
|
+
currentFilePath: string,
|
|
326
|
+
importPath: string
|
|
327
|
+
): string | null {
|
|
328
|
+
try {
|
|
329
|
+
// 处理相对路径
|
|
330
|
+
if (importPath.startsWith(".")) {
|
|
331
|
+
const currentDir = path.dirname(currentFilePath);
|
|
332
|
+
const absolutePath = path.resolve(currentDir, importPath);
|
|
333
|
+
|
|
334
|
+
// 尝试添加 .ts 扩展名
|
|
335
|
+
const tsPath = absolutePath.endsWith(".ts")
|
|
336
|
+
? absolutePath
|
|
337
|
+
: `${absolutePath}.ts`;
|
|
338
|
+
return tsPath;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// 非相对路径暂不处理
|
|
342
|
+
return null;
|
|
343
|
+
} catch (error) {
|
|
344
|
+
logger.warn(`解析导入路径失败: ${importPath}`);
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* 提取请求 Schema
|
|
351
|
+
*/
|
|
352
|
+
private extractRequestSchema(
|
|
353
|
+
args: any[],
|
|
354
|
+
schemaFiles: Map<string, string>
|
|
355
|
+
): RequestSchema | undefined {
|
|
356
|
+
for (const arg of args) {
|
|
357
|
+
// 路由参数可能是 validate(...) 或 validate(...) as any
|
|
358
|
+
// 需要先处理 as any 类型断言
|
|
359
|
+
let targetArg = arg;
|
|
360
|
+
if (
|
|
361
|
+
arg.isKind &&
|
|
362
|
+
arg.isKind(SyntaxKind.AsExpression)
|
|
363
|
+
) {
|
|
364
|
+
targetArg = arg.getExpression();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// 检查是否是 validate 调用
|
|
368
|
+
if (
|
|
369
|
+
targetArg.isKind &&
|
|
370
|
+
targetArg.isKind(SyntaxKind.CallExpression) &&
|
|
371
|
+
targetArg.getExpression &&
|
|
372
|
+
targetArg.getExpression().isKind &&
|
|
373
|
+
targetArg.getExpression().isKind(SyntaxKind.Identifier) &&
|
|
374
|
+
targetArg.getExpression().getText() === "validate"
|
|
375
|
+
) {
|
|
376
|
+
const validateArgs = targetArg.getArguments();
|
|
377
|
+
if (validateArgs.length === 0) continue;
|
|
378
|
+
|
|
379
|
+
// 获取 schema 变量名
|
|
380
|
+
const schemaArg = validateArgs[0];
|
|
381
|
+
let schemaName: string;
|
|
382
|
+
|
|
383
|
+
// 处理 schema 参数中的 as any 类型断言
|
|
384
|
+
if (
|
|
385
|
+
schemaArg.isKind &&
|
|
386
|
+
schemaArg.isKind(SyntaxKind.AsExpression)
|
|
387
|
+
) {
|
|
388
|
+
schemaName = schemaArg.getExpression().getText();
|
|
389
|
+
} else {
|
|
390
|
+
schemaName = schemaArg.getText();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// 获取 target 参数
|
|
394
|
+
let target: "body" | "query" | "params" = "body";
|
|
395
|
+
if (validateArgs.length > 1) {
|
|
396
|
+
const targetArgParam = validateArgs[1];
|
|
397
|
+
if (
|
|
398
|
+
targetArgParam.isKind &&
|
|
399
|
+
targetArgParam.isKind(SyntaxKind.StringLiteral)
|
|
400
|
+
) {
|
|
401
|
+
target = (targetArgParam as StringLiteral).getLiteralValue() as
|
|
402
|
+
| "body"
|
|
403
|
+
| "query"
|
|
404
|
+
| "params";
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// 解析 schema 文件
|
|
409
|
+
const schemaFilePath = schemaFiles.get(schemaName);
|
|
410
|
+
if (schemaFilePath) {
|
|
411
|
+
try {
|
|
412
|
+
const schemaSourceFile =
|
|
413
|
+
this.project.addSourceFileAtPath(schemaFilePath);
|
|
414
|
+
const fields = this.schemaParser.parseSchemaVariable(
|
|
415
|
+
schemaSourceFile,
|
|
416
|
+
schemaName
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
if (fields) {
|
|
420
|
+
return { target, fields };
|
|
421
|
+
}
|
|
422
|
+
} catch (error) {
|
|
423
|
+
logger.warn(`解析 schema 文件失败: ${schemaFilePath}`, error);
|
|
424
|
+
}
|
|
425
|
+
} else {
|
|
426
|
+
logger.debug(`未找到 schema 文件映射: ${schemaName}`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return undefined;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* 解析多个路由文件
|
|
436
|
+
* @param filePaths 路由文件路径数组
|
|
437
|
+
* @returns 路由信息数组
|
|
438
|
+
*/
|
|
439
|
+
public parseRouteFiles(filePaths: string[]): RouteInfo[] {
|
|
440
|
+
const allRoutes: RouteInfo[] = [];
|
|
441
|
+
for (const filePath of filePaths) {
|
|
442
|
+
const routes = this.parseRouteFile(filePath);
|
|
443
|
+
allRoutes.push(...routes);
|
|
444
|
+
}
|
|
445
|
+
return allRoutes;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* 解析 apidoc 格式的注释标签
|
|
450
|
+
* @param commentText JSDoc 注释文本
|
|
451
|
+
* @returns 解析出的标签信息
|
|
452
|
+
*/
|
|
453
|
+
private parseApiDocTags(commentText: string): ApiDocTags {
|
|
454
|
+
const tags: ApiDocTags = { params: [], success: [] };
|
|
455
|
+
|
|
456
|
+
// 解析 @apiParam 标签
|
|
457
|
+
// 格式: @apiParam {type} [name] description
|
|
458
|
+
const paramRegex = /@apiParam\s+\{([^}]+)\}\s*(?:\[([^\]]+)\]|(\S+))\s*(.*)/g;
|
|
459
|
+
let match;
|
|
460
|
+
while ((match = paramRegex.exec(commentText)) !== null) {
|
|
461
|
+
const [, typeStr, optionalName, requiredName, description] = match;
|
|
462
|
+
const name = optionalName || requiredName;
|
|
463
|
+
const required = !optionalName;
|
|
464
|
+
|
|
465
|
+
// 解析类型字符串
|
|
466
|
+
const typeInfo = this.parseParamType(typeStr);
|
|
467
|
+
|
|
468
|
+
tags.params.push({
|
|
469
|
+
name,
|
|
470
|
+
type: typeInfo.type,
|
|
471
|
+
description: description.trim(),
|
|
472
|
+
required,
|
|
473
|
+
enumValues: typeInfo.enumValues,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// 解析 @apiSuccess 标签
|
|
478
|
+
// 格式: @apiSuccess {type} field 或 @apiSuccess {type} parent.field
|
|
479
|
+
const successRegex = /@apiSuccess\s+\{([^}]+)\}\s+([^\s]+)\s*(.*)/g;
|
|
480
|
+
while ((match = successRegex.exec(commentText)) !== null) {
|
|
481
|
+
const [, typeStr, fieldPath, description] = match;
|
|
482
|
+
|
|
483
|
+
// 解析字段路径 (如 "data.user.id")
|
|
484
|
+
const parts = fieldPath.split(".");
|
|
485
|
+
const name = parts[parts.length - 1];
|
|
486
|
+
const parent = parts.length > 1 ? parts.slice(0, -1).join(".") : undefined;
|
|
487
|
+
|
|
488
|
+
// 解析类型字符串
|
|
489
|
+
const typeInfo = this.parseParamType(typeStr);
|
|
490
|
+
|
|
491
|
+
tags.success.push({
|
|
492
|
+
name,
|
|
493
|
+
type: typeInfo.type,
|
|
494
|
+
description: description.trim(),
|
|
495
|
+
parent,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return tags;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* 解析 apidoc 类型字符串
|
|
504
|
+
* @param typeStr 类型字符串,如 "string", "string=", "number[]", {string="a","b"}
|
|
505
|
+
* @returns 解析后的类型信息
|
|
506
|
+
*/
|
|
507
|
+
private parseParamType(typeStr: string): {
|
|
508
|
+
type: FieldValidation["type"];
|
|
509
|
+
array?: boolean;
|
|
510
|
+
enumValues?: string[];
|
|
511
|
+
} {
|
|
512
|
+
typeStr = typeStr.trim();
|
|
513
|
+
|
|
514
|
+
// 处理枚举类型: {string="a","b"}
|
|
515
|
+
const enumMatch = typeStr.match(/^(\w+)=(.+)$/);
|
|
516
|
+
if (enumMatch) {
|
|
517
|
+
const baseType = enumMatch[1];
|
|
518
|
+
const enumValues = enumMatch[2]
|
|
519
|
+
.split(",")
|
|
520
|
+
.map((v) => v.trim().replace(/^["']|["']$/g, ""));
|
|
521
|
+
|
|
522
|
+
return {
|
|
523
|
+
type: this.mapTypeName(baseType),
|
|
524
|
+
enumValues,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// 处理数组类型: number[]
|
|
529
|
+
if (typeStr.endsWith("[]")) {
|
|
530
|
+
const baseType = typeStr.slice(0, -2);
|
|
531
|
+
return {
|
|
532
|
+
type: this.mapTypeName(baseType),
|
|
533
|
+
array: true,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// 处理可空类型: string?
|
|
538
|
+
if (typeStr.endsWith("?")) {
|
|
539
|
+
const baseType = typeStr.slice(0, -1);
|
|
540
|
+
return {
|
|
541
|
+
type: this.mapTypeName(baseType),
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// 基础类型
|
|
546
|
+
return {
|
|
547
|
+
type: this.mapTypeName(typeStr),
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* 映射类型名称到 FieldValidation 类型
|
|
553
|
+
*/
|
|
554
|
+
private mapTypeName(typeName: string): FieldValidation["type"] {
|
|
555
|
+
const typeMap: Record<string, FieldValidation["type"]> = {
|
|
556
|
+
string: "string",
|
|
557
|
+
number: "number",
|
|
558
|
+
integer: "number",
|
|
559
|
+
boolean: "boolean",
|
|
560
|
+
array: "array",
|
|
561
|
+
object: "object",
|
|
562
|
+
file: "file",
|
|
563
|
+
date: "date",
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
return typeMap[typeName.toLowerCase()] || "any";
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* 从 @apiParam 标签构建请求 Schema
|
|
571
|
+
*/
|
|
572
|
+
private buildRequestSchemaFromTags(
|
|
573
|
+
params: ApiDocTags["params"]
|
|
574
|
+
): RequestSchema {
|
|
575
|
+
const fields: Record<string, FieldValidation> = {};
|
|
576
|
+
|
|
577
|
+
for (const param of params) {
|
|
578
|
+
fields[param.name] = {
|
|
579
|
+
type: param.type as FieldValidation["type"],
|
|
580
|
+
required: param.required,
|
|
581
|
+
constraints: param.enumValues
|
|
582
|
+
? { custom: [`可选值: ${param.enumValues.join(", ")}`] }
|
|
583
|
+
: undefined,
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
return { target: "body", fields };
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* 从 @apiSuccess 标签构建响应 Schema
|
|
592
|
+
*/
|
|
593
|
+
private buildResponseSchemaFromTags(
|
|
594
|
+
success: ApiDocTags["success"]
|
|
595
|
+
): ResponseSchema {
|
|
596
|
+
const fields: Record<string, FieldValidation> = {};
|
|
597
|
+
|
|
598
|
+
// 先处理所有顶级字段,为嵌套字段做准备
|
|
599
|
+
for (const field of success) {
|
|
600
|
+
if (!field.parent && !fields[field.name]) {
|
|
601
|
+
// 顶级字段,如果是 object 类型,添加 properties 属性
|
|
602
|
+
if (field.type === "object") {
|
|
603
|
+
fields[field.name] = {
|
|
604
|
+
type: "object",
|
|
605
|
+
required: true,
|
|
606
|
+
properties: {},
|
|
607
|
+
};
|
|
608
|
+
} else {
|
|
609
|
+
fields[field.name] = {
|
|
610
|
+
type: field.type as FieldValidation["type"],
|
|
611
|
+
required: true,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// 处理嵌套字段
|
|
618
|
+
for (const field of success) {
|
|
619
|
+
if (field.parent) {
|
|
620
|
+
// 处理嵌套字段 (如 data.user.id)
|
|
621
|
+
const parts = field.parent.split(".");
|
|
622
|
+
let current = fields;
|
|
623
|
+
|
|
624
|
+
// 创建/获取父级对象链
|
|
625
|
+
for (let i = 0; i < parts.length; i++) {
|
|
626
|
+
const part = parts[i];
|
|
627
|
+
if (!current[part]) {
|
|
628
|
+
// 如果父级字段不存在,创建它
|
|
629
|
+
current[part] = {
|
|
630
|
+
type: "object",
|
|
631
|
+
required: true,
|
|
632
|
+
properties: {},
|
|
633
|
+
};
|
|
634
|
+
} else if (!current[part].properties) {
|
|
635
|
+
// 如果父级字段存在但没有 properties(作为顶级字段创建的),添加 properties
|
|
636
|
+
current[part].properties = {};
|
|
637
|
+
}
|
|
638
|
+
// 进入到下一级的 properties
|
|
639
|
+
current = current[part].properties!;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// 添加叶子节点到最内层对象的 properties
|
|
643
|
+
current[field.name] = {
|
|
644
|
+
type: field.type as FieldValidation["type"],
|
|
645
|
+
required: true,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return { fields };
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// 导出单例实例
|
|
655
|
+
export const routeParser = new RouteParser();
|