DEV Community

Andrew Luchuk
Andrew Luchuk

Posted on

GraphQL Mutations

Last week, I described the basics of GraphQL in this post. We covered the basics of GraphQL and the fundamental concepts that go into setting up and using GraphQL. This week, I am covering the critical piece that I left out last week: GraphQL mutations.

GraphQL mutations allow developers to modify the data stored on the server.

GraphQL doesn't know what to do with any data you send to it by itself, so we have to tell it specifically what to do in each mutation we create.

Creating Mutations

The GraphQL gem has a handy generator which we can use to get started building our mutations. Navigate to the project directory and run the following commands:

rails g graphql:mutation create_topic
rails g graphql:mutation create_reply
rails g graphql:mutation create_like
Enter fullscreen mode Exit fullscreen mode

Each of these commands creates a new named mutation. The only argument the generator requires is the name of the mutation.

Add the following code to app/graphql/mutations/create_topic.rb:

module Mutations
  class CreateTopic < BaseMutation
    # just like queries, mutations return certain fields
    # in this case, the mutation returns a list of strings which are potential errors
    # and the topic that was created if the creation was successful
    field :errors, [String], null: false
    field :topic, Types::TopicType, null: true

    # mutations also take arguments, very similar to the way queries can take arguments
    argument :user_id, ID, required: true
    argument :title, String, required: true
    argument :content, String, required: true


    # Unlike queries, mutations must have a resolve method to tell
    # GraphQL what to do with the mutation and the arguments it receives.
    def resolve(title:, content:, user_id:)
      # In this case, we will create a new topic.
      topic = Topic.new(title: title, content: content, user_id: user_id)
      if topic.save
        {
          topic: topic,
          errors: []
        }
      else
        {
          topic: nil,
          errors: topic.errors.full_messages
        }
      end
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Fields, arguments, and the resolve method make up the majority of GraphQL mutations.

GraphQL does not know how to handle the polymorphic association on its own, we'll have add some code specifically to address those:

module Mutations
  class CreateReply < BaseMutation
    # As with create_topic, we have fields, arguments and a resolve method
    field :errors, [String], null: false
    field :reply, Types::ReplyType, null: false

    # We need to give the pieces of the polymorphic info that it needs to
    # properly construct the association
    argument :post_id, ID, required: true
    # post_type tells Rails what kind of model to look for.
    argument :post_type, String, required: true

    argument :user_id, ID, required: true
    argument :content, String, required: true

    def resolve(post_id:, user_id:, content:, post_type:)
      # Use the Rails method `constantize` to turn a string into a constant
      # which we know should refer to an ActiveRecord model, allowing us to run
      # the ActiveRecord method `find_by` to get the correct object with that type
      type = post_type.constantize
      post = type.find_by(id: post_id)
      reply = Reply.new(content: content, user_id: user_id, post: post)
      if reply.save
        {
          reply: reply,
          errors: []
        }
      else
        {
          reply: nil,
          errors: reply.errors.full_messages
        }
      end
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Now try to implement a create_like mutation on your own. If you get stuck, you can always check out the source code for the project here.

GitHub logo speratus / miniforum

Miniforum is a tiny forum app built for a series of blogs

Creating Users and Login Sessions

We need to make user creation separate from GraphQL so that we can easily require all users to be logged in before accessing the GraphQL endpoint.

As a result, we'll need to generate a controller dedicated to user creation:

rails g controller users create
Enter fullscreen mode Exit fullscreen mode

Open up the new controller and add the following:

# app/controllers/users_controller.rb
class UsersController < ApplicationController

    def create
        user = User.new(user_params)

        if user.save
            render json: user_json(user)
        else
            render json: {
                message: 'Failed to create user',
                errors: user.errors.full_messages
            }
        end
    end


    private

    # Use strong parameters to prevent any unwanted parameters from getting through.
    # The password confirmation field is required to allow BCrypt to properly hash the password.
    def user_params
        params.require(:user).permit(:name, :username, :email, :password, :password_confirmation)
    end

    # Turns a user model object into a hash which can be converted to json. Maybe
    # not necessary in a controller this small, but certainly necessary in a larger project.
    def user_json(user)
        user.as_json(only: [:id, :name, :username, :email])
    end
end
Enter fullscreen mode Exit fullscreen mode

Authenticating Users

Since we're using JWT to handle user sessions, we'll need to add another controller to allow users to create those sessions by logging in.

Create a new controller to handle login sessions and add the following code:

rails g controller sessions create
Enter fullscreen mode Exit fullscreen mode

Now edit the new controller:

# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController

    def create
        # find the user by the username included in the parameters
        user = User.find_by(username: session_params[:username])
        # If the user exists, and can be authenticated with the given password, generate a token 
        # for the session and return it.
        if user && user.authenticate(session_params[:password])
            # Generate the token based on the user’s id. Use the value of JWT_SECRET in 
            # the `.env` file.
            token = JWT.encode({user_id: user.id}, ENV['JWT_SECRET'])
            render json: {token: token, user_id: user.id}
        else
            render json: {message: "Incorrect username or password"}
        end
    end

    private

    # Use strong parameters to make sure we only get a username and password.
    def session_params
        params.require(:session).permit(:username, :password)
    end
end
Enter fullscreen mode Exit fullscreen mode

In order to use the JWT_SECRET variable, we need to create a .env file in the root project directory.

To keep the JWT_SECRET secure, be sure to add .env to the project’s .gitignore file. Make sure that .env is never committed into a repository that is publicly accessible.

Once .env is created, run the following command:

rake secret
Enter fullscreen mode Exit fullscreen mode

Paste the output of that command into a variable in .env:

JWT_SECRET=<rake output>
Enter fullscreen mode Exit fullscreen mode

Doing this will ensure that you have a cryptographically secure number to use for encoding the user’s session data.

We need to add a current_user method to ApplicationController so that the back end can check whether a user is authenticated.

# app/controllers/application_controller.rb

# This error is used below to specify that a session is not authenticated
class AuthenticationError < StandardError

end

class ApplicationController < ActionController::API
    def current_user
        # Authentication setting the `Access-Token` header to the result of 
        # the create session route
        token = request.headers['Access-Token']
        raise AuthenticationError if token.nil?
        # Decodes the JWT token and returns only the user id from it.
        user_id = JWT.decode(token, ENV['JWT_SECRET'])[0]['user_id']
        @user = User.find_by(id: user_id)
    end
end
Enter fullscreen mode Exit fullscreen mode

Finally, make current_user the first method invocation in the execute route of the GraphQL controller.

# app/controllers/graphql_controller.rb
class GraphqlController < ApplicationController

  def execute
    current_user
    variables = ensure_hash(params[:variables])
    ...
  end
  ...
end
Enter fullscreen mode Exit fullscreen mode

With that done, we are ready to test our mutations.

Testing GraphQL Mutations with Postman

To test our graphql mutations, we’ll need to have a user in our database. You can use the rails console to create a user directly or you can send a post request to the users_controller using Postman.

Once you’ve created a user, we have to get the access token. Start up the rails development server and send a post request to the /sessions endpoint of the back end with the username and password you want to log in to. If everything is set up correctly, then you should receive a json response with the token in it. Paste the new token as the Access-Token header into the Postman request.

Place Access Token here

Now we can submit GraphQL queries again. Let’s create a new topic:
Run the CreateTopic mutation

As you can see, to run a mutation we have to use the mutation keyword along with the name of the mutation and a json object containing all the required arguments.

You can also use a json object to hold all the GraphQL variables, but for a single query like the one above, putting them in the query string is a little easier.

Our backend should be nominally complete now. We can log in and run queries and create objects. We haven’t implemented updating objects or deleting them yet. Try to create update and delete mutations on your own.

Thank you for reading! As always the final code is available here. If you have any questions, don’t hesitate to ask them in the comments section.

GitHub logo speratus / miniforum

Miniforum is a tiny forum app built for a series of blogs

Travis Build Status

Miniforum

Miniforum is a small Forum app built for a blog series on dev.to: Build a Forum App, from code to Deploy.

Features

  • Sign up as a user to post questions/answers
  • Like posts or replies to express appreciation for the question
  • Ranks users and topics according to the number of people who have liked

Stack

  • Ruby on Rails
  • GraphQL
  • Minitest
  • TravisCI

See full details of the whole application stack here.

Installation

  1. clone the repo
git clone https://github.com/speratus/miniforum.git
Enter fullscreen mode Exit fullscreen mode
  1. install the dependencies
bundle install
Enter fullscreen mode Exit fullscreen mode
  1. run the server
rails s
Enter fullscreen mode Exit fullscreen mode

Top comments (0)