File: mutation_classes.md

package info (click to toggle)
ruby-graphql 2.2.17-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 9,584 kB
  • sloc: ruby: 67,505; ansic: 1,753; yacc: 831; javascript: 331; makefile: 6
file content (192 lines) | stat: -rw-r--r-- 6,851 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
---
layout: guide
doc_stub: false
search: true
section: Mutations
title: Mutation Classes
desc: Use mutation classes to implement behavior, then hook them up to your schema.
index: 1
redirect_from:
  - /queries/mutations/
  - /relay/mutations/
---

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

- Create, update or destroy records in the database
- Establish associations between already-existing records in the database
- Increment counters
- Create, modify or delete files
- Clear caches

These actions are called _side effects_.

Like all GraphQL fields, mutation fields:

- Accept inputs, called _arguments_
- Return values via _fields_

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

- {{ "GraphQL::Schema::Mutation" | api_doc }}, a bare-bones base class
- {{ "GraphQL::Schema::RelayClassicMutation" | api_doc }}, a base class with a set of nice conventions that also supports the Relay Classic mutation specification.

Besides those, you can also use the plain {% internal_link "field API", "/type_definitions/objects#fields" %} to write mutation fields.

## Example mutation class

If you used the {% internal_link "install generator", "/schema/generators#graphqlinstall" %}, 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:

```ruby
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:

```ruby
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 {% internal_link "Mutation Errors", "/mutations/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:

```ruby
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:

```ruby
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 {% internal_link "`Schema#object_from_id`", "/schema/definition.html#object-identification-hooks" %} 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:

```ruby
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:

```ruby
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" | api_doc }}, it passes that object to {{ "Schema.resolve_type" | api_doc }} 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" | api_doc }}), a {{ "GraphQL::LoadApplicationObjectFailedError" | api_doc }} 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:

```ruby
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 {% internal_link "`.authorized?` check", "/authorization/authorization#object-authorization" %}, a {{ "GraphQL::UnauthorizedError" | api_doc }} is raised. By default, it's passed to {{ "Schema.unauthorized_object" | api_doc }} (see {% internal_link "Handling Unauthorized Objects", "/authorization/authorization.html#handling-unauthorized-objects" %}). You can customize this behavior by implementing `def unauthorized_object(err)` in your mutation, for example:

```ruby
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
```