本文预期读者阅读过本专栏之前的两篇文章
《【第十期】基于 Apollo、Koa 搭建 GraphQL 服务端》
和
《【第十一期】实现 Javascript 版本的 Laravel 风格参数验证器》
或对
GraphQL
与Laravel
的验证器有所了解。
前面两篇文章分别讲解了:
- 如何搭建一个
GraphQL
服务器 - 如何实现一个
Laravel
风格的验证器
今天我们来尝试将二者结合,在 GraphQL
工程中实现一个 Laravel
风格的高级验证器。
需求
一个 GraphQL
请求,会经历三个阶段:
- 解析阶段(
Parse phase
) - 验证阶段(
Validation phase
) - 执行阶段(
Execution phase
)
其中,在验证阶段(Validation phase
),会根据 GraphQL SDL
的类型系统,对参数进行基本校验:
- 客户端传递未定义的查询字段,会在验证阶段失败
- 客户端传递与预期类型不匹配的参数,会在验证阶段失败
- 客户端没有传递必传参数,会在验证阶段失败
但是,对于一些稍复杂场景,类型系统的功能无法覆盖到:
- 对某些数字类型的字段,限制上限和下限。例如:年龄,限制在 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
提供了一种方式,有兴趣的读者可以去参考:通过自定义指令动态生成自定义标量
考虑到动态自定义标量对于研发人员并不友好(自定义标量定义在自定义指令的代码中,这增加了阅读和理解工程的成本)
我们选择使用:通过自定义指令调整解析器的方式来实现高级校验。
实现步骤
- 创建自定义指令
@validation
,此指令作用于字段定义上,并支持一个参数rules
,值的类型为字符串。 - 在
GraphQL
服务启动时,在自定义指令@validation
内部,针对定义了rules
的字段,会调整其解析器,在其原有解析器外围包裹一层验证器逻辑。在解析器执行期间,验证逻辑会执行并对字段值进行校验。 - 对于具体某个
rule
的解析和校验工作,由validator-simple
库提供支持(validator-simple
库是我们在之前的文章《【第十一期】实现 Javascript 版本的 Laravel 风格参数验证器》中实现的)
设计语法
-
单个字段的多个
rules
之间,使用|
分割 -
字段名称与
rules
之间,使用=>
分割 -
多个字段校验描述,使用英文分号
;
来分割。例如:
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}
)
}
`
- 关于所有可用
rules
的列表,请查看 validator-simple
准备工作
开始前,准备好:
- 一个基于
NodeJS
实现的GraphQl
工程。本文我们使用文章《【第十期】基于 Apollo、Koa 搭建 GraphQL 服务端》中创建的graphql-server-demo
工程 - 一个基于
JavaScript
实现的Laravel
风格验证器。本文我们使用文章《【第十一期】实现 Javascript 版本的 Laravel 风格参数验证器》中创建的npm
库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
,并增加一个创建 book
的 mutation
,并对 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