@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.
@@ -0,0 +1,420 @@
1
+ import {
2
+ Node,
3
+ SyntaxKind,
4
+ SourceFile,
5
+ VariableDeclaration,
6
+ CallExpression,
7
+ ObjectLiteralExpression,
8
+ PropertyAccessExpression,
9
+ Identifier,
10
+ StringLiteral,
11
+ NumericLiteral,
12
+ AsExpression,
13
+ } from "ts-morph";
14
+ import { FieldValidation, FieldConstraints } from "../types/DocTypes";
15
+ import logger from "./Logger";
16
+
17
+ /**
18
+ * Zod Schema AST 解析器
19
+ * 从 Zod schema 变量定义中提取字段结构
20
+ */
21
+ export class ZodSchemaParser {
22
+ /**
23
+ * 解析 schema 变量,返回字段结构
24
+ */
25
+ public parseSchemaVariable(
26
+ sourceFile: SourceFile,
27
+ schemaName: string
28
+ ): Record<string, FieldValidation> | null {
29
+ try {
30
+ // 1. 查找 schema 变量声明
31
+ const variableDecl = this.findVariableDeclaration(sourceFile, schemaName);
32
+ if (!variableDecl) {
33
+ logger.warn(`未找到 schema 变量: ${schemaName}`);
34
+ return null;
35
+ }
36
+
37
+ // 2. 获取初始化表达式
38
+ const initializer = variableDecl.getInitializer();
39
+ if (!initializer) {
40
+ logger.warn(`schema 变量 ${schemaName} 没有初始化表达式`);
41
+ return null;
42
+ }
43
+
44
+ // 3. 解析 z.object({...}) 调用
45
+ return this.parseZodObject(initializer);
46
+ } catch (error) {
47
+ logger.error(`解析 schema 变量失败: ${schemaName}`, error);
48
+ return null;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * 查找变量声明
54
+ */
55
+ private findVariableDeclaration(
56
+ sourceFile: SourceFile,
57
+ name: string
58
+ ): VariableDeclaration | null {
59
+ for (const decl of sourceFile.getVariableDeclarations()) {
60
+ if (decl.getName() === name) {
61
+ return decl;
62
+ }
63
+ }
64
+ return null;
65
+ }
66
+
67
+ /**
68
+ * 解析 z.object({...}) 调用
69
+ */
70
+ private parseZodObject(node: Node): Record<string, FieldValidation> | null {
71
+ // 处理 as any 类型断言
72
+ let targetNode = node;
73
+ if (Node.isAsExpression(node)) {
74
+ targetNode = node.getExpression();
75
+ }
76
+
77
+ // 检查是否是 z.object() 调用
78
+ if (Node.isCallExpression(targetNode)) {
79
+ const expr = targetNode.getExpression();
80
+ if (Node.isPropertyAccessExpression(expr)) {
81
+ const propText = expr.getText();
82
+ if (propText === "z.object") {
83
+ const args = targetNode.getArguments();
84
+ if (args.length > 0 && Node.isObjectLiteralExpression(args[0])) {
85
+ return this.parseObjectLiteral(args[0]);
86
+ }
87
+ }
88
+ }
89
+ }
90
+ return null;
91
+ }
92
+
93
+ /**
94
+ * 解析对象字面量中的字段定义
95
+ */
96
+ private parseObjectLiteral(
97
+ objLit: ObjectLiteralExpression
98
+ ): Record<string, FieldValidation> {
99
+ const fields: Record<string, FieldValidation> = {};
100
+
101
+ for (const prop of objLit.getProperties()) {
102
+ if (Node.isPropertyAssignment(prop)) {
103
+ const fieldName = prop.getName();
104
+ const initializer = prop.getInitializer();
105
+ if (initializer) {
106
+ fields[fieldName] = this.parseZodField(initializer);
107
+ }
108
+ }
109
+ }
110
+
111
+ return fields;
112
+ }
113
+
114
+ /**
115
+ * 解析单个 Zod 字段定义
116
+ */
117
+ private parseZodField(node: Node): FieldValidation {
118
+ const field: FieldValidation = {
119
+ type: "any",
120
+ required: true,
121
+ };
122
+
123
+ // 处理 as any 类型断言
124
+ let targetNode = node;
125
+ if (Node.isAsExpression(node)) {
126
+ targetNode = node.getExpression();
127
+ }
128
+
129
+ // 收集方法调用链
130
+ const callChain = this.collectCallChain(targetNode);
131
+
132
+ // 解析类型和约束
133
+ for (const call of callChain) {
134
+ this.applyZodMethod(field, call);
135
+ }
136
+
137
+ return field;
138
+ }
139
+
140
+ /**
141
+ * 收集方法调用链
142
+ * 例如: z.string().min(1).optional() -> [{method: "z.string", args: []}, {method: "min", args: [...]}, {method: "optional", args: []}]
143
+ */
144
+ private collectCallChain(
145
+ node: Node
146
+ ): Array<{ method: string; args: Node[] }> {
147
+ const chain: Array<{ method: string; args: Node[] }> = [];
148
+
149
+ let current: Node | undefined = node;
150
+ while (current && Node.isCallExpression(current)) {
151
+ const expr = current.getExpression();
152
+ const args = current.getArguments();
153
+
154
+ if (Node.isPropertyAccessExpression(expr)) {
155
+ const methodName = expr.getName();
156
+ chain.unshift({ method: methodName, args: [...args] });
157
+ current = expr.getExpression();
158
+ } else if (Node.isIdentifier(expr)) {
159
+ // z.string, z.number 等基础类型
160
+ chain.unshift({ method: expr.getText(), args: [] });
161
+ break;
162
+ } else if (Node.isPropertyAccessExpression(expr)) {
163
+ // 处理 z.coerce.number() 这种链式
164
+ chain.unshift({
165
+ method: (expr as PropertyAccessExpression).getName(),
166
+ args: [],
167
+ });
168
+ current = (expr as PropertyAccessExpression).getExpression();
169
+ } else {
170
+ break;
171
+ }
172
+ }
173
+
174
+ return chain;
175
+ }
176
+
177
+ /**
178
+ * 应用 Zod 方法到字段定义
179
+ */
180
+ private applyZodMethod(
181
+ field: FieldValidation,
182
+ call: { method: string; args: Node[] }
183
+ ): void {
184
+ const { method, args } = call;
185
+
186
+ switch (method) {
187
+ // 类型定义
188
+ case "z.string":
189
+ case "string":
190
+ field.type = "string";
191
+ break;
192
+ case "z.number":
193
+ case "number":
194
+ field.type = "number";
195
+ break;
196
+ case "z.boolean":
197
+ case "boolean":
198
+ field.type = "boolean";
199
+ break;
200
+ case "z.array":
201
+ case "array":
202
+ field.type = "array";
203
+ // 尝试解析数组元素类型
204
+ if (args.length > 0) {
205
+ field.items = this.parseZodField(args[0]);
206
+ }
207
+ break;
208
+ case "z.object":
209
+ case "object":
210
+ field.type = "object";
211
+ // 尝试解析对象属性
212
+ if (args.length > 0 && Node.isObjectLiteralExpression(args[0])) {
213
+ field.properties = this.parseObjectLiteral(args[0]);
214
+ }
215
+ break;
216
+ case "z.date":
217
+ case "date":
218
+ field.type = "date";
219
+ break;
220
+ case "z.any":
221
+ case "any":
222
+ field.type = "any";
223
+ break;
224
+ case "z.unknown":
225
+ case "unknown":
226
+ field.type = "any";
227
+ break;
228
+ case "z.undefined":
229
+ case "undefined":
230
+ field.required = false;
231
+ field.optional = true;
232
+ break;
233
+ case "z.null":
234
+ case "null":
235
+ field.nullable = true;
236
+ break;
237
+ case "z.void":
238
+ case "void":
239
+ field.type = "any";
240
+ break;
241
+ case "z.nativeEnum":
242
+ case "nativeEnum":
243
+ case "z.enum":
244
+ case "enum":
245
+ field.type = "string";
246
+ break;
247
+ case "coerce":
248
+ // z.coerce 是属性访问,不做处理,后续调用会设置类型
249
+ break;
250
+
251
+ // 可选性
252
+ case "optional":
253
+ field.required = false;
254
+ field.optional = true;
255
+ break;
256
+ case "nullable":
257
+ field.nullable = true;
258
+ break;
259
+ case "nullish":
260
+ field.required = false;
261
+ field.optional = true;
262
+ field.nullable = true;
263
+ break;
264
+
265
+ // 默认值
266
+ case "default":
267
+ if (args.length > 0) {
268
+ field.defaultValue = this.parseArgumentValue(args[0]);
269
+ field.required = false;
270
+ }
271
+ break;
272
+
273
+ // 约束 - 通用
274
+ case "min":
275
+ if (args.length > 0) {
276
+ const minVal = this.parseArgumentValue(args[0]);
277
+ field.constraints = field.constraints || {};
278
+ if (field.type === "string") {
279
+ field.constraints.minLength = minVal;
280
+ } else if (field.type === "number") {
281
+ field.constraints.min = minVal;
282
+ }
283
+ // 提取错误消息
284
+ if (args.length > 1) {
285
+ const msg = this.parseArgumentValue(args[1]);
286
+ if (typeof msg === "string") {
287
+ field.constraints.custom = field.constraints.custom || [];
288
+ field.constraints.custom.push(msg);
289
+ }
290
+ }
291
+ }
292
+ break;
293
+ case "max":
294
+ if (args.length > 0) {
295
+ const maxVal = this.parseArgumentValue(args[0]);
296
+ field.constraints = field.constraints || {};
297
+ if (field.type === "string") {
298
+ field.constraints.maxLength = maxVal;
299
+ } else if (field.type === "number") {
300
+ field.constraints.max = maxVal;
301
+ }
302
+ }
303
+ break;
304
+ case "length":
305
+ if (args.length > 0) {
306
+ const lenVal = this.parseArgumentValue(args[0]);
307
+ field.constraints = field.constraints || {};
308
+ field.constraints.minLength = lenVal;
309
+ field.constraints.maxLength = lenVal;
310
+ }
311
+ break;
312
+
313
+ // 约束 - 字符串
314
+ case "email":
315
+ field.constraints = field.constraints || {};
316
+ field.constraints.email = true;
317
+ break;
318
+ case "url":
319
+ field.constraints = field.constraints || {};
320
+ field.constraints.url = true;
321
+ break;
322
+ case "uuid":
323
+ field.constraints = field.constraints || {};
324
+ field.constraints.uuid = true;
325
+ break;
326
+ case "regex":
327
+ case "pattern":
328
+ if (args.length > 0) {
329
+ field.constraints = field.constraints || {};
330
+ field.constraints.pattern = args[0].getText();
331
+ }
332
+ break;
333
+ case "startsWith":
334
+ case "endsWith":
335
+ case "includes":
336
+ case "trim":
337
+ case "toLowerCase":
338
+ case "toUpperCase":
339
+ // 字符串转换方法,暂不处理
340
+ break;
341
+
342
+ // 约束 - 数字
343
+ case "int":
344
+ field.constraints = field.constraints || {};
345
+ field.constraints.int = true;
346
+ break;
347
+ case "positive":
348
+ field.constraints = field.constraints || {};
349
+ field.constraints.positive = true;
350
+ break;
351
+ case "nonnegative":
352
+ field.constraints = field.constraints || {};
353
+ field.constraints.nonnegative = true;
354
+ break;
355
+ case "negative":
356
+ field.constraints = field.constraints || {};
357
+ field.constraints.max = -1;
358
+ break;
359
+ case "nonpositive":
360
+ field.constraints = field.constraints || {};
361
+ field.constraints.max = 0;
362
+ break;
363
+ case "finite":
364
+ case "safe":
365
+ // 数字安全检查,暂不处理
366
+ break;
367
+
368
+ // 约束 - 数组
369
+ case "nonempty":
370
+ field.constraints = field.constraints || {};
371
+ field.constraints.minLength = 1;
372
+ break;
373
+
374
+ // 其他方法
375
+ case "refine":
376
+ case "superRefine":
377
+ case "transform":
378
+ case "pipe":
379
+ case " preprocess":
380
+ case "omit":
381
+ case "pick":
382
+ case "partial":
383
+ case "required":
384
+ case "strict":
385
+ case "passthrough":
386
+ case "extend":
387
+ case "merge":
388
+ case "keyof":
389
+ // 高级方法,暂不处理
390
+ break;
391
+ }
392
+ }
393
+
394
+ /**
395
+ * 解析参数值
396
+ */
397
+ private parseArgumentValue(node: Node): any {
398
+ if (Node.isStringLiteral(node)) {
399
+ return node.getLiteralValue();
400
+ } else if (Node.isNumericLiteral(node)) {
401
+ return node.getLiteralValue();
402
+ } else if (Node.isTrueLiteral(node)) {
403
+ return true;
404
+ } else if (Node.isFalseLiteral(node)) {
405
+ return false;
406
+ } else if (Node.isIdentifier(node)) {
407
+ return node.getText();
408
+ } else if (Node.isObjectLiteralExpression(node)) {
409
+ // 返回对象字面量的文本表示
410
+ return node.getText();
411
+ } else if (Node.isArrayLiteralExpression(node)) {
412
+ // 返回数组字面量的文本表示
413
+ return node.getText();
414
+ }
415
+ return node.getText();
416
+ }
417
+ }
418
+
419
+ // 导出单例实例
420
+ export const zodSchemaParser = new ZodSchemaParser();
@@ -0,0 +1,9 @@
1
+ import { z } from "zod";
2
+
3
+ // 路由ID参数
4
+ export const routeIdParamsSchema = z.object({
5
+ id: z.coerce.number().int().positive("无效的路由ID"),
6
+ });
7
+
8
+ // 类型导出
9
+ export type RouteIdParams = z.infer<typeof routeIdParamsSchema>;