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にしたい気持ちもありますが、バリデーションエラーを型付きで取得できるようになりました。やったね!