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
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
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
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.
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
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
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
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
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
Paste the output of that command into a variable in .env
:
JWT_SECRET=<rake output>
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
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
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.
Now we can submit GraphQL queries again. Let’s create a new topic:
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.
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
- clone the repo
git clone https://github.com/speratus/miniforum.git
- install the dependencies
bundle install
- run the server
rails s
Top comments (0)