Socket
Socket
Sign inDemoInstall

egg-typebox-validate

Package Overview
Dependencies
9
Maintainers
1
Versions
9
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

    egg-typebox-validate

another validate for typescript egg projects


Version published
Weekly downloads
10
decreased by-85.29%
Maintainers
1
Created
Weekly downloads
 

Readme

Source

egg-typebox-validate

NPM version build status Test coverage Known Vulnerabilities npm download

基于 typeboxajv 封装的 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 类型:

tpian

怎么使用

  1. 安装
npm i egg-typebox-validate -S
  1. 在项目中配置
// config/plugin.ts
const plugin: EggPlugin = {
  typeboxValidate: {
    enable: true,
    package: 'egg-typebox-validate',
  },
};
  1. 在业务代码中使用
+ 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;

除了类型定义 write once 外,还有更多好处

  1. 类型组合方式特别香,能解决很多 DRY(Don't Repeat Yourself) 问题。比如有几张 db 表,都定义了 name 必填和 description 选填,那这个规则可以在各个实体类方法中被组合了。

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>;
  ...
}
  1. 校验规则使用的是业界标准的 json-schema 规范,内置很多开箱即用的类型
'date-time',
'time',
'date',
'email',
'hostname',
'ipv4',
'ipv6',
'uri',
'uri-reference',
'uuid',
'uri-template',
'json-pointer',
'relative-json-pointer',
'regex'
  1. 写定义的时候写的是 js 对象(Type.Number()),有类型提示,语法也比较简单,有提示不容易写错;写 parameter 规范的时候,写字符串('nunber')有时候会不小心写错 😂,再加上它对于复杂嵌套对象的写法还是比较困难的,我每次都会查文档,官方的文档也不全。但是 typebox,就很容易举一反三了。

与 egg-validate 性能比较

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

从 egg-validate 迁移到这个库的成本

  1. 把原来字符串式 js 对象写法迁移到 typebox 的对象写法。typebox 的写法还算简单和容易举一反三
  2. ctx.validate 替换成 ctx.tValidate
  3. 建议渐进式迁移,先迁简单的,对业务影响不大的

总结

切换到 egg-typebox-validate 校验后:

  1. 可以解决 ts 项目中参数校验代码写两遍类型的问题,提升代码重用率,可维护性等问题
  2. 用标准 json-schema 来做参数校验,是更加标准的业界做法,内置更多业界标准模型

API

  1. ctx.tValidate 参数校验失败后,抛出错误,内部实现(错误码、错误标题等)逻辑和 ctx.validate 的保持一致
+ import { Static, Type } from 'egg-typebox-validate/typebox';

ctx.tValidate(Type.Object({
  name: Type.String(),
}), ctx.request.body);
  1. 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
  ...
}
  1. ⭐⭐⭐ 装饰器 decorator @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 自定义校验失败后的回调逻辑,更多使用案例可以看这个项目里写的测试用例。

怎么写 typebox 定义

参考 https://github.com/sinclairzx81/typebox#types

支持 ajv 对 string 的 transform 校验

比如:

const body = { name: '  david   '}

ctx.tValidate(Type.Object({
  name: Type.String({ minLength: 1, maxLength: 5, transform: ['trim'] })
}), body)
  1. 是可以通过校验的
  2. 会对 body 有副作用,改写 name 字段,trim name 字段,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

  1. 在 config.default.ts 里 patch 默认 ajv 实例的规则
config.typeboxValidate = {
  patchAjv: (ajv) => {
    ajv.addFormat('json-string', {
      type: 'string',
      validate: (x) => {
        try {
          JSON.parse(x);
          return true;
        } catch (err) {
          return false;
        }
      }
    });
  }
}
  1. 使用
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

License

MIT

Keywords

FAQs

Last updated on 04 Jan 2022

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.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc