背景とやりたいこと
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の形式を検証したいとします。
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を作成します。
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することで有効にします。
GraphQL::Schema::Validator.install(:format, CustomValidators::FormatValidator)
バリデーションエラーが一つでもあった場合はcontext#add_errorによってcontext.errorsに格納されるので以下のようにraiseすれば良さそうです!
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
に直接正規表現を入れてあげるのが良さそうです。
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します。
GraphQL::Schema::Validator.install(:email_format, CustomValidators::EmailFormatValidator)
validatesには以下のように指定します。
module Mutations
class CreateUser < ::Mutations::BaseMutation
..
argument :email, String, required: true, validates: { email_format: {} }
..
def resolve(input)
validate_input!
...
end
...
end
end
単体テスト
上記のEmailFormatValidatorのテストの書き方も調べました。
rspecで書いています。
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
まとめ
graphql-rubyが公式で提供している割には、調べてもあまり出てこなかったので、
メンテナンスできそうなら導入するのが良さそうです。
個人的には以下のメリットがあるかなと思いました。
- 入力値バリデーションをロジックに関するバリデータから切り離せる
- add_errorして最後にまとめてraiseすることで、複数のエラーを一度に検知できる