DEV Community

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

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

Sulman Baig
Senior Solutions Architect @ UNation. Experienced in Ruby on Rails, NodeJS and VueJS web application development. Working in industry for more than 8 years now.
Updated on ・8 min read

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!

Discussion (0)