【第十三期】基于 GraphQL 自定义指令实现高级验证器

本文预期读者阅读过本专栏以前的两篇文章javascript

《【第十期】基于 Apollo、Koa 搭建 GraphQL 服务端》前端

java

《【第十一期】实现 Javascript 版本的 Laravel 风格参数验证器》node

或对 GraphQLLaravel 的验证器有所了解。git

前面两篇文章分别讲解了:github

  • 如何搭建一个 GraphQL 服务器
  • 如何实现一个 Laravel 风格的验证器

今天咱们来尝试将两者结合,在 GraphQL工程中实现一个 Laravel 风格的高级验证器。npm

需求

一个 GraphQL 请求,会经历三个阶段:json

  • 解析阶段(Parse phase
  • 验证阶段(Validation phase
  • 执行阶段(Execution phase

其中,在验证阶段(Validation phase),会根据 GraphQL SDL 的类型系统,对参数进行基本校验:bash

  • 客户端传递未定义的查询字段,会在验证阶段失败
  • 客户端传递与预期类型不匹配的参数,会在验证阶段失败
  • 客户端没有传递必传参数,会在验证阶段失败

可是,对于一些稍复杂场景,类型系统的功能没法覆盖到:服务器

  • 对某些数字类型的字段,限制上限和下限。例如:年龄,限制在 0 到 150 之间
  • 对某些日期类型的字段,限制一个时间区间;例如:出生日期,限制在 1900 年到 2020 年之间
  • ......

所以,咱们针对更加复杂一些的校验规则,须要一个更高级的验证器。

设计

肯定了需求,咱们来看如何实现这个高级验证器。

预想中的方案

咱们知道自定义标量(custom scalar)能够限制一个字段值的类型,所以在标量上作高级验证器是个不错的开始。

例如:对于年龄字段,咱们新设计一个名为 age 的标量,限制它的取值范围为 0 到 150 之间。对于出生日期字段同理:birthDay

可是,这么作有一个问题:咱们的字段类型各类各样,没个尽头,若是为每个类型的字段都设计一个标量,那么咱们将被迫维护数量庞大的标量库。

若是标量能支持参数,咱们只须要将各类高级验证规则抽象为一组 rules 库就行了,这样在不一样字段类型之间,能够复用一些 rules,避免了标量库随着字段类型的增长而增加的问题。例如: age(max:150,min:0)birthDay(Date,lt:2020-01-01,gt:1900-01-01)

惋惜的是,目前为止,GraphQL 的实现对于标量并不支持设置参数,所以,咱们只能寻求其余的方式。

实际方案

除了自定义标量外,还有自定义指令(custom directive)。

Apollo GraphQL 提供了一种方式,有兴趣的读者能够去参考:经过自定义指令动态生成自定义标量

考虑到动态自定义标量对于研发人员并不友好(自定义标量定义在自定义指令的代码中,这增长了阅读和理解工程的成本)

咱们选择使用:经过自定义指令调整解析器的方式来实现高级校验。

实现步骤

  1. 建立自定义指令 @validation,此指令做用于字段定义上,并支持一个参数 rules,值的类型为字符串。
  2. GraphQL 服务启动时,在自定义指令 @validation 内部,针对定义了 rules 的字段,会调整其解析器,在其原有解析器外围包裹一层验证器逻辑。在解析器执行期间,验证逻辑会执行并对字段值进行校验。
  3. 对于具体某个 rule 的解析和校验工做,由 validator-simple 库提供支持(validator-simple 库是咱们在以前的文章《【第十一期】实现 Javascript 版本的 Laravel 风格参数验证器》中实现的)

设计语法

  1. 单个字段的多个 rules 之间,使用 | 分割

  2. 字段名称与 rules 之间,使用 => 分割

  3. 多个字段校验描述,使用英文分号 ; 来分割。例如:

gql`
  extend type Mutation {
    createBook(
      book: inputBook
    ): Book  @validation(
      rules: "book.name => max:5|min:3;book.price => max:999|min:10"
    )
  }
`
复制代码

虽然 GraphQL 标准中不容许字符串换行,但为了可读性,咱们能够在外部定义可读性更好的描述:

const createBookValidationRules = `"` +
  `book.name => max:5|min:3;` +
  `book.price => max:999|min:10` +
  `"`
 
gql`
  extend type Mutation {
    createBook(
      book: inputBook
    ): Book  @validation(
      rules: ${createBookValidationRules}
    )
  }
`
复制代码
  1. 关于全部可用 rules 的列表,请查看 validator-simple

准备工做

开始前,准备好:

实现

开始以前,graphql-server-demo 工程的目录结构以下:

.
├── index.js
├── package.json
├── src
│   ├── components
│   │   ├── book
│   │   │   ├── resolver.js
│   │   │   └── schema.js
│   │   └── cat
│   │       ├── resolver.js
│   │       └── schema.js
│   ├── graphql
│   │   ├── directives
│   │   │   ├── auth.js
│   │   │   └── index.js
│   │   ├── index.js
│   │   └── scalars
│   │       ├── date.js
│   │       └── index.js
│   └── middlewares
│       └── auth.js
└── yarn.lock
复制代码

安装 validator-simple:

yarn add validator-simple@1.0.1
复制代码

注意:在文章《【第十一期】实现 Javascript 版本的 Laravel 风格参数验证器》中建立的 v1.0.0 版本的 validator-simple 并不支持在 rules 中使用 . 符号指定深层字段名。在 v1.0.1 版本支持此功能。

适配 validator-simple

由于咱们设计好了在 GraphQL schema 中表达验证规则的语法,它和 validator-simple 的语法有些许差别。

所以,咱们建立一个文件 src/libs/validation.js 来作适配的工做。

代码以下:

const V = require('validator-simple')

const findFirstInvalidParam = (params, rules) => {
  const serializationRules = {}

  rules.split(';').forEach(item => {
    const [itemName, itemRules] = item.split('=>')
    serializationRules[itemName.trim()] = itemRules.trim()
  })

  const invalidMsg = V(params, serializationRules)

  if (invalidMsg && invalidMsg.length) return invalidMsg[0]
}

module.exports = {
  findFirstInvalidParam
}
复制代码

实现自定义指令 @validation

接下来,在文件夹 src/graphql/directives 中新建文件 validation.js

内容以下:

const { SchemaDirectiveVisitor, UserInputError } = require('apollo-server-koa')
const { defaultFieldResolver } = require('graphql')
const { findFirstInvalidParam } = require('../../libs/validation.js')

class VallidationDirective extends SchemaDirectiveVisitor {
  visitFieldDefinition (field) {
    this.modifyResolver(field)
  }

  modifyResolver (field) {
    const { resolve = defaultFieldResolver } = field
    const { rules } = this.args

    if (!rules) return

    field.resolve = async function (...args) {
      const invalidInfo = findFirstInvalidParam(args[1], rules)

      if (invalidInfo) throw new UserInputError(invalidInfo.invalidMessage)

      return resolve.apply(this, args)
    }
  }
}

module.exports = {
  validation: VallidationDirective
}

复制代码

src/graphql/directives/index.js 中导出指令:

module.exports = {
  ...require('./validation.js'),
  ...require('./auth.js')
}
复制代码

而后在 src/graphql/index.js 中注册新的自定义指令:

...
  directive @auth on FIELD_DEFINITION

  # 注册验证器指令
  directive @validation(rules: String) on FIELD_DEFINITION

  type Query {
    _: Boolean
  }
...
复制代码

使用 @validation

打开文件 src/components/book/schema.js,并增长一个建立 bookmutation,并对 book 字段使用咱们刚刚注册好的验证器指令 @validation

代码以下:

...
  extend type Mutation {
    createBook ( book: inputBook ): Book! @validation(
      rules: "book.name => max:5|min:3;book.price => max:999|min:10"
    )
  }
...
复制代码

保存文件,启动服务,而后发出一个建立 book 的请求,并有意填写一个过长的名称,来验证一下咱们刚才设置的规则:

curl 'http://localhost:4000/graphql' \
  -H 'Content-Type: application/json' \
  --data-binary '{"query":"mutation createBook($newBook: inputBook) {\n createBook(book: $newBook) {\n name\n price\n created\n }\n}\n","variables":{"newBook":{"name":"this is new book name","price":100,"created":"2019-01-01"}}}' \
  --compressed
复制代码

上面的请求发出后,咱们会收到下面的响应内容:

{
  "errors":[
    {
      "code":"BAD_USER_INPUT",
      "message":"book.name 的长度或大小不能大于 5. 实际值为:this is new book name"
    }
  ],
  "data":null
}
复制代码

经过响应结果,咱们看到验证器已经生效了。

最终,graphql-server-demo 的目录结构以下:

.
├── index.js
├── package.json
├── src
│   ├── components
│   │   ├── book
│   │   │   ├── resolver.js
│   │   │   └── schema.js
│   │   └── cat
│   │       ├── resolver.js
│   │       └── schema.js
│   ├── graphql
│   │   ├── directives
│   │   │   ├── auth.js
│   │   │   ├── index.js
│   │   │   └── validation.js
│   │   ├── index.js
│   │   └── scalars
│   │       ├── date.js
│   │       └── index.js
│   ├── libs
│   │   └── validation.js
│   └── middlewares
│       └── auth.js
└── yarn.lock
复制代码

结束语

至此,咱们的高级验证器就开发完毕了。

从此只须要根据实际需求在 validator-simple 中增长新的验证规则,就能很容易得在 @validation 指令中使用它们。

validator-simple 只是一个为了方便表达文章内容而建立的库。 这里推荐一个更成熟的库node-input-validator


水滴前端团队招募伙伴,欢迎投递简历到邮箱:fed@shuidihuzhu.com

相关文章
相关标签/搜索