Security News
Node.js EOL Versions CVE Dubbed the "Worst CVE of the Year" by Security Experts
Critics call the Node.js EOL CVE a misuse of the system, sparking debate over CVE standards and the growing noise in vulnerability databases.
egg-typebox-validate
Advanced tools
基于 typebox 和 ajv 封装的 egg validate 插件。
一直以来,在 typescript 的 egg 项目里,对参数校验 ctx.validate 是比较难受的,比如:
class HomeController extends Controller {
async index() {
const { ctx } = this;
// 写一遍 js 的类型校验
ctx.validate({
id: 'string',
name: {
type: 'string',
required: false,
},
timestamp: {
type: 'number',
required: false,
},
}, ctx.params);
// 写一遍 ts 的类型定义,为了后面拿参数定义
const params: {
id: string;
name?: string;
timestamp: number;
} = ctx.params;
...
ctx.body = params.id;
}
}
export default HomeController;
可以看到这里我们写了两遍的类型定义,一遍 js 的定义(用 parameter 库的规则),另一遍用 ts 的方式来强转我们的参数类型,方便我们后面写代码的时候能得到 ts 的类型效果。 对于简单的类型写起来还好,但是对于复杂点的参数定义,开发体验就不是那么好了。
这就是这个库想要解决的问题,对于参数校验,写一遍类型就够了:
+ import { Static, Type } from 'egg-typebox-validate/typebox';
class HomeController extends Controller {
async index() {
const { ctx } = this;
// 写 js 类型定义
- ctx.validate({
- id: 'string',
- name: {
- type: 'string',
- required: false,
- },
- timestamp: {
- type: 'number',
- required: false,
- },
- }, ctx.params);
+ const paramsSchema = Type.Object({
+ id: Type.String(),
+ name: Type.Optional(Type.String()),
+ timestamp: Type.Optional(Type.Integer()),
+ });
// 直接校验
+ ctx.tValidate(paramsSchema, ctx.params);
// 不用写 js 类型定义
+ const params: Static<typeof paramsSchema> = ctx.params;
- const params: {
- id: string;
- name?: string;
- timestamp: number;
- } = ctx.params;
...
ctx.body = params.id;
}
}
export default HomeController;
用 Static<typeof typebox>
推导出的 ts 类型:
npm i egg-typebox-validate -S
// config/plugin.ts
const plugin: EggPlugin = {
typeboxValidate: {
enable: true,
package: 'egg-typebox-validate',
},
};
+ import { Static, Type } from 'egg-typebox-validate/typebox';
// 写在 controller 外面,静态化,性能更好,下面有 benchmark
+ const paramsSchema = Type.Object({
+ id: Type.String(),
+ name: Type.String(),
+ timestamp: Type.Integer(),
+ });
// 可以直接 export 出去,给下游 service 使用
+ export type ParamsType = Static<typeof paramsSchema>;
class HomeController extends Controller {
async index() {
const { ctx } = this;
// 直接校验
+ ctx.tValidate(paramsSchema, ctx.params);
// 不用写 js 类型定义
+ const params: ParamsType = ctx.params;
...
}
}
export default HomeController;
Show me the code!
export const TYPEBOX_NAME_DESC_OBJECT = Type.Object({
name: Type.String(),
description: Type.Optional(Type.String()),
});
// type NameAndDesc = { name: string; description?: string }
type NameAndDesc = Static<typeof TYPEBOX_NAME_DESC_OBJECT>;
// controller User
async create() {
const { ctx } = this;
const USER_TYPEBOX = Type.Intersect([
TYPEBOX_NAME_DESC_OBJECT,
Type.Object({ avatar: Type.String() }),
])
ctx.tValidate(USER_TYPEBOX, ctx.request.body);
// 在编辑器都能正确得到提示
// type User = { name: string; description?: string } & { avatar: string }
const { name, description, avatar } = ctx.request.body as Static<typeof USER_TYPEBOX>;
...
}
// controller Photo
async create() {
const { ctx } = this;
const PHOTO_TYPEBOX = Type.Intersect([
TYPEBOX_NAME_DESC_OBJECT,
Type.Object({ location: Type.String() }),
])
ctx.tValidate(PHOTO_TYPEBOX, ctx.request.body);
// 在编辑器都能正确得到提示
// type Photo = { name: string; description?: string } & { location: string }
const { name, description, location } = ctx.request.body as Static<typeof PHOTO_TYPEBOX>;
...
}
'date-time',
'time',
'date',
'email',
'hostname',
'ipv4',
'ipv6',
'uri',
'uri-reference',
'uuid',
'uri-template',
'json-pointer',
'relative-json-pointer',
'regex'
Type.Number()
),有类型提示,语法也比较简单,有提示不容易写错;写 parameter 规范的时候,写字符串('nunber'
)有时候会不小心写错 😂,再加上它对于复杂嵌套对象的写法还是比较困难的,我每次都会查文档,官方的文档也不全。但是 typebox,就很容易举一反三了。egg-typebox-validate 底层使用的是 ajv, 官网上宣称是 The fastest JSON validator for Node.js and browser.
结论是在静态化的场景下,ajv 的性能要比 parameter 好得多,快不是一个数量级,详见benchmark
suite
.add('#ajv', function() {
const rule = Type.Object({
name: Type.String(),
description: Type.Optional(Type.String()),
location: Type.Enum({shanghai: 'shanghai', hangzhou: 'hangzhou'}),
})
ajv.validate(rule, DATA);
})
.add('#ajv define once', function() {
ajv.validate(typeboxRule, DATA);
})
.add('#parameter', function() {
const rule = {
name: 'string',
description: {
type: 'string',
required: false,
},
location: ['shanghai', 'hangzhou'],
}
p.validate(rule, DATA);
})
.add('#parameter define once', function() {
p.validate(parameterRule, DATA);
})
在 MacBook Pro(2.2 GHz 六核Intel Core i7)上,跑出来结果是:
#ajv x 941 ops/sec ±3.97% (73 runs sampled)
#ajv define once x 17,188,370 ops/sec ±11.53% (73 runs sampled)
#parameter x 2,544,118 ops/sec ±4.68% (79 runs sampled)
#parameter define once x 2,541,590 ops/sec ±5.34% (77 runs sampled)
Fastest is #ajv define once
ctx.validate
替换成 ctx.tValidate
切换到 egg-typebox-validate 校验后:
ctx.tValidate
参数校验失败后,抛出错误,内部实现(错误码、错误标题等)逻辑和 ctx.validate
的保持一致+ import { Static, Type } from 'egg-typebox-validate/typebox';
ctx.tValidate(Type.Object({
name: Type.String(),
}), ctx.request.body);
ctx.tValidateWithoutThrow
直接校验,不抛出错误+ import { Static, Type } from 'egg-typebox-validate/typebox';
const valid = ctx.tValidateWithoutThrow(Type.Object({
name: Type.String(),
}), ctx.request.body);
if (valid) {
...
} else {
const errors = this.app.ajv.errors
// handle errors
...
}
@Validate([ [rule1, ctx => ctx.xx1], [rule2, ctx => ctx.xx2] ])
调用(写法更干净,推荐使用!️)+ import { Validate, ValidateFactory } from 'egg-typebox-validate/decorator';
const ValidateWithRedirect = ValidateFactory(ctx => ctx.redirect('/422'));
class HomeController extends Controller {
+ @Validate([
+ [paramsSchema, ctx => ctx.params],
+ [bodySchema, ctx => ctx.request.body, (ctx, errors) => 'MyErrorPrefix: ' + errors.map(e => e.message).join(', ')],
+ ])
async index() {
const { ctx } = this;
// 直接校验
- ctx.tValidate(paramsSchema, ctx.params);
- ctx.tValidate(bodySchema, ctx.request.body);
// 不用写 js 类型定义
const params: ParamsType = ctx.params;
...
}
+ @ValidateWithRedirect([paramsSchema, ctx => ctx.params])
async post() {
// ...
}
}
export default HomeController;
目前装饰器只支持有 this.ctx
的 class 上使用,比如 controller,service 等。也可以通过内置的 ValidateFactory
自定义校验失败后的回调逻辑,更多使用案例可以看这个项目里写的测试用例。
参考 https://github.com/sinclairzx81/typebox#types
比如:
const body = { name: ' david '}
ctx.tValidate(Type.Object({
name: Type.String({ minLength: 1, maxLength: 5, transform: ['trim'] })
}), body)
{ name: 'david' }
更多 ajv 对 string 的 transform 操作,详见 https://ajv.js.org/packages/ajv-keywords.html#transform
比如想校验上传的 string 是否是合法的 json string,我们可以对 Type.String 的 format 做 patch,针对 string 加一个 'json-string' 的 format
config.typeboxValidate = {
patchAjv: (ajv) => {
ajv.addFormat('json-string', {
type: 'string',
validate: (x) => {
try {
JSON.parse(x);
return true;
} catch (err) {
return false;
}
}
});
}
}
async someFunc() {
const typebox = Type.Object({
jsonString: Type.Optional(Type.String({ format: 'json-string' })),
});
const res = ctx.tValidate(typebox, { a: '{"a":1}' }) // valid
const res = ctx.tValidate(typebox, { a: 'wrong{"a":1}' }) // invalid
}
当然也可以定义其他各种规则,比如我们常见的 semver 规范,那可以在我们的配置里继续 patch ajv string format
+ import { valid } from 'semver';
config.typeboxValidate = {
patchAjv: (ajv) => {
ajv.addFormat('json-string', {
type: 'string',
validate: (x) => {
try {
JSON.parse(x);
return true;
} catch (err) {
return false;
}
}
});
+ ajv.addFormat("semver", {
+ type: "string",
+ validate: (x) => valid(x) != null,
+ })
}
}
使用例子:
async someFunc() {
const typebox = Type.Object({
version: Type.String({ format: 'semver' }),
});
const res = ctx.tValidate(typebox, { a: '1.0.0' }) // valid
const res = ctx.tValidate(typebox, { a: 'a.b.c' }) // invalid
}
上面例子是 string 的例子,当然也可以对其他类型做其他 patch,比如 number,array 等,限制你的只有想象力。
全部 json-schema 支持的类型:https://json-schema.org/understanding-json-schema/reference/type.html
FAQs
another validate for typescript egg projects
The npm package egg-typebox-validate receives a total of 8 weekly downloads. As such, egg-typebox-validate popularity was classified as not popular.
We found that egg-typebox-validate demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Critics call the Node.js EOL CVE a misuse of the system, sparking debate over CVE standards and the growing noise in vulnerability databases.
Security News
cURL and Go security teams are publicly rejecting CVSS as flawed for assessing vulnerabilities and are calling for more accurate, context-aware approaches.
Security News
Bun 1.2 enhances its JavaScript runtime with 90% Node.js compatibility, built-in S3 and Postgres support, HTML Imports, and faster, cloud-first performance.