【第十三期】基於 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

相關文章
相關標籤/搜索