Class: GraphQL::Schema::Resolver

Inherits:
Object
  • Object
show all
Extended by:
Member::BaseDSLMethods, Member::HasArguments, Member::HasPath
Includes:
Member::GraphQLTypeNames, Member::HasPath
Defined in:
lib/graphql/schema/resolver.rb,
lib/graphql/schema/resolver/has_payload_type.rb

Overview

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 Resolver.field_options to see how a Resolver becomes a set of field configuration options.

See Also:

  • for a concrete subclass of `Resolver`.
  • `Resolver` is a replacement for `GraphQL::Function`

Direct Known Subclasses

Mutation, Subscription

Defined Under Namespace

Modules: HasPayloadType

Constant Summary

Constants included from Member::GraphQLTypeNames

Member::GraphQLTypeNames::Boolean, Member::GraphQLTypeNames::ID, Member::GraphQLTypeNames::Int

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Member::HasPath

path

Methods included from Member::HasArguments

add_argument, argument, argument_class, arguments, coerce_arguments, own_arguments

Methods included from Member::BaseDSLMethods

accessible?, default_graphql_name, description, graphql_name, introspection, introspection?, mutation, name, overridden_graphql_name, to_graphql, visible?

Constructor Details

#initialize(object:, context:, field:) ⇒ Resolver


33
34
35
36
37
38
39
40
41
42
43
# File 'lib/graphql/schema/resolver.rb', line 33

def initialize(object:, context:, field:)
  @object = object
  @context = context
  @field = field
  # 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

Instance Attribute Details

#contextGraphQL::Query::Context (readonly)


49
50
51
# File 'lib/graphql/schema/resolver.rb', line 49

def context
  @context
end

#fieldGraphQL::Schema::Field (readonly)


52
53
54
# File 'lib/graphql/schema/resolver.rb', line 52

def field
  @field
end

#objectObject (readonly)


46
47
48
# File 'lib/graphql/schema/resolver.rb', line 46

def object
  @object
end

Class Method Details

.argument(*args, **kwargs, &block) ⇒ Object

Add an argument to this field's signature, but also add some preparation hook methods which will be used for this argument

See Also:

  • for the signature

275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
# File 'lib/graphql/schema/resolver.rb', line 275

def argument(*args, **kwargs, &block)
  loads = kwargs[:loads]
  # Use `from_resolver: true` to short-circuit the InputObject's own `loads:` implementation
  # so that we can support `#load_{x}` methods below.
  arg_defn = super(*args, from_resolver: true, **kwargs)
  own_arguments_loads_as_type[arg_defn.keyword] = loads if loads

  if loads && arg_defn.type.list?
    class_eval <<-RUBY, __FILE__, __LINE__ + 1
    def load_#{arg_defn.keyword}(values)
      argument = @arguments_by_keyword[:#{arg_defn.keyword}]
      lookup_as_type = @arguments_loads_as_type[:#{arg_defn.keyword}]
      context.schema.after_lazy(values) do |values2|
        GraphQL::Execution::Lazy.all(values2.map { |value| load_application_object(argument, lookup_as_type, value, context) })
      end
    end
    RUBY
  elsif loads
    class_eval <<-RUBY, __FILE__, __LINE__ + 1
    def load_#{arg_defn.keyword}(value)
      argument = @arguments_by_keyword[:#{arg_defn.keyword}]
      lookup_as_type = @arguments_loads_as_type[:#{arg_defn.keyword}]
      load_application_object(argument, lookup_as_type, value, context)
    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_typeObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.


312
313
314
315
# File 'lib/graphql/schema/resolver.rb', line 312

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) ⇒ Integer, Proc

Specifies the complexity of the field. Defaults to 1


246
247
248
249
250
251
# File 'lib/graphql/schema/resolver.rb', line 246

def complexity(new_complexity = nil)
  if new_complexity
    @complexity = new_complexity
  end
  @complexity || (superclass.respond_to?(:complexity) ? superclass.complexity : 1)
end

.extension(extension, **options) ⇒ Object

Registers new extension


320
321
322
# File 'lib/graphql/schema/resolver.rb', line 320

def extension(extension, **options)
  extensions << {extension => options}
end

.extensionsObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.


325
326
327
# File 'lib/graphql/schema/resolver.rb', line 325

def extensions
  @extensions ||= []
end

.extras(new_extras = nil) ⇒ Object

Additional info injected into #resolve

See Also:

  • {GraphQL{GraphQL::Schema{GraphQL::Schema::Field{GraphQL::Schema::Field#extras}

200
201
202
203
204
205
206
# File 'lib/graphql/schema/resolver.rb', line 200

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_optionsObject


253
254
255
256
257
258
259
260
261
262
263
264
265
# File 'lib/graphql/schema/resolver.rb', line 253

def field_options
  {
    type: type_expr,
    description: description,
    extras: extras,
    resolver_method: :resolve_with_support,
    resolver_class: self,
    arguments: arguments,
    null: null,
    complexity: complexity,
    extensions: extensions,
  }
end

.null(allow_null = nil) ⇒ Object

Specifies whether or not the field is nullable. Defaults to true TODO unify with #type


211
212
213
214
215
216
217
# File 'lib/graphql/schema/resolver.rb', line 211

def null(allow_null = nil)
  if !allow_null.nil?
    @null = allow_null
  end

  @null.nil? ? (superclass.respond_to?(:null) ? superclass.null : true) : @null
end

.resolve_method(new_method = nil) ⇒ Symbol

Default :resolve set below.


191
192
193
194
195
196
# File 'lib/graphql/schema/resolver.rb', line 191

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) ⇒ Class

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


226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# File 'lib/graphql/schema/resolver.rb', line 226

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_exprObject

A non-normalized type configuration, without null applied


268
269
270
# File 'lib/graphql/schema/resolver.rb', line 268

def type_expr
  @type_expr || (superclass.respond_to?(:type_expr) ? superclass.type_expr : nil)
end

Instance Method Details

#authorized?(**inputs) ⇒ Boolean, early_return_data

Called after arguments are loaded, but before resolving.

Override it to check everything before calling the mutation.

Raises:


135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/graphql/schema/resolver.rb', line 135

def authorized?(**inputs)
  self.class.arguments.each_value do |argument|
    arg_keyword = argument.keyword
    if inputs.key?(arg_keyword) && !(arg_value = inputs[arg_keyword]).nil? && (arg_value != argument.default_value)
      arg_auth, err = argument.authorized?(self, arg_value, context)
      if !arg_auth
        return arg_auth, err
      else
        true
      end
    else
      true
    end
  end
end

#ready?(**args) ⇒ Boolean, early_return_data

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).

Raises:


124
125
126
# File 'lib/graphql/schema/resolver.rb', line 124

def ready?(**args)
  true
end

#resolve(**args) ⇒ Object

Do the work. Everything happens here.


110
111
112
# File 'lib/graphql/schema/resolver.rb', line 110

def resolve(**args)
  raise GraphQL::RequiredImplementationMissingError, "#{self.class.name}#resolve should execute the field's logic"
end

#resolve_with_support(**args) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

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


58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/graphql/schema/resolver.rb', line 58

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