In the previous version, we created a rails API in docker with MongoDB and graphql initializations. Then we went to create mutations for signup and sign in users and testing those with RSpec. Now we continue with the project further by creating mutations and queries for user lists and todos which we will call tasks here.
Code Repo is the same as the previous part:
TODO APP
built by @sulmanweb
Technologies
- Docker
API (rails-api)
- Ruby on Rails 6
- MongoDB 4
- GraphQL
Vue Front End
- VueJS 2
- TailwindCSS
- FontAwesome
- Apollo GraphQL Client
To Run
- Need Docker Installed in system
- In terminal in working repo write
docker-compose up --build -d
- Rails API Playground will be at http://localhost:3000/graphiql
- Front end App can be viewed at http://localhost:8080
The previous post is available at:
Simple ToDo GraphQL API in Ruby on Rails and MongoDB with Docker [PART 01]
Sulman Baig ・ Aug 1 '20
List Model with Testing:
To create a list model, write in terminal:
docker-compose run rails-api rails g model List name:string
This will create a model, factory, and RSpec testing model files.
Modify the list model to create a relationship with user and validation.
todo-app/rails-api/app/models/list.rb
class List
include Mongoid::Document
field :name, type: String
belongs_to :user
validates :name, presence: true
end
Also add to user model:
todo-app/rails-api/app/models/user.rb
has_many :lists
Now update factory for testing suite:
todo-app/rails-api/spec/factories/lists.rb
FactoryBot.define do
factory :list do
name { "MyString" }
association :user
end
end
Finally, Create an RSpec test for the list model. I simply created the test for a valid factory:
todo-app/rails-api/spec/models/list_spec.rb
require 'rails_helper'
RSpec.describe List, type: :model do
it "has a valid factory" do
list = FactoryBot.build(:list)
expect(list.valid?).to be_truthy
end
end
User’s lists Types, Mutations, and Queries:
Now create the List Type by writing in terminal:
docker-compose run rails-api rails g graphql:object list
todo-app/rails-api/app/graphql/types/list_type.rb
module Types
class ListType < Types::BaseObject
field :id, ID, null: false, description: "MongoDB List id string"
field :name, String, null: false, description: "Name of the List"
field :user, Types::UserType, null: false, description: "User of the List"
end
end
Here we included user that will automatically picked up by graphql because mongoid has the relationship. Same we add to users:
todo-app/rails-api/app/graphql/types/user_type.rb
field :lists, [Types::ListType], null: true, description: "User's Lists in the system"
So while we output a user, we can output the user’s list because of has many relationship.
Now we create a list input type that will be a simple one argument which is the name.
todo-app/rails-api/app/graphql/types/inputs/list_input.rb
module Types
module Inputs
class ListInput < BaseInputObject
argument :name, String, required: true, description: "List Name"
end
end
end
Now we create mutations of creating and delete lists. First, we create a method of authenticate_user
so that we can define which user’s list is being created. So put a method in base mutation file and graphql controller file.
todo-app/rails-api/app/controllers/graphql_controller.rb
class GraphqlController < ApplicationController
# If accessing from outside this domain, nullify the session
# This allows for outside API access while preventing CSRF attacks,
# but you'll have to authenticate your user separately
# protect_from_forgery with: :null_session
require 'json_web_token'
def execute
variables = ensure_hash(params[:variables])
query = params[:query]
operation_name = params[:operationName]
context = {
# Query context goes here, for example:
current_user: current_user,
decoded_token: decoded_token
}
result = RailsApiSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
render json: result
rescue => e
raise e unless Rails.env.development?
handle_error_in_development e
end
def current_user
@current_user = nil
if decoded_token
data = decoded_token
user = User.find(id: data[:user_id]) if data[:user_id].present?
if data[:user_id].present? && !user.nil?
@current_user ||= user
end
end
end
def decoded_token
header = request.headers['Authorization']
header = header.split(' ').last if header
if header
begin
@decoded_token ||= JsonWebToken.decode(header)
rescue JWT::DecodeError => e
raise GraphQL::ExecutionError.new(e.message)
rescue StandardError => e
raise GraphQL::ExecutionError.new(e.message)
rescue e
raise GraphQL::ExecutionError.new(e.message)
end
end
end
private
# Handle form data, JSON body, or a blank value
def ensure_hash(ambiguous_param)
case ambiguous_param
when String
if ambiguous_param.present?
ensure_hash(JSON.parse(ambiguous_param))
else
{}
end
when Hash, ActionController::Parameters
ambiguous_param
when nil
{}
else
raise ArgumentError, "Unexpected parameter: #{ambiguous_param}"
end
end
def handle_error_in_development(e)
logger.error e.message
logger.error e.backtrace.join("\n")
render json: { errors: [{ message: e.message, backtrace: e.backtrace }], data: {} }, status: 500
end
end
todo-app/rails-api/app/graphql/mutations/base_mutation.rb
# The method authenticates the token
def authenticate_user
unless context[:current_user]
raise GraphQL::ExecutionError.new("You must be logged in to perform this action")
end
end
Now mutations are simple:
todo-app/rails-api/app/graphql/mutations/lists/create_list.rb
module Mutations
module Lists
class CreateList < BaseMutation
description "Create List for the user"
# Inputs
argument :input, Types::Inputs::ListInput, required: true
# Outputs
field :list, Types::ListType, null: false
def resolve(input: nil)
authenticate_user
list = context[:current_user].lists.build(input.to_h)
if list.save
{list: list}
else
raise GraphQL::ExecutionError.new(list.errors.full_messages.join(","))
end
end
end
end
end
Delete list only requires id and authenticate user will tell if the user is trying to delete his/her own list.
todo-app/rails-api/app/graphql/mutations/lists/delete_list.rb
module Mutations
module Lists
class DeleteList < BaseMutation
description "Deleting a List from the user"
# Inputs
argument :id, ID, required: true
# Outputs
field :success, Boolean, null: false
def resolve(id)
authenticate_user
list = context[:current_user].lists.find(id)
if list && list.destroy
{success: true}
else
raise GraphQL::ExecutionError.new("Error removing the list.")
end
end
end
end
end
Also enable these two mutations in mutation type:
todo-app/rails-api/app/graphql/types/mutation_type.rb
# List
field :create_list, mutation: Mutations::Lists::CreateList
field :delete_list, mutation: Mutations::Lists::DeleteList
The RSpec test are are now like signup signin:
todo-app/rails-api/spec/graphql/mutations/lists/create_list_spec.rb
require 'rails_helper'
module Mutations
module Lists
RSpec.describe CreateList, type: :request do
describe '.resolve' do
it 'creates a users list' do
user = FactoryBot.create(:user)
headers = sign_in_test_headers user
query = <<~GQL
mutation {
createList(input: {name: "Test List"}) {
list {
id
}
}
}
GQL
post '/graphql', params: {query: query}, headers: headers
expect(response).to have_http_status(200)
json = JSON.parse(response.body)
expect(json["data"]["createList"]["list"]["id"]).not_to be_nil
end
end
end
end
end
todo-app/rails-api/spec/graphql/mutations/lists/delete_list_spec.rb
require 'rails_helper'
module Mutations
module Lists
RSpec.describe DeleteList, type: :request do
describe '.resolve' do
it 'deletes a users list' do
user = FactoryBot.create(:user)
list = FactoryBot.create(:list, user_id: user.id)
headers = sign_in_test_headers user
query = <<~GQL
mutation {
deleteList(id: "#{list.id}") {
success
}
}
GQL
post '/graphql', params: {query: query}, headers: headers
expect(response).to have_http_status(200)
json = JSON.parse(response.body)
expect(json["data"]["deleteList"]["success"]).to be_truthy
end
end
end
end
end
Now run RSpec using following command in terminal
docker-compose run rails-api bin/rspec
You can now see new mutations in the UI as well.
For Query, first update base query with same authenticate user method.
todo-app/rails-api/app/graphql/queries/base_query.rb
module Queries
class BaseQuery < GraphQL::Schema::Resolver
# The method authenticates the token
def authenticate_user
unless context[:current_user]
raise GraphQL::ExecutionError.new("You must be logged in to perform this action")
end
end
end
end
Now User List Query is simple like mutation.
todo-app/rails-api/app/graphql/queries/lists/user_lists.rb
module Queries
module Lists
class UserLists < BaseQuery
description "Get the Cureent User Lists"
type [Types::ListType], null: true
def resolve
authenticate_user
context[:current_user].lists
end
end
end
end
and to show user single list
todo-app/rails-api/app/graphql/queries/lists/list_show.rb
module Queries
module Lists
class ListShow < BaseQuery
description "Get the selected list"
# Inputs
argument :id, ID, required: true, description: "List Id"
type Types::ListType, null: true
def resolve(id:)
authenticate_user
context[:current_user].lists.find(id)
rescue
raise GraphQL::ExecutionError.new("List Not Found")
end
end
end
end
Also on the topic we should create me
query for user
todo-app/rails-api/app/graphql/queries/users/me.rb
module Queries
module Users
class Me < BaseQuery
description "Logged in user"
# outputs
type Types::UserType, null: false
def resolve
authenticate_user
context[:current_user]
end
end
end
end
So me
query can show all the data to create an app including user info, lists, and tasks as well.
Enable Queries by adding to query type.
todo-app/rails-api/app/graphql/types/query_type.rb
module Types
class QueryType < Types::BaseObject
# Add root-level fields here.
# They will be entry points for queries on your schema.
field :me, resolver: Queries::Users::Me
field :user_lists, resolver: Queries::Lists::UserLists
field :show_list, resolver: Queries::Lists::ListShow
end
end
RSpec Tests are given in the repo code.
Task Model:
Create a task model by writing in terminal
docker-compose run rails-api rails g model Task name:string done:boolean
and change the model to the following code
todo-app/rails-api/app/models/task.rb
class Task
include Mongoid::Document
field :name, type: String
field :done, type: Boolean, default: false
belongs_to :list
validates :name, presence: true
end
Add the following in list model
todo-app/rails-api/app/models/list.rb
has_many :tasks
Factory and RSpec testing are in the repo.
Task Type, Mutation and Queries:
Create task object in graphql
docker-compose run rails-api rails g graphql:object task
Add following code in task type
todo-app/rails-api/app/graphql/types/task_type.rb
module Types
class TaskType < Types::BaseObject
field :id, ID, null: false, description: "MongoDB Tassk id string"
field :name, String, null: true, description: "Task's name"
field :done, Boolean, null: true, description: "Task's status"
field :list, Types::ListType, null: true, description: "Task's List"
end
end
We added to list as a parent of the task and similarly, in the list we show dependent task, and then we don’t need queries as list will be enough.
todo-app/rails-api/app/graphql/types/list_type.rb
field :tasks, [Types::TaskType], null: true, description: "List Tasks"
Now even making user’s me
query can show user, user's lists and list tasks if want to.
Now we create input type which will be name of task:
todo-app/rails-api/app/graphql/types/inputs/task_input.rb
module Types
module Inputs
class TaskInput < BaseInputObject
argument :name, String, required: true, description: "Task Name"
argument :list_id, ID, required: true, description: "List Id to which it is to be input"
end
end
end
We now create three mutations create, delete and change the status
todo-app/rails-api/app/graphql/mutations/tasks/create_task.rb
module Mutations
module Tasks
class CreateTask < BaseMutation
description "Create Task in user's list"
argument :input, Types::Inputs::TaskInput, required: true
field :task, Types::TaskType, null: false
def resolve(input: nil)
authenticate_user
list = context[:current_user].lists.find(input.list_id)
if list
task = list.tasks.build(name: input.name)
if task.save
{task: task}
else
raise GraphQL::ExecutionError.new(task.errors.full_messages.join(', '))
end
else
raise GraphQL::ExecutionError.new("List Not Found")
end
end
end
end
end
todo-app/rails-api/app/graphql/mutations/tasks/delete_task.rb
module Mutations
module Tasks
class DeleteTask < BaseMutation
description "Deleting a Task from the user's list"
# Inputs
argument :id, ID, required: true
# Outputs
field :success, Boolean, null: false
def resolve(id)
authenticate_user
task = Task.find(id)
if task && task.list.user == context[:current_user] && task.destroy
{success: true}
else
raise GraphQL::ExecutionError.new("Task could not be found in the system")
end
end
end
end
end
todo-app/rails-api/app/graphql/mutations/tasks/change_task_status.rb
module Mutations
module Tasks
class ChangeTaskStatus < BaseMutation
description "Deleting a Task from the user's list"
# Inputs
argument :id, ID, required: true
# Outputs
field :task, Types::TaskType, null: false
def resolve(id)
authenticate_user
task = Task.find(id)
if task && task.list.user == context[:current_user] && task.update(done: !task.done)
{task: task}
else
raise GraphQL::ExecutionError.new("Task could not be found in the system")
end
end
end
end
end
Add to mutation type to enable:
todo-app/rails-api/app/graphql/types/mutation_type.rb
# Task
field :create_task, mutation: Mutations::Tasks::CreateTask
field :delete_task, mutation: Mutations::Tasks::DeleteTask
field :change_task_status, mutation: Mutations::Tasks::ChangeTaskStatus
All RSpec Testing is in Repo.
So Now everything we need from a graphql API. Now we will create the VueJS app for this ToDo App in the next part.
In the next part, I will create vue app with tailwindcss for these queries and mutations to work in front end.
Simple ToDo GraphQL API in VueJS & TailwindCSS with Docker [PART 03]
Sulman Baig ・ Aug 14 '20
Happy Coding!
Top comments (0)