Simple ToDo GraphQL API in Ruby on Rails and MongoDB with Docker [PART 02]
Sulman Baig
Posted on August 1, 2020
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.
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.describeList,type: :modeldoit"has a valid factory"dolist=FactoryBot.build(:list)expect(list.valid?).tobe_truthyendend
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
moduleTypesclassListType<Types::BaseObjectfield: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"endend
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.
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.
classGraphqlController<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_sessionrequire'json_web_token'defexecutevariables=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)renderjson: resultrescue=>eraiseeunlessRails.env.development?handle_error_in_developmenteenddefcurrent_user@current_user=nilifdecoded_tokendata=decoded_tokenuser=User.find(id: data[:user_id])ifdata[:user_id].present?ifdata[:user_id].present?&&!user.nil?@current_user||=userendendenddefdecoded_tokenheader=request.headers['Authorization']header=header.split(' ').lastifheaderifheaderbegin@decoded_token||=JsonWebToken.decode(header)rescueJWT::DecodeError=>eraiseGraphQL::ExecutionError.new(e.message)rescueStandardError=>eraiseGraphQL::ExecutionError.new(e.message)rescueeraiseGraphQL::ExecutionError.new(e.message)endendendprivate# Handle form data, JSON body, or a blank valuedefensure_hash(ambiguous_param)caseambiguous_paramwhenStringifambiguous_param.present?ensure_hash(JSON.parse(ambiguous_param))else{}endwhenHash,ActionController::Parametersambiguous_paramwhennil{}elseraiseArgumentError,"Unexpected parameter: #{ambiguous_param}"endenddefhandle_error_in_development(e)logger.errore.messagelogger.errore.backtrace.join("\n")renderjson: {errors: [{message: e.message,backtrace: e.backtrace}],data: {}},status: 500endend
# The method authenticates the tokendefauthenticate_userunlesscontext[:current_user]raiseGraphQL::ExecutionError.new("You must be logged in to perform this action")endend
moduleMutationsmoduleListsclassCreateList<BaseMutationdescription"Create List for the user"# Inputsargument:input,Types::Inputs::ListInput,required: true# Outputsfield:list,Types::ListType,null: falsedefresolve(input: nil)authenticate_userlist=context[:current_user].lists.build(input.to_h)iflist.save{list: list}elseraiseGraphQL::ExecutionError.new(list.errors.full_messages.join(","))endendendendend
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
moduleMutationsmoduleListsclassDeleteList<BaseMutationdescription"Deleting a List from the user"# Inputsargument:id,ID,required: true# Outputsfield:success,Boolean,null: falsedefresolve(id)authenticate_userlist=context[:current_user].lists.find(id)iflist&&list.destroy{success: true}elseraiseGraphQL::ExecutionError.new("Error removing the list.")endendendendend
Also enable these two mutations in mutation type: todo-app/rails-api/app/graphql/types/mutation_type.rb
For Query, first update base query with same authenticate user method. todo-app/rails-api/app/graphql/queries/base_query.rb
moduleQueriesclassBaseQuery<GraphQL::Schema::Resolver# The method authenticates the tokendefauthenticate_userunlesscontext[:current_user]raiseGraphQL::ExecutionError.new("You must be logged in to perform this action")endendendend
Now User List Query is simple like mutation. todo-app/rails-api/app/graphql/queries/lists/user_lists.rb
moduleQueriesmoduleListsclassUserLists<BaseQuerydescription"Get the Cureent User Lists"type[Types::ListType],null: truedefresolveauthenticate_usercontext[:current_user].listsendendendend
and to show user single list todo-app/rails-api/app/graphql/queries/lists/list_show.rb
moduleQueriesmoduleListsclassListShow<BaseQuerydescription"Get the selected list"# Inputsargument:id,ID,required: true,description: "List Id"typeTypes::ListType,null: truedefresolve(id:)authenticate_usercontext[:current_user].lists.find(id)rescueraiseGraphQL::ExecutionError.new("List Not Found")endendendend
Also on the topic we should create me query for user todo-app/rails-api/app/graphql/queries/users/me.rb
moduleQueriesmoduleUsersclassMe<BaseQuerydescription"Logged in user"# outputstypeTypes::UserType,null: falsedefresolveauthenticate_usercontext[:current_user]endendendend
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
moduleTypesclassQueryType<Types::BaseObject# Add root-level fields here.# They will be entry points for queries on your schema.field:me,resolver: Queries::Users::Mefield:user_lists,resolver: Queries::Lists::UserListsfield:show_list,resolver: Queries::Lists::ListShowendend
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
moduleTypesmoduleInputsclassTaskInput<BaseInputObjectargument:name,String,required: true,description: "Task Name"argument:list_id,ID,required: true,description: "List Id to which it is to be input"endendend
We now create three mutations create, delete and change the status
moduleMutationsmoduleTasksclassDeleteTask<BaseMutationdescription"Deleting a Task from the user's list"# Inputsargument:id,ID,required: true# Outputsfield:success,Boolean,null: falsedefresolve(id)authenticate_usertask=Task.find(id)iftask&&task.list.user==context[:current_user]&&task.destroy{success: true}elseraiseGraphQL::ExecutionError.new("Task could not be found in the system")endendendendend
moduleMutationsmoduleTasksclassChangeTaskStatus<BaseMutationdescription"Deleting a Task from the user's list"# Inputsargument:id,ID,required: true# Outputsfield:task,Types::TaskType,null: falsedefresolve(id)authenticate_usertask=Task.find(id)iftask&&task.list.user==context[:current_user]&&task.update(done: !task.done){task: task}elseraiseGraphQL::ExecutionError.new("Task could not be found in the system")endendendendend