class GraphQL::Schema::Resolver
A class-based container for field configuration and resolution logic. It supports:
-
Arguments, via `.argument(…)` helper, which will be applied to the field.
-
Return type, via `.type(…, null: …)`, which will be applied to the field.
-
Description, via `.description(…)`, which will be applied to the field
-
Resolution, via `#resolve(**args)` method, which will be called to resolve the field.
-
`#object` and `#context` accessors for use during `#resolve`.
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
@return [GraphQL::Query::Context]
@return [Object] The application object this field is being resolved on
Public Class Methods
@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
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
# 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
@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
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
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
# 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
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
# File lib/graphql/schema/resolver.rb, line 381 def own_arguments_loads_as_type @own_arguments_loads_as_type ||= {} end
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
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
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
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
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
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
# 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
# File lib/graphql/schema/resolver.rb, line 239 def load_application_object_failed(err) raise err end
# File lib/graphql/schema/resolver.rb, line 166 def load_argument(name, value) public_send("load_#{name}", value) end
# 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
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