class GraphQL::Schema::Resolver

A class-based container for field configuration and resolution logic. It supports:

Resolvers can be attached with the `resolver:` option in a `field(…)` call.

A resolver's configuration may be overridden with other keywords in the `field(…)` call.

See the {.field_options} to see how a Resolver becomes a set of field configuration options.

@see {GraphQL::Schema::Mutation} for a concrete subclass of `Resolver`. @see {GraphQL::Function} `Resolver` is a replacement for `GraphQL::Function`

Attributes

context[R]

@return [GraphQL::Query::Context]

object[R]

@return [Object] The application object this field is being resolved on

Public Class Methods

new(object:, context:) click to toggle source

@param object [Object] the initialize object, pass to {Query.initialize} as `root_value` @param context [GraphQL::Query::Context]

# File lib/graphql/schema/resolver.rb, line 31
def initialize(object:, context:)
  @object = object
  @context = context
  # Since this hash is constantly rebuilt, cache it for this call
  @arguments_by_keyword = {}
  self.class.arguments.each do |name, arg|
    @arguments_by_keyword[arg.keyword] = arg
  end
  @arguments_loads_as_type = self.class.arguments_loads_as_type
end

Private Class Methods

argument(name, type, *rest, loads: nil, **kwargs, &block) click to toggle source

Add an argument to this field's signature, but also add some preparation hook methods which will be used for this argument @see {GraphQL::Schema::Argument#initialize} for the signature

Calls superclass method
# File lib/graphql/schema/resolver.rb, line 329
        def argument(name, type, *rest, loads: nil, **kwargs, &block)
          if loads
            name_as_string = name.to_s

            inferred_arg_name = case name_as_string
            when /_id$/
              name_as_string.sub(/_id$/, "").to_sym
            when /_ids$/
              name_as_string.sub(/_ids$/, "")
                .sub(/([^s])$/, "\\1s")
                .to_sym
            else
              name
            end

            kwargs[:as] ||= inferred_arg_name
            own_arguments_loads_as_type[kwargs[:as]] = loads
          end

          arg_defn = super(name, type, *rest, **kwargs, &block)

          if loads && arg_defn.type.list?
            class_eval <<-RUBY, __FILE__, __LINE__ + 1
            def load_#{arg_defn.keyword}(values)
              GraphQL::Execution::Lazy.all(values.map { |value| load_application_object(:#{arg_defn.keyword}, value) })
            end
            RUBY
          elsif loads
            class_eval <<-RUBY, __FILE__, __LINE__ + 1
            def load_#{arg_defn.keyword}(value)
              load_application_object(:#{arg_defn.keyword}, value)
            end
            RUBY
          else
            class_eval <<-RUBY, __FILE__, __LINE__ + 1
            def load_#{arg_defn.keyword}(value)
              value
            end
            RUBY
          end

          arg_defn
        end
arguments_loads_as_type() click to toggle source

@api private

# File lib/graphql/schema/resolver.rb, line 374
def arguments_loads_as_type
  inherited_lookups = superclass.respond_to?(:arguments_loads_as_type) ? superclass.arguments_loads_as_type : {}
  inherited_lookups.merge(own_arguments_loads_as_type)
end
complexity(new_complexity = nil) click to toggle source

Specifies the complexity of the field. Defaults to `1` @return [Integer, Proc]

# File lib/graphql/schema/resolver.rb, line 301
def complexity(new_complexity = nil)
  if new_complexity
    @complexity = new_complexity
  end
  @complexity || (superclass.respond_to?(:complexity) ? superclass.complexity : 1)
end
extras(new_extras = nil) click to toggle source

Additional info injected into {#resolve} @see {GraphQL::Schema::Field#extras}

# File lib/graphql/schema/resolver.rb, line 255
def extras(new_extras = nil)
  if new_extras
    @own_extras = new_extras
  end
  own_extras = @own_extras || []
  own_extras + (superclass.respond_to?(:extras) ? superclass.extras : [])
end
field_options() click to toggle source
# File lib/graphql/schema/resolver.rb, line 308
def field_options
  {
    type: type_expr,
    description: description,
    extras: extras,
    method: :resolve_with_support,
    resolver_class: self,
    arguments: arguments,
    null: null,
    complexity: complexity,
  }
end
null(allow_null = nil) click to toggle source

Specifies whether or not the field is nullable. Defaults to `true` TODO unify with {#type} @param allow_null [Boolean] Whether or not the response can be null

# File lib/graphql/schema/resolver.rb, line 266
def null(allow_null = nil)
  if !allow_null.nil?
    @null = allow_null
  end

  @null.nil? ? (superclass.respond_to?(:null) ? superclass.null : true) : @null
end
own_arguments_loads_as_type() click to toggle source
# File lib/graphql/schema/resolver.rb, line 381
def own_arguments_loads_as_type
  @own_arguments_loads_as_type ||= {}
end
resolve_method(new_method = nil) click to toggle source

Default `:resolve` set below. @return [Symbol] The method to call on instances of this object to resolve the field

# File lib/graphql/schema/resolver.rb, line 246
def resolve_method(new_method = nil)
  if new_method
    @resolve_method = new_method
  end
  @resolve_method || (superclass.respond_to?(:resolve_method) ? superclass.resolve_method : :resolve)
end
type(new_type = nil, null: nil) click to toggle source

Call this method to get the return type of the field, or use it as a configuration method to assign a return type instead of generating one. TODO unify with {#null} @param new_type [Class, nil] If a type definition class is provided, it will be used as the return type of the field @param null [true, false] Whether or not the field may return `nil` @return [Class] The type which this field returns.

# File lib/graphql/schema/resolver.rb, line 281
def type(new_type = nil, null: nil)
  if new_type
    if null.nil?
      raise ArgumentError, "required argument `null:` is missing"
    end
    @type_expr = new_type
    @null = null
  else
    if @type_expr
      GraphQL::Schema::Member::BuildType.parse_type(@type_expr, null: @null)
    elsif superclass.respond_to?(:type)
      superclass.type
    else
      nil
    end
  end
end
type_expr() click to toggle source

A non-normalized type configuration, without `null` applied

# File lib/graphql/schema/resolver.rb, line 322
def type_expr
  @type_expr || (superclass.respond_to?(:type_expr) ? superclass.type_expr : nil)
end

Public Instance Methods

authorized?(**args) click to toggle source

Called after arguments are loaded, but before resolving.

Override it to check everything before calling the mutation. @param args [Hash] The input arguments @raise [GraphQL::ExecutionError] To add an error to the response @raise [GraphQL::UnauthorizedError] To signal an authorization failure @return [Boolean, early_return_data] If `false`, execution will stop (and `early_return_data` will be returned instead, if present.)

# File lib/graphql/schema/resolver.rb, line 129
def authorized?(**args)
  true
end
ready?(**args) click to toggle source

Called before arguments are prepared. Implement this hook to make checks before doing any work.

If it returns a lazy object (like a promise), it will be synced by GraphQL (but the resulting value won't be used).

@param args [Hash] The input arguments, if there are any @raise [GraphQL::ExecutionError] To add an error to the response @raise [GraphQL::UnauthorizedError] To signal an authorization failure @return [Boolean, early_return_data] If `false`, execution will stop (and `early_return_data` will be returned instead, if present.)

# File lib/graphql/schema/resolver.rb, line 118
def ready?(**args)
  true
end
resolve(**args) click to toggle source

Do the work. Everything happens here. @return [Object] An object corresponding to the return type

# File lib/graphql/schema/resolver.rb, line 104
def resolve(**args)
  raise NotImplementedError, "#{self.class.name}#resolve should execute the field's logic"
end
resolve_with_support(**args) click to toggle source

This method is actually called by the runtime, it does some preparation and then eventually calls the user-defined `#resolve` method. @api private

# File lib/graphql/schema/resolver.rb, line 52
def resolve_with_support(**args)
  # First call the ready? hook which may raise
  ready_val = if args.any?
    ready?(**args)
  else
    ready?
  end
  context.schema.after_lazy(ready_val) do |is_ready, ready_early_return|
    if ready_early_return
      if is_ready != false
        raise "Unexpected result from #ready? (expected `true`, `false` or `[false, {...}]`): [#{authorized_result.inspect}, #{ready_early_return.inspect}]"
      else
        ready_early_return
      end
    elsif is_ready
      # Then call each prepare hook, which may return a different value
      # for that argument, or may return a lazy object
      load_arguments_val = load_arguments(args)
      context.schema.after_lazy(load_arguments_val) do |loaded_args|
        # Then call `authorized?`, which may raise or may return a lazy object
        authorized_val = if loaded_args.any?
          authorized?(loaded_args)
        else
          authorized?
        end
        context.schema.after_lazy(authorized_val) do |(authorized_result, early_return)|
          # If the `authorized?` returned two values, `false, early_return`,
          # then use the early return value instead of continuing
          if early_return
            if authorized_result == false
              early_return
            else
              raise "Unexpected result from #authorized? (expected `true`, `false` or `[false, {...}]`): [#{authorized_result.inspect}, #{early_return.inspect}]"
            end
          elsif authorized_result
            # Finally, all the hooks have passed, so resolve it
            if loaded_args.any?
              public_send(self.class.resolve_method, **loaded_args)
            else
              public_send(self.class.resolve_method)
            end
          else
            nil
          end
        end
      end
    end
  end
end

Private Instance Methods

load_application_object(arg_kwarg, id) click to toggle source
# File lib/graphql/schema/resolver.rb, line 196
def load_application_object(arg_kwarg, id)
  argument = @arguments_by_keyword[arg_kwarg]
  lookup_as_type = @arguments_loads_as_type[arg_kwarg]
  # See if any object can be found for this ID
  loaded_application_object = object_from_id(lookup_as_type, id, context)
  context.schema.after_lazy(loaded_application_object) do |application_object|
    begin
      if application_object.nil?
        raise LoadApplicationObjectFailedError.new(argument: argument, id: id, object: application_object)
      end
      # Double-check that the located object is actually of this type
      # (Don't want to allow arbitrary access to objects this way)
      application_object_type = context.schema.resolve_type(lookup_as_type, application_object, context)
      possible_object_types = context.schema.possible_types(lookup_as_type)
      if !possible_object_types.include?(application_object_type)
        raise LoadApplicationObjectFailedError.new(argument: argument, id: id, object: application_object)
      else
        # This object was loaded successfully
        # and resolved to the right type,
        # now apply the `.authorized?` class method if there is one
        if (class_based_type = application_object_type.metadata[:type_class])
          context.schema.after_lazy(class_based_type.authorized?(application_object, context)) do |authed|
            if authed
              application_object
            else
              raise GraphQL::UnauthorizedError.new(
                object: application_object,
                type: class_based_type,
                context: context,
              )
            end
          end
        else
          application_object
        end
      end
    rescue LoadApplicationObjectFailedError => err
      # pass it to a handler
      load_application_object_failed(err)
    end
  end
end
load_application_object_failed(err) click to toggle source
# File lib/graphql/schema/resolver.rb, line 239
def load_application_object_failed(err)
  raise err
end
load_argument(name, value) click to toggle source
# File lib/graphql/schema/resolver.rb, line 166
def load_argument(name, value)
  public_send("load_#{name}", value)
end
load_arguments(args) click to toggle source
# File lib/graphql/schema/resolver.rb, line 135
def load_arguments(args)
  prepared_args = {}
  prepare_lazies = []

  args.each do |key, value|
    arg_defn = @arguments_by_keyword[key]
    if arg_defn
      if value.nil?
        prepared_args[key] = value
      else
        prepped_value = prepared_args[key] = load_argument(key, value)
        if context.schema.lazy?(prepped_value)
          prepare_lazies << context.schema.after_lazy(prepped_value) do |finished_prepped_value|
            prepared_args[key] = finished_prepped_value
          end
        end
      end
    else
      # These are `extras: [...]`
      prepared_args[key] = value
    end
  end

  # Avoid returning a lazy if none are needed
  if prepare_lazies.any?
    GraphQL::Execution::Lazy.all(prepare_lazies).then { prepared_args }
  else
    prepared_args
  end
end
object_from_id(type, id, context) click to toggle source

Look up the corresponding object for a provided ID. By default, it uses Relay-style {Schema.object_from_id}, override this to find objects another way.

@param type [Class, Module] A GraphQL type definition @param id [String] A client-provided to look up @param context [GraphQL::Query::Context] the current context

# File lib/graphql/schema/resolver.rb, line 192
def object_from_id(type, id, context)
  context.schema.object_from_id(id, context)
end