Simple ToDo GraphQL API in Ruby on Rails and MongoDB with Docker and VueJS (PART 02)

In this tutorial, We will create a to-do app with Rails API, MongoDB, GraphQL, and VueJS using docker compose

Simple ToDo GraphQL API in Ruby on Rails and MongoDB with Docker and VueJS (PART 02)

In the previous version, we created rails api in docker with mongodb and graphql initialisations. 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 to-dos which we will call tasks here.

Code Repo is same as previous part:

sulmanweb/todo-app
ToDo App. Contribute to sulmanweb/todo-app development by creating an account on GitHub.
GitHub Repo

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

This will create model, factory and rspec testing model files.

Modify list model to create 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 RSpec test for list model. I simply created the test for 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 a user that will automatically pick 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 are output user, we can output user's list because of has many relations.

Now we create list input type that will be simple one argument which is a 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 create and delete list. 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 user is trying to delete his/her 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 now like signup sign-in:

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

Furthermore, 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

As 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 task model by writing in terminal

docker-compose run rails-api rails g model Task name:string done:boolean

and change the model to 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 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 list as parent of task and similarly in 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, lists, and list tasks if you want to.

Now we create input type which will be named 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 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 VueJS app for this To-Do App in next part.

Happy Coding!