diff --git a/app/models/graph.rb b/app/models/graph.rb index d8ca5e0..c8cb6d8 100644 --- a/app/models/graph.rb +++ b/app/models/graph.rb @@ -5,14 +5,21 @@ def find_by_id_field(type, model) type type argument :id, !types.ID resolve ->(_, args, _) do - gid = GlobalID.parse(args[:id]) + model_id = Graph.parse_id(args[:id], model) - return unless gid - return unless gid.model_name == type.name - - Graph::FindLoader.for(model).load(gid.model_id.to_i) + Graph::FindLoader.for(model).load(model_id) end end end + + def parse_id(gid, model) + parsed_gid = GlobalID.parse(gid) + + return unless parsed_gid + return unless parsed_gid.app == GlobalID.app + return unless parsed_gid.model_name != model.name.downcase + + parsed_gid.model_id.to_i + end end end diff --git a/app/models/graph/mutations/.keep b/app/models/graph/mutations/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/app/models/graph/mutations/film_rate.rb b/app/models/graph/mutations/film_rate.rb new file mode 100644 index 0000000..bf1dae0 --- /dev/null +++ b/app/models/graph/mutations/film_rate.rb @@ -0,0 +1,39 @@ +module Graph + module Mutations + FilmRate = GraphQL::Relay::Mutation.define do + name "FilmRate" + + input_field :filmId, !types.ID + input_field :rating, !types.Int + + return_field :film, !Graph::Types::Film + return_field :rating, Graph::Types::Rating + return_field :errors, !types[!Graph::Types::MutationError] + + resolve ->(_, input, ctx) do + raise GraphQL::ExecutionError.new('Authentication required to rate a film.') unless user = ctx[:user] + + film_id = Graph.parse_id(input['filmId'], Film) + film = Film.find_by(id: film_id) if film_id + raise GraphQL::ExecutionError.new('Invalid filmId.') unless film + + rating = user.ratings.where(film: film).first_or_initialize + rating.rating = input['rating'] + + if rating.save + { + film: film, + rating: rating, + errors: [] + } + else + { + film: film, + rating: nil, + errors: rating.errors.map { |field, message| MutationError.new(field, message) }, + } + end + end + end + end +end diff --git a/app/models/graph/mutations/mutation.rb b/app/models/graph/mutations/mutation.rb new file mode 100644 index 0000000..38d901c --- /dev/null +++ b/app/models/graph/mutations/mutation.rb @@ -0,0 +1,8 @@ +module Graph + module Mutations + Mutation = GraphQL::ObjectType.define do + name "Mutation" + field :filmRate, field: Graph::Mutations::FilmRate.field + end + end +end diff --git a/app/models/graph/mutations/mutation_error.rb b/app/models/graph/mutations/mutation_error.rb new file mode 100644 index 0000000..d6c6920 --- /dev/null +++ b/app/models/graph/mutations/mutation_error.rb @@ -0,0 +1,6 @@ +module Graph + module Mutations + class MutationError < Struct.new(:field, :message) + end + end +end diff --git a/app/models/graph/schema.rb b/app/models/graph/schema.rb index 7c508dc..59ab357 100644 --- a/app/models/graph/schema.rb +++ b/app/models/graph/schema.rb @@ -1,6 +1,7 @@ module Graph Schema = GraphQL::Schema.define do query Graph::Types::Query + mutation Graph::Mutations::Mutation resolve_type ->(obj, ctx) do Graph::Schema.types.values.find { |type| type.name == obj.class.name } @@ -12,6 +13,8 @@ module Graph object_from_id ->(id, query_ctx) do gid = GlobalID.parse(id) + return unless gid + possible_types = query_ctx.warden.possible_types(GraphQL::Relay::Node.interface) return unless possible_types.map(&:name).include?(gid.model_name) diff --git a/app/models/graph/types/mutation_error.rb b/app/models/graph/types/mutation_error.rb new file mode 100644 index 0000000..35e1d28 --- /dev/null +++ b/app/models/graph/types/mutation_error.rb @@ -0,0 +1,10 @@ +module Graph + module Types + MutationError = GraphQL::ObjectType.define do + name "MutationError" + + field :field, !types.String, "The name of the input field that caused the error." + field :message, !types.String, "The description of the error." + end + end +end diff --git a/app/models/rating.rb b/app/models/rating.rb index f48f86f..cf185b1 100644 --- a/app/models/rating.rb +++ b/app/models/rating.rb @@ -2,5 +2,5 @@ class Rating < ApplicationRecord belongs_to :user belongs_to :film - validates_inclusion_of :rating, in: 0..5 + validates :rating, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 5 } end diff --git a/test/models/graph/mutations/film_rate_test.rb b/test/models/graph/mutations/film_rate_test.rb new file mode 100644 index 0000000..559d300 --- /dev/null +++ b/test/models/graph/mutations/film_rate_test.rb @@ -0,0 +1,136 @@ +require 'test_helper' + +class Graph::Mutations::FilmRateTest < ActiveSupport::TestCase + def setup + @context = { + user: @user = User.first + } + + @query_string = " + mutation ($input: FilmRateInput!) { + filmRate(input: $input) { + film { + title + } + rating { + rating + } + errors { + field + message + } + } + } + " + + @film = Film.first + + @variables = { + "input" => { + "filmId" => @film.to_global_id.to_s, + "rating" => 5, + } + } + end + + test "raises an execution error when user is not logged in" do + expected = { + "data" => { "filmRate" => nil}, + "errors" => [{ + "message" => "Authentication required to rate a film.", + "locations" => [{ "line" => 3, "column" => 9}], + "path" => ["filmRate"]} + ] + } + + result = Graph::Schema.execute(@query_string, variables: @variables, context: {}) + assert_equal expected, result + end + + test "raises an execution error when filmId is invalid" do + @variables['input']['filmId'] = 'invalid' + expected = { + "data" => { "filmRate" => nil}, + "errors" => [{ + "message" => "Invalid filmId.", + "locations" => [{ "line" => 3, "column" => 9}], + "path" => ["filmRate"]} + ] + } + + result = Graph::Schema.execute(@query_string, variables: @variables, context: @context) + assert_equal expected, result + end + + test "returns error when an invalid rating is inputted" do + @variables['input']['rating'] = 100 + expected = { + "data" => { + "filmRate" => { + "film" => { + "title" => @film.title + }, + "rating" => nil, + "errors" => [ + { "field" => "rating", "message" => "must be less than or equal to 5" } + ] + } + } + } + + result = Graph::Schema.execute(@query_string, variables: @variables, context: @context) + assert_equal expected, result + end + + test "creates a new rating on success" do + expected = { + "data" => { + "filmRate" => { + "film" => { + "title" => @film.title + }, + "rating" => { + "rating" => 5 + }, + "errors" => [], + } + } + } + + assert_difference "Rating.count", 1 do + result = Graph::Schema.execute(@query_string, variables: @variables, context: @context) + assert_equal expected, result + end + + rating = Rating.last + assert_equal @film, rating.film + assert_equal @variables['input']['rating'], rating.rating + assert_equal @user, rating.user + end + + test "updates existing rating if user already rated film" do + expected = { + "data" => { + "filmRate" => { + "film" => { + "title" => @film.title + }, + "rating" => { + "rating" => 5 + }, + "errors" => [], + } + } + } + + rating = @user.ratings.create(film: @film, rating: 1) + + assert_difference "Rating.count", 0 do + result = Graph::Schema.execute(@query_string, variables: @variables, context: @context) + assert_equal expected, result + end + + rating.reload + assert_equal @variables['input']['rating'], rating.rating + end +end