3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

graphql-rubyで入力値バリデーションをカスタマイズする

Last updated at Posted at 2022-04-11

背景とやりたいこと

graphql-rubyにはargumentに対してバリデーションをかけることができるのですが、

愚直にやると以下のようなpathとlocations付きの形式でエラーが返ってきます。

参考: https://graphql-ruby.org/errors/execution_errors#adding-errors-to-the-array

{
  "errors": [
    {
      "message": "Can't continue with this query",
      "locations": [
        {
          "line": 2,
          "column": 10,
        }
      ],
      "path": ["user", "login"],
    }
  ]
}

今回、こちらの形式ではなくextensionsを用いた形式で返したいと思い、どうやればできそうなのか調べてみました。

環境

  • ruby: 2.6.6
  • rails: 5.2.6
  • graphql-ruby: 1.12.7
  • rspec: 3.10.0
  • rspec-rails: 5.0.2

format validationを上書きしてみる

例えば以下のようなemailの形式を検証したいとします。

app/graphql/mutations/create_user.rb
module Mutations
  class CreateUser < ::Mutations::BaseMutation
	..
	argument :email, String, required: true, validates: { format: { with: /\A([a-zA-Z0-9]+)([a-zA-Z0-9\._\-\+]*)@([a-zA-Z0-9]+)([a-zA-Z0-9\._\-]+)\z/ } }
	..

    def resolve(input)
	  ...
	end
  end
end

ここでGraphQL::Schema::Validator::FormatValidatorを継承するCustomValidatorを作成します。

app/graphql/custom_validators/format_validator.rb
module CustomValidators
  class FormatValidator < GraphQL::Schema::Validator::FormatValidator
    def validate(_object, context, _value)
      return unless super

      context.add_error(GraphQL::ExecutionError.new("#{validated.name}の形式が正しくありません。", extensions: { code: "invalid_argument_#{validated.name}" }))
    end
  end
end

superでFormatValidatorのvalidateメソッドを実行し、エラーの場合はmessageが、エラーでない場合はnilが返ってきます。

エラーの場合はcontext.add_errorでカスタムエラーを追加することでextensionsを実現できます。

つづいてカスタムバリデータを初期化時にinstallすることで有効にします。

config/initializers/graphql.rb
GraphQL::Schema::Validator.install(:format, CustomValidators::FormatValidator)

バリデーションエラーが一つでもあった場合はcontext#add_errorによってcontext.errorsに格納されるので以下のようにraiseすれば良さそうです!

app/graphql/mutations/create_user.rb
module Mutations
  class CreateUser < ::Mutations::BaseMutation
	..
	argument :email, String, required: true, validates: { format: { with: /\A([a-zA-Z0-9]+)([a-zA-Z0-9\._\-\+]*)@([a-zA-Z0-9]+)([a-zA-Z0-9\._\-]+)\z/ } }
	..

	def resolve(input)
		validate_input!
		...
	end

	def validate_input!
	  return if context.errors.empty?
		
	  # To avoid duplicated error
	  last_error = context.errors.pop
	  raise last_error
    end
  end
end

試すと以下のように返ってきました。

{
  "data": {
    "createUser": null
  },
  "errors": [
    {
      "message": "emailの形式が正しくありません。",
      "extensions": {
        "code": "invalid_argument_email"
      }
    }
  ]
}

CustomValidatorとして切り出す

上記のように汎用的なものでもいいのですが、メールアドレスや電話番号など使いまわしたいformatはCustomValidatorとして切り出した方が使い勝手がいいかもしれません。

やり方はあまり変わりません。

同じようにカスタムバリデータを作成します。

@with_pattern に直接正規表現を入れてあげるのが良さそうです。

app/graphql/custom_validators/email_format_validator.rb
module CustomValidators
  class EmailFormatValidator < GraphQL::Schema::Validator::FormatValidator
    def validate(_object, context, _value)
      @with_pattern = /\A([a-zA-Z0-9]+)([a-zA-Z0-9\._\-\+]*)@([a-zA-Z0-9]+)([a-zA-Z0-9\._\-]+)\z/
      return unless super

      context.add_error(GraphQL::ExecutionError.new('メールアドレスの形式が正しくありません。', extensions: { code: 'invalid_argument_email_format' }))
    end
  end
end

email_formatとしてinstallします。

config/initializers/graphql.rb
GraphQL::Schema::Validator.install(:email_format, CustomValidators::EmailFormatValidator)

validatesには以下のように指定します。

app/graphql/mutations/create_user.rb
module Mutations
  class CreateUser < ::Mutations::BaseMutation
	..
    argument :email, String, required: true, validates: { email_format: {} }
	..

    def resolve(input)
	  validate_input!
	  ...
    end
		
    ...
  end
end

単体テスト

上記のEmailFormatValidatorのテストの書き方も調べました。

rspecで書いています。

spec/graphql/custom_validators/email_format_validator_spec.rb
RSpec.describe CustomValidators::EmailFormatValidator do
  describe '#validate' do
    subject do
      test_schema.execute(
        <<~GQL
          query {
            validated(value: "#{email}")
          }
        GQL
      )
    end

    let(:query_type) do
      Class.new(GraphQL::Schema::Object) do
        graphql_name 'Query'
        field :validated, String, null: true do
          argument :value, String, required: false, validates: { email_format: {} }
        end

        def validated(value: nil)
          value
        end
      end
    end
    let(:test_schema) do
      schema = Class.new(GraphQL::Schema)
      schema.query(query_type)
      schema
    end

    context 'when valid format' do
      let(:email) { 'test@example.com' }

      it 'does not return error' do
        expect(subject['errors']).to eq(nil)
      end
    end

    context 'when invalid format' do
      let(:email) { 'example.com' }

      it 'returns error' do
        errors = subject['errors']
        expect(errors.count).to eq(1)
        expect(errors.first['extensions']['code']).to eq('invalid_argument_email_format')
      end
    end
  end
end

このテストは実際にgraphql-rubyの単体テストを参考に書いてみました。

https://github.com/rmosolgo/graphql-ruby/pull/3671/files

https://github.com/rmosolgo/graphql-ruby/blob/master/spec/graphql/schema/validator/validator_helpers.rb

まとめ

graphql-rubyが公式で提供している割には、調べてもあまり出てこなかったので、

メンテナンスできそうなら導入するのが良さそうです。

個人的には以下のメリットがあるかなと思いました。

  • 入力値バリデーションをロジックに関するバリデータから切り離せる
  • add_errorして最後にまとめてraiseすることで、複数のエラーを一度に検知できる

参考リンク

公式ドキュメント
公式issue

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?