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)