DEV Community

Cover image for Simple ToDo GraphQL API in Ruby on Rails and MongoDB with Docker [PART 02]
Sulman Baig
Sulman Baig

Posted on • Edited on

Simple ToDo GraphQL API in Ruby on Rails and MongoDB with Docker [PART 02]

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

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




The previous post is available at:


List Model with Testing:

To create a list model, write in terminal:

docker-compose run rails-api rails g model List name:string 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

Also add to user model:

todo-app/rails-api/app/models/user.rb

has_many :lists 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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" 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

Now run RSpec using following command in terminal

docker-compose run rails-api bin/rspec 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

Add the following in list model

todo-app/rails-api/app/models/list.rb

has_many :tasks 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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" 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

All RSpec Testing is in Repo.

Final GraphQL API View
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.

Happy Coding!

Top comments (0)