Rails Kitchen

Its a place to write on stuff I learned recently.

GraphQL -Mutation Query Implementation - Ruby on Rails

| Comments

Mutation is a special type of query used to change data in the database like Creating, Editing or Deleting Records from a table or Store. These are the equivalent to the POST, PUT, PATCH and DELETE in HTTP/REST speak. Defining mutations is very similar to defining queries. The only difference is how you implement the logic inside the mutation. In mutation, we can control and specify the output data that API need to return after mutation procedure.

In this article, I am Adding a mutation query to add comments to an article which we discussed in previous example.

To add mutations to your GraphQL schema, first we need to define a mutation type in mutations folder
app/graphql/mutations/mutation_type.rb
1
2
3
MutationType = GraphQL::ObjectType.define do
  name "Mutation"
end
Now we need to pass it into the schema
app/graphql/graphql_ruby_sample_schema.rb
1
2
3
4
GraphqlRubySampleSchema = GraphQL::Schema.define do
  query QueryType
  mutation MutationType
end
Like QueryType, MutationType is a root of the schema. Members of MutationType are mutation fields. For GraphQL in general, mutation fields are identical to query fields except that they have side-effects (which mutate application state, eg, update the database).

Since we created new folder for mutations, we have to tell Rails to autoload paths. Put below code in application.rb to autoload it.
config/application.rb.rb
1
config.autoload_paths << Rails.root.join('app/graphql/mutations')

Now we need to define specific mutation query. Following are the process to define a mutation
- give operation a name
- declare its inputs
- declare its outputs
- declare the mutation procedure in resolve block.
resolve should return a hash with a key for each of the return_fields

In out example, we need to define CommentMutations in mutations folder.
app/graphql/mutations/comment_mutations.rb
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
# encoding: utf-8
module CommentMutations
  Create = GraphQL::Relay::Mutation.define do
    name "AddComment"

    # Define input parameters
    input_field :articleId, !types.ID
    input_field :userId, !types.ID
    input_field :comment, !types.String

    # Define return parameters
    return_field :article, ArticleType
    return_field :errors, types.String


    resolve ->(object, inputs, ctx) {
      article = Article.find_by_id(inputs[:articleId])
      return { errors: 'Article not found' } if article.nil?

      comments = article.comments
      new_comment = comments.build(user_id: inputs[:userId], comment: inputs[:comment])
      if new_comment.save
        { article: article }
      else
        { errors: new_comment.errors.to_a }
      end
    }
  end
end
Here input_field specify the input params we can pass in the query. In return_field, we can specify the fields returning after the update. Inside resolve block, we define the business logic. and resolve should return a hash with a key for each of the return_fields.

After defining this, we need to add the mutation’s derived field to the mutation type.
app/graphql/mutations/mutation_type.rb
1
2
3
4
5
MutationType = GraphQL::ObjectType.define do
  name "Mutation"
  # Add the mutation's derived field to the mutation type
  field :addComment, field: CommentMutations::Create.field
end
Now we can try this mutation in GraphiQL:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
mutation addComment{
  addComment(input: { comment: "New comment", articleId: 1, userId: 1})
  {
    article{
      id
      comments{
        comment
        user{
          name
        }
      }
    }
  }
}
Here’s a possible JSON response for that query:
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
{
  "data": {
    "addComment": {
      "article": {
        "id": 1,
        "comments": [
          {
            "comment": "Good article",
            "user": {
              "name": "Shaiju E"
            }
          },
          {
            "comment": "Keep going",
            "user": {
              "name": "Shaiju E"
            }
          },
          {
            "comment": "Another Comment",
            "user": {
              "name": "David"
            }
          },
          {
            "comment": "New Comment",
            "user": {
              "name": "Shaiju E"
            }
          },
          {
            "comment": "Another Comment from User 2",
            "user": {
              "name": "David"
            }
          },
          {
            "comment": "Another Comment from User 1",
            "user": {
              "name": "Shaiju E"
            }
          },
          {
            "comment": "TEST",
            "user": {
              "name": "Shaiju E"
            }
          },
          {
            "comment": "New comment",
            "user": {
              "name": "Shaiju E"
            }
          }
        ]
      }
    }
  }
}
We can call the same query by passing inputs using variables
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mutation addComment($comments: AddCommentInput!){
  addComment(input: $comments){
    article{
      id
      comments{
        comment
        user{
          name
        }
      }
    }
  }
}

Query Variabbles

{
  "comments": {
    "comment": "New comment1",
    "articleId": 1,
    "userId": 1
  }
}
$comments: AddCommentInput! will configure the variable $comments to take values from query variables section. input: $comments will pass $comments as input to mutation query.

Lets write another example for updation mutation. If we want to update a comment, we need to write UpdateComment mutation in comment_mutations.rb
app/graphql/mutations/comment_mutations.rb
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
# encoding: utf-8
module CommentMutations
  Create = GraphQL::Relay::Mutation.define do
    name "AddComment"

    # Define input parameters
    input_field :articleId, !types.ID
    input_field :userId, !types.ID
    input_field :comment, !types.String

    # Define return parameters
    return_field :article, ArticleType
    return_field :errors, types.String


    resolve ->(object, inputs, ctx) {
      article = Article.find_by_id(inputs[:articleId])
      return { errors: 'Article not found' } if article.nil?

      comments = article.comments
      new_comment = comments.build(user_id: inputs[:userId], comment: inputs[:comment])
      if new_comment.save
        { article: article }
      else
        { errors: new_comment.errors.to_a }
      end
    }
  end

  Update = GraphQL::Relay::Mutation.define do
    name "UpdateComment"

    # Define input parameters
    input_field :id, !types.ID
    input_field :comment, types.ID
    input_field :userId, types.ID
    input_field :articleId, types.ID

    # Define return parameters
    return_field :comment, CommentType
    return_field :errors, types.String


    resolve ->(object, inputs, ctx) {
      comment = Comment.find_by_id(inputs[:id])
      return { errors: 'Comment not found' } if comment.nil?

      valid_inputs = ActiveSupport::HashWithIndifferentAccess.new(inputs.instance_variable_get(:@original_values).select { |k, _| comment.respond_to? "#{k}=".underscore }).except(:id)
      if comment.update_attributes(valid_inputs)
        { comment: comment }
      else
        { errors: comment.errors.to_a }
      end
    }
  end
end
Main defference here is, we need to create valid_inputs. This will allow us mass assignment with update attributes with valied fields which we passed.

After defining this, we need to add the mutation’s derived field to the mutation type.
app/graphql/mutations/mutation_type.rb
1
2
3
4
5
6
MutationType = GraphQL::ObjectType.define do
  name "Mutation"
  # Add the mutation's derived field to the mutation type
  field :addComment, field: CommentMutations::Create.field
  field :updateComment, field: CommentMutations::Update.field
end
Mutation for delete a comment and return post and deleted comment ID
app/graphql/mutations/comment_mutations.rb
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
# encoding: utf-8
module CommentMutations
  Destroy = GraphQL::Relay::Mutation.define do
    name 'DestroyComment'
    description 'Delete a comment and return post and deleted comment ID'

    # Define input parameters
    input_field :id, !types.ID

    # Define return parameters
    return_field :deletedId, !types.ID
    return_field :article, ArticleType
    return_field :errors, types.String

    resolve ->(_obj, inputs, ctx) {
      comment = Comment.find_by_id(inputs[:id])
      return { errors: 'Comment not found' } if comment.nil?

      article = comment.article
      comment.destroy

      { article: article.reload, deletedId: inputs[:id] }
    }
  end

  # Other mutations defined here....
end
app/graphql/mutations/mutation_type.rb
1
2
3
4
5
6
7
MutationType = GraphQL::ObjectType.define do
  name "Mutation"
  # Add the mutation's derived field to the mutation type
  field :addComment, field: CommentMutations::Create.field
  field :updateComment, field: CommentMutations::Update.field
  field :destroyComment, field: CommentMutations::Destroy.field
end
Now we can try this mutation in GraphiQL:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mutation destroyComment($comment: DestroyCommentInput!){
  destroyComment(input: $comment){
    errors
    article{
      id
      comments{
        id
        comment
      }
    }
  }
}

Query Variabbles

{
  "comment": {
    "id": 3
  }
}
You can see sample code here.

Comments