Simple ToDo GraphQL API in Ruby on Rails and MongoDB with Docker [PART 02]
Tags: rails, graphql, mongodb, docker
Published: 2020-08-01 19:15:38

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:

{% github sulmanweb/todo-app %}

The previous post is available at:

{% link sulmanweb/simple-todo-graphql-api-in-ruby-on-rails-and-mongodb-with-docker-part-01-40n7 %}

---- 
# List Model with Testing:
To create a list model, write in terminal:
```bash
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`
```ruby
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`
```ruby
has_many :lists
```

Now update factory for testing suite:

`todo-app/rails-api/spec/factories/lists.rb`
```ruby
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`
```ruby
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:
```bash
docker-compose run rails-api rails g graphql:object list
```

`todo-app/rails-api/app/graphql/types/list_type.rb`
```ruby
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`
```ruby
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`
```ruby
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`
```ruby
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`
```ruby
# 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`
```ruby
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`
```ruby
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`
```ruby
# 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`
```ruby
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`
```ruby
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
```bash
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`
```ruby
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`
```ruby
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`
```ruby
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`
```ruby
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`
```ruby
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
```bash
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`
```ruby
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`
```ruby
has_many :tasks
```

Factory and RSpec testing are in the repo.

# Task Type, Mutation and Queries:
Create task object in graphql
```bash
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`
```ruby
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`
```ruby
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`
```ruby
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`
```ruby
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`
```ruby
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`
```ruby
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`
```ruby
# 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.
---- 
![Final GraphQL API View](https://dev-to-uploads.s3.amazonaws.com/i/jho410xzhjroxla0vd4s.png)
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.

{% link sulmanweb/simple-todo-graphql-api-in-vuejs-tailwindcss-with-docker-part-03-1483 %}

Happy Coding!