graphqlでエラーを返す方法はいくつかあり、それぞれメリデメありますが、Unionで返すのが好みです。
例えば、以下のようにmutationで実行時のバリデーションエラーを共通のフォーマットで返すことを考えてみました。
mutation createPost($input: CreatePostInput!) {
createPost(input: $input) {
result {
... on Post {
id
name
}
... on InvalidObject {
errors {
fullMessage
}
}
}
}
}
このクエリを見るとわかるようにCreate, Updateを行うmutationのエラーを共通のInvalidObjectで受け取ることができます。
この方法を取ると、想定済のエラーのパターンと、エラーオブジェクトを型付で知ることができるため、実装時の時点でそれぞれのエラーパターンへの対応コードを書きやすくなると思います。
ということで、union型を使ったエラーの内、バリデーションエラーの型付をGraphQL Rubyでやってみました。
mutationファイル
module Mutations
class CreatePost < BaseMutation
field :result, Mutations::CreatePostResult, null: false
argument :title, String, required: true
argument :body, String, required: true
def resolve(title:, body:)
post = Post.create(title:, body:)
{ result: post }
end
end
end
mutationはこのようになります。 resultというフィールドに Mutations::CreatePostResult
を指定しているのがポイントです。
mutation resultファイル
class Mutations::CreatePostResult < Types::BaseUnion
possible_types Types::Objects::PostType, Types::Objects::InvalidObjectType
def self.resolve_type(object, _context)
if object.invalid?
Types::Objects::InvalidObjectType
else
Types::Objects::PortfolioType
end
end
end
resultファイルはmutations配下においています。mutationではないのでちょっと気持ち悪いですが、命名ルールを揃えることで、ファイルがmutationと隣になるので、扱いやすくなります。
バリデーション失敗したモデルのためのオブジェクト
module Types::Objects
# バリデーション失敗したモデル用のオブジェクト
class InvalidObjectType < Types::BaseObject
field :id, ID, null: true
field :model, String, null: false
def model
object.class.name
end
field :errors, [Types::Objects::ValidationErrorType], null: false
field :full_messages, [String], null: false
def full_messages
object.errors.full_messages
end
end
end
InvalidObjectTypeはinvalid?なモデルの表示を行うオブジェクトです。
バリデーションエラーのためのオブジェクト
module Types::Objects
# バリデーションエラー用のオブジェクト
class ValidationErrorType < Types::BaseObject
field :attribute, String, null: false
field :type, String, null: false
field :message, String, null: false
field :full_message, String, null: false
end
end
attribute名をEnumにしたい気持ちもありますが、バリデーションエラーを型付きで取得できるようになりました。やったね!