@mindbase/express-common 1.0.7 → 1.0.9
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 +16 -12
- 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 -360
- package/dist/index.mjs +0 -2449
- package/dist/index.mjs.map +0 -1
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Express, Router } from "express";
|
|
2
|
+
import { ScanResult } from "../types/Index";
|
|
3
|
+
import logger from "./Logger";
|
|
4
|
+
import { routeParser } from "./RouteParser";
|
|
5
|
+
import { docManager } from "./DocManager";
|
|
6
|
+
import path from "path";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 递归提取 Router 中注册的所有路由路径
|
|
10
|
+
*/
|
|
11
|
+
function getRoutes(router: any, prefix: string = ""): { path: string; method: string }[] {
|
|
12
|
+
const routes: { path: string; method: string }[] = [];
|
|
13
|
+
|
|
14
|
+
if (router && router.stack) {
|
|
15
|
+
router.stack.forEach((layer: any) => {
|
|
16
|
+
if (layer.route) {
|
|
17
|
+
// 处理直接定义的路由 (如 router.get('/hello'))
|
|
18
|
+
const path = (prefix + layer.route.path).replace(/\/+/g, "/");
|
|
19
|
+
const methods = Object.keys(layer.route.methods).map((m) => m.toUpperCase());
|
|
20
|
+
methods.forEach((method) => {
|
|
21
|
+
routes.push({ path, method });
|
|
22
|
+
});
|
|
23
|
+
} else if (layer.name === "router" && layer.handle && layer.handle.stack) {
|
|
24
|
+
// 处理嵌套的 Router (如 router.use('/sub', subRouter))
|
|
25
|
+
const newPrefix = (prefix + (layer.regexp.source.replace("^\\/", "").replace("\\/?(?=\\/|$)", "") || "")).replace(/\/+/g, "/");
|
|
26
|
+
routes.push(...getRoutes(layer.handle, newPrefix));
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return routes;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function registerRoute(app: Express, config: ScanResult, apiPrefix: string = "/api"): Promise<void> {
|
|
35
|
+
const { fileName, defaultExport: handler, filePath, cacheHit } = config;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const lowercaseModuleName = fileName.toLowerCase();
|
|
39
|
+
|
|
40
|
+
// 缓存未命中时才解析和保存文档
|
|
41
|
+
if (!cacheHit) {
|
|
42
|
+
const routes = routeParser.parseRouteFile(filePath);
|
|
43
|
+
routes.forEach((route) => {
|
|
44
|
+
docManager.saveRoute(route, apiPrefix, lowercaseModuleName);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ===== 支持从子目录生成嵌套 URL =====
|
|
49
|
+
// 例如:d:/packages/auth/routes/auth/user.route.ts → /api/auth/user
|
|
50
|
+
|
|
51
|
+
// 正确找到 routes 目录位置
|
|
52
|
+
const filePathParts = filePath.split(path.sep);
|
|
53
|
+
const routesIndex = filePathParts.lastIndexOf("routes");
|
|
54
|
+
if (routesIndex === -1) {
|
|
55
|
+
throw new Error(`路由文件必须在 routes 目录下: ${filePath}`);
|
|
56
|
+
}
|
|
57
|
+
const routesDir = filePathParts.slice(0, routesIndex + 1).join(path.sep);
|
|
58
|
+
|
|
59
|
+
// 获取相对于 routes 目录的路径
|
|
60
|
+
const fileDir = path.dirname(filePath);
|
|
61
|
+
const relativePath = path.relative(routesDir, fileDir);
|
|
62
|
+
|
|
63
|
+
// 构建路径部分
|
|
64
|
+
let pathParts: string[] = [];
|
|
65
|
+
if (relativePath && relativePath !== ".") {
|
|
66
|
+
pathParts = relativePath.split(path.sep).map(p => p.toLowerCase());
|
|
67
|
+
}
|
|
68
|
+
// 添加文件名作为最后一部分
|
|
69
|
+
pathParts.push(lowercaseModuleName);
|
|
70
|
+
|
|
71
|
+
const baseRoutePath = `${apiPrefix}/${pathParts.join("/")}`.replace(/\/+/g, "/");
|
|
72
|
+
// ===== 嵌套 URL 支持结束 =====
|
|
73
|
+
|
|
74
|
+
app.use(baseRoutePath, handler);
|
|
75
|
+
|
|
76
|
+
// 提取并打印所有子路由
|
|
77
|
+
const subRoutes = getRoutes(handler);
|
|
78
|
+
if (subRoutes.length > 0) {
|
|
79
|
+
logger.startup("路由", `${baseRoutePath} (${subRoutes.length} 个端点)`);
|
|
80
|
+
subRoutes.forEach((route) => {
|
|
81
|
+
const fullPath = `${baseRoutePath}${route.path}`.replace(/\/+/g, "/");
|
|
82
|
+
// Method 填充到 6 字符宽度,保证 path 对齐
|
|
83
|
+
const method = route.method.padEnd(6, " ");
|
|
84
|
+
logger.startup(` └─ ${method} ${fullPath}`);
|
|
85
|
+
});
|
|
86
|
+
} else {
|
|
87
|
+
logger.startup("路由", `${baseRoutePath} (无端点)`);
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
logger.error(`注册路由失败[${fileName.toLowerCase()}]: ${error}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Node,
|
|
3
|
+
SyntaxKind,
|
|
4
|
+
SourceFile,
|
|
5
|
+
InterfaceDeclaration,
|
|
6
|
+
TypeAliasDeclaration,
|
|
7
|
+
PropertySignature,
|
|
8
|
+
TypeReferenceNode,
|
|
9
|
+
ArrayTypeNode,
|
|
10
|
+
UnionTypeNode,
|
|
11
|
+
IntersectionTypeNode,
|
|
12
|
+
LiteralTypeNode,
|
|
13
|
+
Identifier,
|
|
14
|
+
} from "ts-morph";
|
|
15
|
+
import { FieldValidation, ResponseSchema } from "../types/DocTypes";
|
|
16
|
+
import logger from "./Logger";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 类型引用解析结果
|
|
20
|
+
*/
|
|
21
|
+
interface TypeRef {
|
|
22
|
+
typeName: string;
|
|
23
|
+
typeArgs: string[]; // 泛型参数
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* TypeScript 类型解析器
|
|
28
|
+
* 从 interface/type 定义中提取字段结构
|
|
29
|
+
*/
|
|
30
|
+
export class TSTypeParser {
|
|
31
|
+
/**
|
|
32
|
+
* 解析类型引用字符串
|
|
33
|
+
* 例如: "ApiResponse<User[]>" -> { typeName: "ApiResponse", typeArgs: ["User[]"] }
|
|
34
|
+
*/
|
|
35
|
+
public parseTypeRef(typeString: string): TypeRef | null {
|
|
36
|
+
// 移除花括号内的内容(如果有)
|
|
37
|
+
typeString = typeString.trim();
|
|
38
|
+
|
|
39
|
+
// 匹配泛型: TypeName<Arg1, Arg2>
|
|
40
|
+
const genericMatch = typeString.match(/^(\w+)<(.+)>$/);
|
|
41
|
+
if (genericMatch) {
|
|
42
|
+
const typeName = genericMatch[1];
|
|
43
|
+
const argsStr = genericMatch[2];
|
|
44
|
+
// 简单分割泛型参数(不支持嵌套泛型)
|
|
45
|
+
const typeArgs = argsStr.split(",").map((s) => s.trim());
|
|
46
|
+
return { typeName, typeArgs };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 简单类型引用
|
|
50
|
+
if (/^\w+$/.test(typeString)) {
|
|
51
|
+
return { typeName: typeString, typeArgs: [] };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 数组类型: User[]
|
|
55
|
+
const arrayMatch = typeString.match(/^(\w+)\[\]$/);
|
|
56
|
+
if (arrayMatch) {
|
|
57
|
+
return { typeName: "Array", typeArgs: [arrayMatch[1]] };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 解析响应类型
|
|
65
|
+
* @param sourceFile 源文件
|
|
66
|
+
* @param typeString 类型字符串如 "ApiResponse<User[]>"
|
|
67
|
+
* @param imports 导入映射 (类型名 -> 文件路径)
|
|
68
|
+
*/
|
|
69
|
+
public parseResponseType(
|
|
70
|
+
sourceFile: SourceFile,
|
|
71
|
+
typeString: string,
|
|
72
|
+
imports: Map<string, string>
|
|
73
|
+
): ResponseSchema | null {
|
|
74
|
+
const typeRef = this.parseTypeRef(typeString);
|
|
75
|
+
if (!typeRef) {
|
|
76
|
+
logger.debug(`无法解析类型引用: ${typeString}`);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 内置类型处理: ApiResponse<T>
|
|
81
|
+
if (typeRef.typeName === "ApiResponse") {
|
|
82
|
+
return this.parseApiResponse(sourceFile, typeRef.typeArgs, imports);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 查找类型定义
|
|
86
|
+
const typeDecl = this.findTypeDeclaration(sourceFile, typeRef.typeName, imports);
|
|
87
|
+
if (!typeDecl) {
|
|
88
|
+
logger.debug(`未找到类型定义: ${typeRef.typeName}`);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 解析字段
|
|
93
|
+
const fields = this.extractFieldsFromType(typeDecl, typeRef.typeArgs, imports);
|
|
94
|
+
|
|
95
|
+
return { fields };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 解析 ApiResponse<T> 类型
|
|
100
|
+
* ApiResponse 是标准的 API 响应包装器,包含 code, data, msg 字段
|
|
101
|
+
*/
|
|
102
|
+
private parseApiResponse(
|
|
103
|
+
sourceFile: SourceFile,
|
|
104
|
+
typeArgs: string[],
|
|
105
|
+
imports: Map<string, string>
|
|
106
|
+
): ResponseSchema {
|
|
107
|
+
const fields: Record<string, FieldValidation> = {
|
|
108
|
+
code: { type: "number", required: true },
|
|
109
|
+
msg: { type: "string", required: true },
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// 解析 data 字段的类型
|
|
113
|
+
if (typeArgs.length > 0) {
|
|
114
|
+
const dataType = typeArgs[0];
|
|
115
|
+
const dataField = this.parseDataType(sourceFile, dataType, imports);
|
|
116
|
+
fields.data = dataField;
|
|
117
|
+
} else {
|
|
118
|
+
fields.data = { type: "any", required: true };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { fields };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 解析 data 字段的类型
|
|
126
|
+
*/
|
|
127
|
+
private parseDataType(
|
|
128
|
+
sourceFile: SourceFile,
|
|
129
|
+
typeString: string,
|
|
130
|
+
imports: Map<string, string>
|
|
131
|
+
): FieldValidation {
|
|
132
|
+
// 基础类型
|
|
133
|
+
if (typeString === "string") {
|
|
134
|
+
return { type: "string", required: true };
|
|
135
|
+
} else if (typeString === "number") {
|
|
136
|
+
return { type: "number", required: true };
|
|
137
|
+
} else if (typeString === "boolean") {
|
|
138
|
+
return { type: "boolean", required: true };
|
|
139
|
+
} else if (typeString === "any") {
|
|
140
|
+
return { type: "any", required: true };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 数组类型: User[]
|
|
144
|
+
const arrayMatch = typeString.match(/^(\w+)\[\]$/);
|
|
145
|
+
if (arrayMatch) {
|
|
146
|
+
const elementType = arrayMatch[1];
|
|
147
|
+
const elementField = this.parseDataType(sourceFile, elementType, imports);
|
|
148
|
+
return {
|
|
149
|
+
type: "array",
|
|
150
|
+
required: true,
|
|
151
|
+
items: elementField,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 类型引用: LoginResult
|
|
156
|
+
const typeDecl = this.findTypeDeclaration(sourceFile, typeString, imports);
|
|
157
|
+
if (typeDecl) {
|
|
158
|
+
const fields = this.extractFieldsFromType(typeDecl, [], imports);
|
|
159
|
+
return {
|
|
160
|
+
type: "object",
|
|
161
|
+
required: true,
|
|
162
|
+
properties: fields,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 默认返回 any
|
|
167
|
+
return { type: "any", required: true };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 查找类型声明
|
|
172
|
+
*/
|
|
173
|
+
private findTypeDeclaration(
|
|
174
|
+
sourceFile: SourceFile,
|
|
175
|
+
typeName: string,
|
|
176
|
+
imports: Map<string, string>
|
|
177
|
+
): InterfaceDeclaration | TypeAliasDeclaration | null {
|
|
178
|
+
// 先在当前文件中查找
|
|
179
|
+
const interfaceDecl = sourceFile.getInterface(typeName);
|
|
180
|
+
if (interfaceDecl) return interfaceDecl;
|
|
181
|
+
|
|
182
|
+
const typeAlias = sourceFile.getTypeAlias(typeName);
|
|
183
|
+
if (typeAlias) return typeAlias;
|
|
184
|
+
|
|
185
|
+
// 在导入的文件中查找
|
|
186
|
+
const importPath = imports.get(typeName);
|
|
187
|
+
if (importPath) {
|
|
188
|
+
try {
|
|
189
|
+
const importedFile = sourceFile.getProject().addSourceFileAtPath(importPath);
|
|
190
|
+
|
|
191
|
+
const importedInterface = importedFile.getInterface(typeName);
|
|
192
|
+
if (importedInterface) return importedInterface;
|
|
193
|
+
|
|
194
|
+
const importedTypeAlias = importedFile.getTypeAlias(typeName);
|
|
195
|
+
if (importedTypeAlias) return importedTypeAlias;
|
|
196
|
+
} catch (error) {
|
|
197
|
+
logger.warn(`无法加载导入文件: ${importPath}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* 从类型声明中提取字段
|
|
206
|
+
*/
|
|
207
|
+
private extractFieldsFromType(
|
|
208
|
+
typeDecl: InterfaceDeclaration | TypeAliasDeclaration,
|
|
209
|
+
typeArgs: string[],
|
|
210
|
+
imports: Map<string, string>
|
|
211
|
+
): Record<string, FieldValidation> {
|
|
212
|
+
const fields: Record<string, FieldValidation> = {};
|
|
213
|
+
|
|
214
|
+
// 获取类型节点
|
|
215
|
+
let typeNode: Node | undefined;
|
|
216
|
+
|
|
217
|
+
if (Node.isInterfaceDeclaration(typeDecl)) {
|
|
218
|
+
// 接口: 直接遍历属性
|
|
219
|
+
for (const prop of typeDecl.getProperties()) {
|
|
220
|
+
const fieldName = prop.getName();
|
|
221
|
+
const fieldType = prop.getType();
|
|
222
|
+
fields[fieldName] = this.parsePropertyType(prop, typeArgs, imports);
|
|
223
|
+
}
|
|
224
|
+
return fields;
|
|
225
|
+
} else if (Node.isTypeAliasDeclaration(typeDecl)) {
|
|
226
|
+
typeNode = typeDecl.getTypeNode();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (!typeNode) return fields;
|
|
230
|
+
|
|
231
|
+
// 处理类型别名 - 使用 getKind() 检查节点类型
|
|
232
|
+
if (typeNode.getKind() === SyntaxKind.TypeReference) {
|
|
233
|
+
// 如果是类型引用,可能需要展开
|
|
234
|
+
return this.extractFieldsFromTypeReference(typeNode as TypeReferenceNode, typeArgs, imports);
|
|
235
|
+
} else if (typeNode.getKind() === SyntaxKind.IntersectionType) {
|
|
236
|
+
// 交叉类型: 合并所有类型的字段
|
|
237
|
+
for (const child of (typeNode as IntersectionTypeNode).getTypeNodes()) {
|
|
238
|
+
const childFields = this.extractFieldsFromTypeNode(child, typeArgs, imports);
|
|
239
|
+
Object.assign(fields, childFields);
|
|
240
|
+
}
|
|
241
|
+
} else if (typeNode.getKind() === SyntaxKind.UnionType) {
|
|
242
|
+
// 联合类型: 取第一个类型的字段(简化处理)
|
|
243
|
+
const firstType = (typeNode as UnionTypeNode).getTypeNodes()[0];
|
|
244
|
+
if (firstType) {
|
|
245
|
+
return this.extractFieldsFromTypeNode(firstType, typeArgs, imports);
|
|
246
|
+
}
|
|
247
|
+
} else {
|
|
248
|
+
return this.extractFieldsFromTypeNode(typeNode, typeArgs, imports);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return fields;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* 从类型节点提取字段
|
|
256
|
+
*/
|
|
257
|
+
private extractFieldsFromTypeNode(
|
|
258
|
+
typeNode: Node,
|
|
259
|
+
typeArgs: string[],
|
|
260
|
+
imports: Map<string, string>
|
|
261
|
+
): Record<string, FieldValidation> {
|
|
262
|
+
const fields: Record<string, FieldValidation> = {};
|
|
263
|
+
|
|
264
|
+
if (typeNode.getKind() === SyntaxKind.TypeReference) {
|
|
265
|
+
return this.extractFieldsFromTypeReference(typeNode as TypeReferenceNode, typeArgs, imports);
|
|
266
|
+
} else if (typeNode.getKind() === SyntaxKind.IntersectionType) {
|
|
267
|
+
for (const child of (typeNode as IntersectionTypeNode).getTypeNodes()) {
|
|
268
|
+
const childFields = this.extractFieldsFromTypeNode(child, typeArgs, imports);
|
|
269
|
+
Object.assign(fields, childFields);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return fields;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* 从类型引用提取字段
|
|
278
|
+
*/
|
|
279
|
+
private extractFieldsFromTypeReference(
|
|
280
|
+
typeRefNode: TypeReferenceNode,
|
|
281
|
+
typeArgs: string[],
|
|
282
|
+
imports: Map<string, string>
|
|
283
|
+
): Record<string, FieldValidation> {
|
|
284
|
+
const fields: Record<string, FieldValidation> = {};
|
|
285
|
+
const typeName = typeRefNode.getTypeName().getText();
|
|
286
|
+
|
|
287
|
+
// 特殊处理泛型参数
|
|
288
|
+
if (typeName === "Array" || typeName === "ReadonlyArray") {
|
|
289
|
+
// 数组类型,返回 items 信息
|
|
290
|
+
const typeArgsNodes = typeRefNode.getTypeArguments();
|
|
291
|
+
if (typeArgsNodes.length > 0) {
|
|
292
|
+
const elementType = typeArgsNodes[0];
|
|
293
|
+
fields["[]"] = {
|
|
294
|
+
type: "array",
|
|
295
|
+
required: true,
|
|
296
|
+
items: this.parseTypeNodeToFieldValidation(elementType, typeArgs, imports),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
return fields;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// 处理 Promise<T> - 提取 T
|
|
303
|
+
if (typeName === "Promise") {
|
|
304
|
+
const typeArgsNodes = typeRefNode.getTypeArguments();
|
|
305
|
+
if (typeArgsNodes.length > 0) {
|
|
306
|
+
return this.extractFieldsFromTypeNode(typeArgsNodes[0], typeArgs, imports);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 处理泛型参数 T, K 等
|
|
311
|
+
if (typeArgs.includes(typeName) && typeArgs.length > 0) {
|
|
312
|
+
// 使用实际类型参数
|
|
313
|
+
const actualType = typeArgs[0]; // 简化处理,取第一个
|
|
314
|
+
// 这里可以递归解析实际类型
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// 解析类型定义
|
|
318
|
+
const sourceFile = typeRefNode.getSourceFile();
|
|
319
|
+
const typeDecl = this.findTypeDeclarationInFile(sourceFile, typeName);
|
|
320
|
+
|
|
321
|
+
if (typeDecl) {
|
|
322
|
+
return this.extractFieldsFromType(typeDecl, typeArgs, imports);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return fields;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* 在文件中查找类型声明
|
|
330
|
+
*/
|
|
331
|
+
private findTypeDeclarationInFile(
|
|
332
|
+
sourceFile: SourceFile,
|
|
333
|
+
typeName: string
|
|
334
|
+
): InterfaceDeclaration | TypeAliasDeclaration | null {
|
|
335
|
+
const interfaceDecl = sourceFile.getInterface(typeName);
|
|
336
|
+
if (interfaceDecl) return interfaceDecl;
|
|
337
|
+
|
|
338
|
+
const typeAlias = sourceFile.getTypeAlias(typeName);
|
|
339
|
+
if (typeAlias) return typeAlias;
|
|
340
|
+
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* 解析属性类型
|
|
346
|
+
*/
|
|
347
|
+
private parsePropertyType(
|
|
348
|
+
prop: PropertySignature,
|
|
349
|
+
typeArgs: string[],
|
|
350
|
+
imports: Map<string, string>
|
|
351
|
+
): FieldValidation {
|
|
352
|
+
const type = prop.getType();
|
|
353
|
+
const isOptional = prop.hasQuestionToken();
|
|
354
|
+
|
|
355
|
+
// 获取类型文本
|
|
356
|
+
const typeText = type.getText();
|
|
357
|
+
|
|
358
|
+
// 基础类型判断
|
|
359
|
+
if (type.isString()) {
|
|
360
|
+
return { type: "string", required: !isOptional };
|
|
361
|
+
} else if (type.isNumber()) {
|
|
362
|
+
return { type: "number", required: !isOptional };
|
|
363
|
+
} else if (type.isBoolean()) {
|
|
364
|
+
return { type: "boolean", required: !isOptional };
|
|
365
|
+
} else if (type.isArray()) {
|
|
366
|
+
const elementType = type.getArrayElementType();
|
|
367
|
+
let items: FieldValidation | undefined;
|
|
368
|
+
if (elementType) {
|
|
369
|
+
items = {
|
|
370
|
+
type: this.mapTypeToString(elementType),
|
|
371
|
+
required: !elementType.isUndefined() && !elementType.isNull(),
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
return { type: "array", required: !isOptional, items };
|
|
375
|
+
} else if (type.isObject()) {
|
|
376
|
+
// 尝试获取对象属性
|
|
377
|
+
const properties = type.getProperties();
|
|
378
|
+
if (properties.length > 0) {
|
|
379
|
+
const props: Record<string, FieldValidation> = {};
|
|
380
|
+
for (const prop of properties) {
|
|
381
|
+
const propName = prop.getName();
|
|
382
|
+
const propType = prop.getTypeAtLocation(prop.getValueDeclaration() || prop.getDeclarations()[0]);
|
|
383
|
+
props[propName] = {
|
|
384
|
+
type: this.mapTypeToString(propType),
|
|
385
|
+
required: !prop.isOptional(),
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
return { type: "object", required: !isOptional, properties: props };
|
|
389
|
+
}
|
|
390
|
+
return { type: "object", required: !isOptional };
|
|
391
|
+
} else if (type.isNull()) {
|
|
392
|
+
return { type: "any", required: false, nullable: true };
|
|
393
|
+
} else if (type.isUndefined()) {
|
|
394
|
+
return { type: "any", required: false, optional: true };
|
|
395
|
+
} else if (type.isAny()) {
|
|
396
|
+
return { type: "any", required: !isOptional };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// 其他类型,使用类型文本
|
|
400
|
+
return { type: "any", required: !isOptional };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* 将类型映射为字符串
|
|
405
|
+
*/
|
|
406
|
+
private mapTypeToString(type: any): FieldValidation["type"] {
|
|
407
|
+
if (type.isString()) return "string";
|
|
408
|
+
if (type.isNumber()) return "number";
|
|
409
|
+
if (type.isBoolean()) return "boolean";
|
|
410
|
+
if (type.isArray()) return "array";
|
|
411
|
+
if (type.isObject()) return "object";
|
|
412
|
+
if (type.isDate()) return "date";
|
|
413
|
+
return "any";
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* 从类型节点创建 FieldValidation
|
|
418
|
+
*/
|
|
419
|
+
private parseTypeNodeToFieldValidation(
|
|
420
|
+
typeNode: Node,
|
|
421
|
+
typeArgs: string[],
|
|
422
|
+
imports: Map<string, string>
|
|
423
|
+
): FieldValidation {
|
|
424
|
+
if (Node.isStringKeyword(typeNode)) {
|
|
425
|
+
return { type: "string", required: true };
|
|
426
|
+
} else if (Node.isNumberKeyword(typeNode)) {
|
|
427
|
+
return { type: "number", required: true };
|
|
428
|
+
} else if (Node.isBooleanKeyword(typeNode)) {
|
|
429
|
+
return { type: "boolean", required: true };
|
|
430
|
+
} else if (Node.isArrayTypeNode(typeNode)) {
|
|
431
|
+
const elementType = (typeNode as ArrayTypeNode).getElementTypeNode();
|
|
432
|
+
return {
|
|
433
|
+
type: "array",
|
|
434
|
+
required: true,
|
|
435
|
+
items: this.parseTypeNodeToFieldValidation(elementType, typeArgs, imports),
|
|
436
|
+
};
|
|
437
|
+
} else if (typeNode.getKind() === SyntaxKind.TypeReference) {
|
|
438
|
+
const refNode = typeNode as TypeReferenceNode;
|
|
439
|
+
const typeName = refNode.getTypeName().getText();
|
|
440
|
+
const sourceFile = refNode.getSourceFile();
|
|
441
|
+
const typeDecl = this.findTypeDeclarationInFile(sourceFile, typeName);
|
|
442
|
+
|
|
443
|
+
if (typeDecl) {
|
|
444
|
+
const fields = this.extractFieldsFromType(typeDecl, typeArgs, imports);
|
|
445
|
+
return { type: "object", required: true, properties: fields };
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return { type: "any", required: true };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return { type: "any", required: true };
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// 导出单例实例
|
|
456
|
+
export const tsTypeParser = new TSTypeParser();
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from "express";
|
|
2
|
+
import { AnyZodObject, ZodError, ZodSchema } from "zod";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 验证位置类型
|
|
6
|
+
*/
|
|
7
|
+
export type ValidationTarget = "body" | "query" | "params";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Zod 验证中间件
|
|
11
|
+
* @param schema Zod Schema
|
|
12
|
+
* @param target 验证目标 (默认 body)
|
|
13
|
+
*/
|
|
14
|
+
export const validate = (schema: ZodSchema, target: ValidationTarget = "body") => {
|
|
15
|
+
return async (req: Request, res: Response, next: NextFunction) => {
|
|
16
|
+
try {
|
|
17
|
+
await schema.parseAsync(req[target]);
|
|
18
|
+
// 注意: req.query 和 req.params 是只读的,无法赋值
|
|
19
|
+
// 只有 req.body 可以赋值,但通常 parse 验证后类型转换已生效
|
|
20
|
+
next();
|
|
21
|
+
} catch (error) {
|
|
22
|
+
next(error);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
};
|