graphql-ruby icon indicating copy to clipboard operation
graphql-ruby copied to clipboard

improve load_application_object_failed

Open rmosolgo opened this issue 3 years ago • 2 comments

When loads: ... fails to find an object, or when it finds an object that doesn't resolve to the loads: ... type, it calls load_application_object_failed(err).

Currently, the code assumes that load_application_object_failed(err) will raise an error, halting control flow in the caller.

However, if you want to use errors-as-data in your mutation, this doesn't work, because you don't want to raise an error here. So, we should:

  • Support errors-as-data when loads: ... fails
  • Make sure the docs describe the expected behavior of that method
  • Improve the runtime behavior when those expectations aren't met (currently the method just keeps running with nil, resulting in weird behavior)
  • Make sure errors from that method hit normal rescue_from handlers

rmosolgo avatar Dec 01 '21 15:12 rmosolgo

Hi @rmosolgo, wanted to give some insight into how we're currently using (or abusing) load_application_object_failed.

We're currently transitioning from integer IDs to global IDs, and doing so in a gradual approach and allowing consumers to opt in. When doing this, we can expect to receive both global and non-global IDs for arguments. This has led us to discover the loads keyword for arguments, and it works well when provided a global ID (as the schema level object_from_id method can find the object), but doesn't work well with the integer IDs.

To assist with the transition we've looked at implementing load_application_object_failed to find the object using the following:

# method added to base mutation and resolver classes
def load_application_object_failed(error)
  type_class = error.argument.loads
  type_class.model.find_by(id: error.id)
end

Types which are implementing global IDs have a model added, which is an ActiveRecord class. This appeared to work well - however we also noticed some issues which would be blocking for us to use this approach. The authorized? method isn't called on the type even when it might be returned from load_application_object_failed. We noticed that this else block doesn't get triggered: https://github.com/rmosolgo/graphql-ruby/blob/master/lib/graphql/schema/member/has_arguments.rb#L290

However, the mutation would still resolve, even if not authorized, and the user would only hit unauthorized errors when getting the fields returned from the mutation.


Should we be using this approach?

joelzwarrington avatar Dec 14 '21 17:12 joelzwarrington

Wanted to add on to my previous comment, after finding out about the authorized? issue, we have used a work-around to get around this if anyone is in a similar ⛵

  • remove load_application_object_failed and don't override the method
  • augment our BaseArgument to handle both cases using prepare so that a global ID is always returned
class BaseArgument < GraphQL::Schema::Argument
  def initialize(*args, prepare: nil, loads: nil, **kwargs, &block)
    if loads.present?
      prepare = ->(value, _context) {
        return value if global_id? # this check isn't important, but assume it's being done in some way
        
        object = loads.model.find_by(id: value)
        type = loads

        return if object.nil?

        global_id(object, type)
     }
   end

   super(*args, prepare: prepare, loads: loads, **kwargs, &block)
end

This way object_from_id will always be a global ID and an object should be found regardless of an integer or global ID is being used.

joelzwarrington avatar Dec 14 '21 19:12 joelzwarrington

Hi @rmosolgo , have you had a change to come back to this issue?

In our GraphQL API we return most errors as userErrors (errors as data), except the ones caused by failed loads because these raise an error and it is handled in the top level errors.

I've been trying to find a way to hook into load_application_object_failed but instead of raising an error or continuing the normal execution of the mutation resolver, to return a payload like this:

{
  event: nil,
  user_errors: {
    message: "Could not find Event",
    code: "NOT_FOUND,
    path: ["input", "eventId"]
  }
}

That would be added in the response data under the appropriate mutation field.

Is there a workaround for this problem or is this behaviour currently not possible?

rodloboz avatar Oct 22 '22 09:10 rodloboz

Hey, after a while, I finally got a PR for this: #4667. After that PR, any object returned from load_application_object_failed is passed along as if it had been loaded :+1:

rmosolgo avatar Oct 17 '23 18:10 rmosolgo