Skip to content

Commit 57dcb3e

Browse files
committed
docs: clarify validator usage in services
1 parent 2cd6957 commit 57dcb3e

5 files changed

Lines changed: 244 additions & 67 deletions

File tree

website/docs/api/app.md

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -589,22 +589,38 @@ import { z } from "zod";
589589
export default definePlugin({
590590
name: "zod-validator",
591591
setup(app) {
592+
const originalValidator = app.getValidator();
593+
592594
app.setValidator({
593595
compile(schema) {
594-
const zodSchema = z.object(schema);
595-
return (data) => {
596-
const result = zodSchema.safeParse(data);
597-
if (result.success) {
598-
return { valid: true, data: result.data };
596+
const toVextResult = (result: ReturnType<z.ZodType["safeParse"]>) =>
597+
result.success
598+
? { valid: true, data: result.data }
599+
: {
600+
valid: false,
601+
errors: result.error.issues.map((issue) => ({
602+
field: issue.path.join("."),
603+
message: issue.message,
604+
})),
605+
};
606+
607+
if (schema instanceof z.ZodType) {
608+
return (data) => toVextResult(schema.safeParse(data));
609+
}
610+
611+
const zodShape: Record<string, z.ZodType> = {};
612+
for (const [key, value] of Object.entries(schema)) {
613+
if (value instanceof z.ZodType) {
614+
zodShape[key] = value;
599615
}
600-
return {
601-
valid: false,
602-
errors: result.error.issues.map((issue) => ({
603-
field: issue.path.join("."),
604-
message: issue.message,
605-
})),
606-
};
607-
};
616+
}
617+
618+
if (Object.keys(zodShape).length > 0) {
619+
const zodSchema = z.object(zodShape);
620+
return (data) => toVextResult(zodSchema.safeParse(data));
621+
}
622+
623+
return originalValidator.compile(schema);
608624
},
609625
});
610626
},
@@ -615,19 +631,46 @@ export default definePlugin({
615631
616632
#### `app.getValidator()`
617633
618-
获取当前校验引擎实例
634+
获取当前全局校验引擎实例
619635
620636
```typescript
621637
getValidator(): VextValidator;
622638
```
623639
640+
默认 validator 基于 schema-dsl 实现。插件可以通过 `app.setValidator()` 将其替换为 Zod、Yup 等实现,因此 `getValidator()` 不等同于固定的 schema-dsl,而是始终返回当前生效的 validator。
641+
624642
```typescript
625643
const validator = app.getValidator();
626644
const validate = validator.compile({ name: "string:1-50" });
627645
const result = validate({ name: "Alice" });
628646
// { valid: true, data: { name: 'Alice' } }
629647
```
630648
649+
service 中处理非 HTTP 输入时也可以复用它:
650+
651+
```typescript
652+
import { VextValidationError, type VextApp, type VextValidator } from "vextjs";
653+
654+
export default class UserService {
655+
private validateCreateUser: ReturnType<VextValidator["compile"]>;
656+
657+
constructor(private app: VextApp) {
658+
this.validateCreateUser = app.getValidator().compile({
659+
name: "string:1-50!",
660+
email: "email!",
661+
});
662+
}
663+
664+
async createFromMessage(input: unknown) {
665+
const result = this.validateCreateUser(input);
666+
if (!result.valid) {
667+
throw new VextValidationError(result.errors ?? []);
668+
}
669+
return result.data;
670+
}
671+
}
672+
```
673+
631674
---
632675
633676
#### `app.setThrow(wrapper)`

website/docs/examples/zod-validation.md

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,17 +52,17 @@ pnpm add zod
5252
```typescript
5353
// src/plugins/zod-validator.ts
5454
import { definePlugin } from "vextjs";
55-
import { ZodType, ZodError } from "zod";
55+
import { z, ZodType } from "zod";
5656

5757
/**
5858
* Zod 校验插件
5959
*
60-
* 替换内置 schema-dsl 校验引擎,使路由 validate 配置支持 Zod schema。
60+
* 替换内置 schema-dsl 校验引擎,使路由 validate 配置支持字段级 Zod schema。
6161
*
6262
* 使用方式:
6363
* validate: {
64-
* body: userCreateSchema, // 直接传入 Zod schema 对象
65-
* query: paginationSchema,
64+
* body: userCreateSchema.shape, // 字段级 Zod schema 对象
65+
* query: paginationSchema.shape,
6666
* }
6767
*
6868
* 校验流程:
@@ -114,15 +114,14 @@ export default definePlugin({
114114
//
115115
// 支持 validate: { body: { name: z.string(), age: z.number() } }
116116
// 这种「对象字段为 Zod」的混合模式。
117-
// 但更推荐直接传入完整的 z.object({...}) 作为 schema
117+
// 这种写法与 RouteOptions.validate 的公开类型保持一致
118118

119119
const hasZodFields = Object.values(schema).some(
120120
(v) => v instanceof ZodType,
121121
);
122122

123123
if (hasZodFields) {
124124
// 将散落的 Zod 字段收集为 z.object
125-
const { z } = require("zod");
126125
const shape: Record<string, ZodType> = {};
127126

128127
for (const [key, value] of Object.entries(schema)) {
@@ -889,8 +888,8 @@ app.post(
889888
"/users",
890889
{
891890
validate: {
892-
body: createUserBody, // ← Zod schema
893-
query: paginationQuery, // ← Zod schema
891+
body: createUserBody.shape, // 字段级 Zod schema
892+
query: paginationQuery.shape, // 字段级 Zod schema
894893
},
895894
},
896895
handler,
@@ -910,7 +909,7 @@ app.get(
910909
);
911910
```
912911

913-
插件内部会自动检测 schema 类型Zod 实例走 Zod 校验路径,普通对象走 schema-dsl 路径
912+
插件内部会自动检测 schema 类型:字段值为 Zod 实例时走 Zod 校验路径,普通 schema-dsl 对象走默认校验路径
914913

915914
## 关键对比
916915

website/docs/guide/plugins.md

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -214,19 +214,47 @@ export default definePlugin({
214214
```typescript
215215
import { definePlugin } from "vextjs";
216216
import type { VextValidator } from "vextjs";
217+
import { z } from "zod";
217218

218219
export default definePlugin({
219220
name: "zod-validator",
220221

221222
setup(app) {
223+
const originalValidator = app.getValidator();
224+
222225
const zodValidator: VextValidator = {
223-
validate(schema, data) {
224-
// Zod 校验逻辑...
225-
return { valid: true, data };
226-
},
227-
toJSONSchema(schema) {
228-
// 转换为 JSON Schema(供 OpenAPI 使用)
229-
return {};
226+
compile(schema) {
227+
const toVextResult = (result: ReturnType<z.ZodType["safeParse"]>) =>
228+
result.success
229+
? { valid: true, data: result.data }
230+
: {
231+
valid: false,
232+
errors: result.error.issues.map((issue) => ({
233+
field: issue.path.join("."),
234+
message: issue.message,
235+
})),
236+
};
237+
238+
// 如果整个 location schema 是 Zod schema,直接执行 safeParse
239+
if (schema instanceof z.ZodType) {
240+
return (data) => toVextResult(schema.safeParse(data));
241+
}
242+
243+
// 如果 schema 对象的字段是 Zod schema,将其组合为 z.object
244+
const zodShape: Record<string, z.ZodType> = {};
245+
for (const [key, value] of Object.entries(schema)) {
246+
if (value instanceof z.ZodType) {
247+
zodShape[key] = value;
248+
}
249+
}
250+
251+
if (Object.keys(zodShape).length > 0) {
252+
const zodSchema = z.object(zodShape);
253+
return (data) => toVextResult(zodSchema.safeParse(data));
254+
}
255+
256+
// 非 Zod schema 回退到默认 schema-dsl validator
257+
return originalValidator.compile(schema);
230258
},
231259
};
232260

website/docs/guide/services.md

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ export default class OrderService {
233233
}
234234
```
235235

236-
:::warning 避免循环依赖
236+
::::warning 避免循环依赖
237237
`service-loader` 内置循环依赖检测。如果 `ServiceA``ServiceB` 相互依赖,框架会在启动时报错。
238238

239239
**✅ 正确做法** — 在方法中延迟访问:
@@ -262,7 +262,7 @@ export default class OrderService {
262262
}
263263
```
264264

265-
:::
265+
::::
266266

267267
## 使用插件提供的能力
268268

@@ -303,7 +303,7 @@ export default class UserService {
303303
}
304304
```
305305

306-
:::tip 类型提示
306+
::::tip 类型提示
307307
使用 `declare module` 扩展 `VextApp` 接口可获得完整的类型提示:
308308

309309
```typescript
@@ -319,7 +319,7 @@ declare module "vextjs" {
319319
```
320320

321321
扩展后 `this.app.cache` 即可获得 IDE 自动补全。
322-
:::
322+
::::
323323

324324
## 使用 `app.throw()` 抛出错误
325325

@@ -357,6 +357,52 @@ export default class UserService {
357357
}
358358
```
359359

360+
## 在服务中校验非 HTTP 输入
361+
362+
路由入口参数优先通过 `RouteOptions.validate` 声明,并在 handler 中使用 `req.valid()` 读取校验后的数据。对于 service 直接处理的非 HTTP 输入,例如定时任务、消息队列、外部回调或其他 service 调用,可以通过 `this.app.getValidator()` 复用当前全局校验引擎。
363+
364+
`getValidator()` 默认返回基于 schema-dsl 的 validator;如果插件通过 `app.setValidator()` 替换为 Zod、Yup 等实现,service 中获取到的也是替换后的 validator。
365+
366+
```typescript
367+
import { VextValidationError, type VextApp, type VextValidator } from "vextjs";
368+
369+
const createUserSchema = {
370+
name: "string:1-50!",
371+
email: "email!",
372+
};
373+
374+
export default class UserService {
375+
private validateCreateUser: ReturnType<VextValidator["compile"]>;
376+
377+
constructor(private app: VextApp) {
378+
const validator = app.getValidator();
379+
this.validateCreateUser = validator.compile(createUserSchema);
380+
}
381+
382+
async createFromJob(input: unknown) {
383+
const result = this.validateCreateUser(input);
384+
385+
if (!result.valid) {
386+
throw new VextValidationError(result.errors ?? []);
387+
}
388+
389+
const data = result.data as { name: string; email: string };
390+
return this.create(data);
391+
}
392+
393+
async create(data: { name: string; email: string }) {
394+
// 创建逻辑...
395+
return { id: crypto.randomUUID(), ...data };
396+
}
397+
}
398+
```
399+
400+
::::tip
401+
402+
不要在 service 中直接 `import "schema-dsl"`。直接引用 schema-dsl 会绕过 `app.setValidator()` 的全局替换能力,导致 service 校验与路由校验使用不同引擎。
403+
404+
::::
405+
360406
## 使用 `app.logger` 记录日志
361407

362408
服务层推荐通过 `this.app.logger` 记录结构化日志。日志自动携带 requestId(通过 AsyncLocalStorage 上下文传播):

0 commit comments

Comments
 (0)