Mutation Classes

GraphQL mutations are special fields: instead of reading data or performing calculations, they may modify the application state. For example, mutation fields may:

These actions are called side effects.

Like all GraphQL fields, mutation fields:

GraphQL-Ruby includes two classes to help you write mutations:

Besides those, you can also use the plain field API to write mutation fields.

Example mutation class

If you used the install generator, a base mutation class will already have been generated for you. If that’s not the case, you should add a base class to your application, for example:

class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
  # Add your custom classes if you have them:
  # This is used for generating payload types
  object_class Types::BaseObject
  # This is used for return fields on the mutation's payload
  field_class Types::BaseField
  # This is used for generating the `input: { ... }` object type
  input_object_class Types::BaseInputObject
end

Then extend it for your mutations:

class Mutations::CreateComment < Mutations::BaseMutation
  null true
  argument :body, String
  argument :post_id, ID

  field :comment, Types::Comment
  field :errors, [String], null: false

  def resolve(body:, post_id:)
    post = Post.find(post_id)
    comment = post.comments.build(body: body, author: context[:current_user])
    if comment.save
      # Successful creation, return the created object with no errors
      {
        comment: comment,
        errors: [],
      }
    else
      # Failed save, return the errors to the client
      {
        comment: nil,
        errors: comment.errors.full_messages
      }
    end
  end
end

The #resolve method should return a hash whose symbols match the field names.

(See Mutation Errors for more information about returning errors.)

Also, you can configure null(false) in your mutation class to make the generated payload class non-null.

Hooking up mutations

Mutations must be attached to the mutation root using the mutation: keyword, for example:

class Types::Mutation < Types::BaseObject
  field :create_comment, mutation: Mutations::CreateComment
end

Auto-loading arguments

In most cases, a GraphQL mutation will act against a given global relay ID. Loading objects from these global relay IDs can require a lot of boilerplate code in the mutation’s resolver.

An alternative approach is to use the loads: argument when defining the argument:

class Mutations::AddStar < Mutations::BaseMutation
  argument :post_id, ID, loads: Types::Post

  field :post, Types::Post

  def resolve(post:)
    post.star

    {
      post: post,
    }
  end
end

By specifying that the post_id argument loads a Types::Post object type, a Post object will be loaded via Schema#object_from_id with the provided post_id.

All arguments that end in _id and use the loads: method will have their _id suffix removed. For example, the mutation resolver above receives a post argument which contains the loaded object, instead of a post_id argument.

The loads: option also works with list of IDs, for example:

class Mutations::AddStars < Mutations::BaseMutation
  argument :post_ids, [ID], loads: Types::Post

  field :posts, [Types::Post]

  def resolve(posts:)
    posts.map(&:star)

    {
      posts: posts,
    }
  end
end

All arguments that end in _ids and use the loads: method will have their _ids suffix removed and an s appended to their name. For example, the mutation resolver above receives a posts argument which contains all the loaded objects, instead of a post_ids argument.

In some cases, you may want to control the resulting argument name. This can be done using the as: argument, for example:

class Mutations::AddStar < Mutations::BaseMutation
  argument :post_id, ID, loads: Types::Post, as: :something

  field :post, Types::Post

  def resolve(something:)
    something.star

    {
      post: something
    }
  end
end

In the above examples, loads: is provided a concrete type, but it also supports abstract types (i.e. interfaces and unions).

Resolving the type of loaded objects

When loads: gets an object from Schema.object_from_id, it passes that object to Schema.resolve_type to confirm that it resolves to the same type originally configured with loads:.

Handling failed loads

If loads: fails to find an object or if the loaded object isn’t resolved to the specified loads: type (using Schema.resolve_type), a GraphQL::LoadApplicationObjectFailedError is raised and returned to the client.

You can customize this behavior by implementing def load_application_object_failed in your mutation class, for example:

def load_application_object_failed(error)
  raise GraphQL::ExecutionError, "Couldn't find an object for ID: `#{error.id}`"
end

Or, if load_application_object_failed returns a new object, that object will be used as the loads: result.

Handling unauthorized loaded objects

When an object is loaded but fails its .authorized? check, a GraphQL::UnauthorizedError is raised. By default, it’s passed to Schema.unauthorized_object (see Handling Unauthorized Objects). You can customize this behavior by implementing def unauthorized_object(err) in your mutation, for example:

def unauthorized_object(error)
  # Raise a nice user-facing error instead
  raise GraphQL::ExecutionError, "You don't have permission to modify the loaded #{error.type.graphql_name}."
end