Skip to content

Commit

Permalink
Merge pull request #4667 from rmosolgo/load-application-object-failed…
Browse files Browse the repository at this point in the history
…-improvements

Load application object failed improvements
  • Loading branch information
rmosolgo authored Oct 17, 2023
2 parents d5968d9 + 6dd7d16 commit ab01562
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 37 deletions.
4 changes: 3 additions & 1 deletion guides/mutations/mutation_classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,10 +170,12 @@ You can customize this behavior by implementing `def load_application_object_fai

```ruby
def load_application_object_failed(error)
nil # instead of returning an error, fail silently.
raise GraphQL::ExecutionError, "Couldn't find an object for ID: `#{error.id}`"
end
```

Or, if `load_application_object_fails` returns a new object, that object will be used as the `loads:` result.

### Handling unauthorized loaded objects

When an object is _loaded_ but fails its {% internal_link "`.authorized?` check", "/authorization/authorization#object-authorization" %}, a {{ "GraphQL::UnauthorizedError" | api_doc }} is raised. By default, it's passed to {{ "Schema.unauthorized_object" | api_doc }} (see {% internal_link "Handling Unauthorized Objects", "/authorization/authorization.html#handling-unauthorized-objects" %}). You can customize this behavior by implementing `def unauthorized_object(err)` in your mutation, for example:
Expand Down
6 changes: 5 additions & 1 deletion lib/graphql/load_application_object_failed_error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@ class LoadApplicationObjectFailedError < GraphQL::ExecutionError
attr_reader :id
# @return [Object] The value found with this ID
attr_reader :object
def initialize(argument:, id:, object:)
# @return [GraphQL::Query::Context]
attr_reader :context

def initialize(argument:, id:, object:, context:)
@id = id
@argument = argument
@object = object
@context = context
super("No object found for `#{argument.graphql_name}: #{id.inspect}`")
end
end
Expand Down
74 changes: 41 additions & 33 deletions lib/graphql/schema/member/has_arguments.rb
Original file line number Diff line number Diff line change
Expand Up @@ -379,44 +379,52 @@ def load_and_authorize_application_object(argument, id, context)
def authorize_application_object(argument, id, context, loaded_application_object)
context.query.after_lazy(loaded_application_object) do |application_object|
if application_object.nil?
err = GraphQL::LoadApplicationObjectFailedError.new(argument: argument, id: id, object: application_object)
load_application_object_failed(err)
err = GraphQL::LoadApplicationObjectFailedError.new(context: context, argument: argument, id: id, object: application_object)
application_object = load_application_object_failed(err)
end
# Double-check that the located object is actually of this type
# (Don't want to allow arbitrary access to objects this way)
maybe_lazy_resolve_type = context.schema.resolve_type(argument.loads, application_object, context)
context.query.after_lazy(maybe_lazy_resolve_type) do |resolve_type_result|
if resolve_type_result.is_a?(Array) && resolve_type_result.size == 2
application_object_type, application_object = resolve_type_result
else
application_object_type = resolve_type_result
# application_object is already assigned
end
if application_object.nil?
nil
else
maybe_lazy_resolve_type = context.schema.resolve_type(argument.loads, application_object, context)
context.query.after_lazy(maybe_lazy_resolve_type) do |resolve_type_result|
if resolve_type_result.is_a?(Array) && resolve_type_result.size == 2
application_object_type, application_object = resolve_type_result
else
application_object_type = resolve_type_result
# application_object is already assigned
end

if !(
context.warden.possible_types(argument.loads).include?(application_object_type) ||
context.warden.loadable?(argument.loads, context)
)
err = GraphQL::LoadApplicationObjectFailedError.new(argument: argument, id: id, object: application_object)
load_application_object_failed(err)
else
# This object was loaded successfully
# and resolved to the right type,
# now apply the `.authorized?` class method if there is one
context.query.after_lazy(application_object_type.authorized?(application_object, context)) do |authed|
if authed
application_object
else
err = GraphQL::UnauthorizedError.new(
object: application_object,
type: application_object_type,
context: context,
)
if self.respond_to?(:unauthorized_object)
err.set_backtrace(caller)
unauthorized_object(err)
if !(
context.warden.possible_types(argument.loads).include?(application_object_type) ||
context.warden.loadable?(argument.loads, context)
)
err = GraphQL::LoadApplicationObjectFailedError.new(context: context, argument: argument, id: id, object: application_object)
application_object = load_application_object_failed(err)
end

if application_object.nil?
nil
else
# This object was loaded successfully
# and resolved to the right type,
# now apply the `.authorized?` class method if there is one
context.query.after_lazy(application_object_type.authorized?(application_object, context)) do |authed|
if authed
application_object
else
raise err
err = GraphQL::UnauthorizedError.new(
object: application_object,
type: application_object_type,
context: context,
)
if self.respond_to?(:unauthorized_object)
err.set_backtrace(caller)
unauthorized_object(err)
else
raise err
end
end
end
end
Expand Down
96 changes: 94 additions & 2 deletions spec/graphql/schema/resolver_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@

describe GraphQL::Schema::Resolver do
module ResolverTest
class SpecialError < StandardError
attr_accessor :id
end

class LazyBlock
def initialize(&block)
@get_value = block
Expand Down Expand Up @@ -296,11 +300,21 @@ def object_from_id(type, id, ctx)
end

def resolve(int:)
int * 4
int ? int * 4 : nil
end

def load_application_object_failed(err)
raise GraphQL::ExecutionError.new("ResolverWithErrorHandler failed for id: #{err.id.inspect} (#{err.object.inspect}) (#{err.class.name})")
if (next_obj = err.context[:fallback_object])
next_obj
elsif err.context[:return_nil_on_load_failed]
nil
elsif err.context[:reraise_special_error]
spec_err = SpecialError.new
spec_err.id = err.id
raise(spec_err)
else
raise(GraphQL::ExecutionError.new("ResolverWithErrorHandler failed for id: #{err.id.inspect} (#{err.object.inspect}) (#{err.class.name})"))
end
end
end

Expand Down Expand Up @@ -494,6 +508,10 @@ def self.object_from_id(id, ctx)
1
end
end

rescue_from SpecialError do |err|
raise GraphQL::ExecutionError, "A special error was raised from #{err.id.inspect}"
end
end
end

Expand Down Expand Up @@ -588,6 +606,43 @@ def exec_query(*args, **kwargs)
expected_err = "ResolverWithErrorHandler failed for id: \"failed_to_find\" (nil) (GraphQL::LoadApplicationObjectFailedError)"
assert_equal [expected_err], res["errors"].map { |e| e["message"] }
end

it "uses rescue_from handlers" do
query_str = <<-GRAPHQL
{
resolverWithErrorHandler(int: "failed_to_find") { value }
}
GRAPHQL

res = exec_query(query_str, context: { reraise_special_error: true })
assert_nil res["data"].fetch("resolverWithErrorHandler")
expected_err = "A special error was raised from \"failed_to_find\""
assert_equal [expected_err], res["errors"].map { |e| e["message"] }
end

it "uses the new value from lookup" do
query_str = <<-GRAPHQL
{
resolverWithErrorHandler(int: "failed_to_find") { value }
}
GRAPHQL

res = exec_query(query_str, context: { fallback_object: 212 })
assert_equal 848, res["data"]["resolverWithErrorHandler"]["value"]
refute res.key?("errors")
end

it "halts silently when it returns nil" do
query_str = <<-GRAPHQL
{
resolverWithErrorHandler(int: "failed_to_find") { value }
}
GRAPHQL

res = exec_query(query_str, context: { return_nil_on_load_failed: true })
assert_nil res["data"].fetch("resolverWithErrorHandler")
refute res.key?("errors")
end
end

describe "when resolve_type returns a no-good type" do
Expand All @@ -603,6 +658,43 @@ def exec_query(*args, **kwargs)
expected_err = "ResolverWithErrorHandler failed for id: \"resolve_type_as_wrong_type\" (:resolve_type_as_wrong_type) (GraphQL::LoadApplicationObjectFailedError)"
assert_equal [expected_err], res["errors"].map { |e| e["message"] }
end

it "uses rescue_from handlers" do
query_str = <<-GRAPHQL
{
resolverWithErrorHandler(int: "resolve_type_as_wrong_type") { value }
}
GRAPHQL

res = exec_query(query_str, context: { reraise_special_error: true })
assert_nil res["data"].fetch("resolverWithErrorHandler")
expected_err = "A special error was raised from \"resolve_type_as_wrong_type\""
assert_equal [expected_err], res["errors"].map { |e| e["message"] }
end

it "uses a new value from resolve_type" do
query_str = <<-GRAPHQL
{
resolverWithErrorHandler(int: "resolve_type_as_wrong_type") { value }
}
GRAPHQL

res = exec_query(query_str, context: { fallback_object: 4 })
assert_equal 16, res["data"]["resolverWithErrorHandler"]["value"]
refute res.key?("errors")
end

it "halts silently when it returns nil" do
query_str = <<-GRAPHQL
{
resolverWithErrorHandler(int: "resolve_type_as_wrong_type") { value }
}
GRAPHQL

res = exec_query(query_str, context: { return_nil_on_load_failed: true })
assert_nil res["data"].fetch("resolverWithErrorHandler")
refute res.key?("errors")
end
end
end

Expand Down

0 comments on commit ab01562

Please sign in to comment.