本文預期讀者閱讀過本專欄以前的兩篇文章javascript
《【第十期】基於 Apollo、Koa 搭建 GraphQL 服務端》前端
和java
《【第十一期】實現 Javascript 版本的 Laravel 風格參數驗證器》node
或對
GraphQL
與Laravel
的驗證器有所瞭解。git
前面兩篇文章分別講解了:github
GraphQL
服務器Laravel
風格的驗證器今天咱們來嘗試將兩者結合,在 GraphQL
工程中實現一個 Laravel
風格的高級驗證器。npm
一個 GraphQL
請求,會經歷三個階段:json
Parse phase
)Validation phase
)Execution phase
)其中,在驗證階段(Validation phase
),會根據 GraphQL SDL
的類型系統,對參數進行基本校驗:bash
可是,對於一些稍複雜場景,類型系統的功能沒法覆蓋到:服務器
所以,咱們針對更加複雜一些的校驗規則,須要一個更高級的驗證器。
肯定了需求,咱們來看如何實現這個高級驗證器。
咱們知道自定義標量(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