Skip to content

Commit b957fb5

Browse files
authored
Merge pull request #3916 from rmosolgo/resolver-dynamism
Make many Field properties delegate to @resolver_class instead of calling .field_options once
2 parents 807948c + 3ee0762 commit b957fb5

File tree

11 files changed

+223
-121
lines changed

11 files changed

+223
-121
lines changed

lib/graphql/schema/field.rb

Lines changed: 65 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,13 @@ class FieldImplementationFailed < GraphQL::Error; end
2929
attr_reader :method_str
3030

3131
# @return [Symbol] The method on the type to look up
32-
attr_reader :resolver_method
32+
def resolver_method
33+
if @resolver_class
34+
@resolver_class.resolver_method
35+
else
36+
@resolver_method
37+
end
38+
end
3339

3440
# @return [Class] The thing this field was defined on (type, mutation, resolver)
3541
attr_accessor :owner
@@ -68,7 +74,10 @@ def inspect
6874
attr_reader :trace
6975

7076
# @return [String, nil]
71-
attr_accessor :subscription_scope
77+
def subscription_scope
78+
@subscription_scope || (@resolver_class.respond_to?(:subscription_scope) ? @resolver_class.subscription_scope : nil)
79+
end
80+
attr_writer :subscription_scope
7281

7382
# Create a field instance from a list of arguments, keyword arguments, and a block.
7483
#
@@ -82,11 +91,9 @@ def inspect
8291
# @return [GraphQL::Schema:Field] an instance of `self
8392
# @see {.initialize} for other options
8493
def self.from_options(name = nil, type = nil, desc = nil, resolver: nil, mutation: nil, subscription: nil,**kwargs, &block)
85-
if (parent_config = resolver || mutation || subscription)
86-
# Get the parent config, merge in local overrides
87-
kwargs = parent_config.field_options.merge(kwargs)
94+
if (resolver_class = resolver || mutation || subscription)
8895
# Add a reference to that parent class
89-
kwargs[:resolver_class] = parent_config
96+
kwargs[:resolver_class] = resolver_class
9097
end
9198

9299
if name
@@ -119,7 +126,9 @@ def self.from_options(name = nil, type = nil, desc = nil, resolver: nil, mutatio
119126
def connection?
120127
if @connection.nil?
121128
# Provide default based on type name
122-
return_type_name = if @return_type_expr
129+
return_type_name = if @resolver_class && @resolver_class.type
130+
Member::BuildType.to_type_name(@resolver_class.type)
131+
elsif @return_type_expr
123132
Member::BuildType.to_type_name(@return_type_expr)
124133
else
125134
# As a last ditch, try to force loading the return type:
@@ -192,7 +201,7 @@ def method_conflict_warning?
192201
# @param ast_node [Language::Nodes::FieldDefinition, nil] If this schema was parsed from definition, this AST node defined the field
193202
# @param method_conflict_warning [Boolean] If false, skip the warning if this field's method conflicts with a built-in method
194203
# @param validates [Array<Hash>] Configurations for validating this field
195-
def initialize(type: nil, name: nil, owner: nil, null: true, description: nil, deprecation_reason: nil, method: nil, hash_key: nil, dig: nil, resolver_method: nil, connection: nil, max_page_size: :not_given, scope: nil, introspection: false, camelize: true, trace: nil, complexity: 1, ast_node: nil, extras: EMPTY_ARRAY, extensions: EMPTY_ARRAY, connection_extension: self.class.connection_extension, resolver_class: nil, subscription_scope: nil, relay_node_field: false, relay_nodes_field: false, method_conflict_warning: true, broadcastable: nil, arguments: EMPTY_HASH, directives: EMPTY_HASH, validates: EMPTY_ARRAY, &definition_block)
204+
def initialize(type: nil, name: nil, owner: nil, null: true, description: nil, deprecation_reason: nil, method: nil, hash_key: nil, dig: nil, resolver_method: nil, connection: nil, max_page_size: :not_given, scope: nil, introspection: false, camelize: true, trace: nil, complexity: nil, ast_node: nil, extras: EMPTY_ARRAY, extensions: EMPTY_ARRAY, connection_extension: self.class.connection_extension, resolver_class: nil, subscription_scope: nil, relay_node_field: false, relay_nodes_field: false, method_conflict_warning: true, broadcastable: nil, arguments: EMPTY_HASH, directives: EMPTY_HASH, validates: EMPTY_ARRAY, &definition_block)
196205
if name.nil?
197206
raise ArgumentError, "missing first `name` argument or keyword `name:`"
198207
end
@@ -238,7 +247,9 @@ def initialize(type: nil, name: nil, owner: nil, null: true, description: nil, d
238247
@max_page_size = max_page_size == :not_given ? nil : max_page_size
239248
@introspection = introspection
240249
@extras = extras
241-
@broadcastable = broadcastable
250+
if !broadcastable.nil?
251+
@broadcastable = broadcastable
252+
end
242253
@resolver_class = resolver_class
243254
@scope = scope
244255
@trace = trace
@@ -282,6 +293,10 @@ def initialize(type: nil, name: nil, owner: nil, null: true, description: nil, d
282293
self.extensions(extensions)
283294
end
284295

296+
if resolver_class && resolver_class.extensions.any?
297+
self.extensions(resolver_class.extensions)
298+
end
299+
285300
if directives.any?
286301
directives.each do |(dir_class, options)|
287302
self.directive(dir_class, **options)
@@ -306,14 +321,22 @@ def initialize(type: nil, name: nil, owner: nil, null: true, description: nil, d
306321
# @return [Boolean, nil]
307322
# @see GraphQL::Subscriptions::BroadcastAnalyzer
308323
def broadcastable?
309-
@broadcastable
324+
if defined?(@broadcastable)
325+
@broadcastable
326+
elsif @resolver_class
327+
@resolver_class.broadcastable?
328+
else
329+
nil
330+
end
310331
end
311332

312333
# @param text [String]
313334
# @return [String]
314335
def description(text = nil)
315336
if text
316337
@description = text
338+
elsif @resolver_class
339+
@description || @resolver_class.description
317340
else
318341
@description
319342
end
@@ -379,7 +402,12 @@ def extension(extension_class, options = nil)
379402
def extras(new_extras = nil)
380403
if new_extras.nil?
381404
# Read the value
382-
@extras
405+
field_extras = @extras
406+
if @resolver_class && @resolver_class.extras.any?
407+
field_extras + @resolver_class.extras
408+
else
409+
field_extras
410+
end
383411
else
384412
if @extras.frozen?
385413
@extras = @extras.dup
@@ -462,33 +490,43 @@ def complexity(new_complexity = nil)
462490
when Numeric
463491
@complexity = new_complexity
464492
when nil
465-
@complexity
493+
if @resolver_class
494+
@complexity || @resolver_class.complexity || 1
495+
else
496+
@complexity || 1
497+
end
466498
else
467499
raise("Invalid complexity: #{new_complexity.inspect} on #{@name}")
468500
end
469501
end
470502

471503
# @return [Boolean] True if this field's {#max_page_size} should override the schema default.
472504
def has_max_page_size?
473-
@has_max_page_size
505+
@has_max_page_size || (@resolver_class && @resolver_class.has_max_page_size?)
474506
end
475507

476508
# @return [Integer, nil] Applied to connections if {#has_max_page_size?}
477-
attr_reader :max_page_size
509+
def max_page_size
510+
@max_page_size || (@resolver_class && @resolver_class.max_page_size)
511+
end
478512

479513
class MissingReturnTypeError < GraphQL::Error; end
480514
attr_writer :type
481515

482516
def type
483-
@type ||= if @return_type_expr.nil?
484-
# Not enough info to determine type
485-
message = "Can't determine the return type for #{self.path}"
486-
if @resolver_class
487-
message += " (it has `resolver: #{@resolver_class}`, perhaps that class is missing a `type ...` declaration, or perhaps its type causes a cyclical loading issue)"
488-
end
489-
raise MissingReturnTypeError, message
517+
if @resolver_class && (t = @resolver_class.type)
518+
t
490519
else
491-
Member::BuildType.parse_type(@return_type_expr, null: @return_type_null)
520+
@type ||= if @return_type_expr.nil?
521+
# Not enough info to determine type
522+
message = "Can't determine the return type for #{self.path}"
523+
if @resolver_class
524+
message += " (it has `resolver: #{@resolver_class}`, perhaps that class is missing a `type ...` declaration, or perhaps its type causes a cyclical loading issue)"
525+
end
526+
raise MissingReturnTypeError, message
527+
else
528+
Member::BuildType.parse_type(@return_type_expr, null: @return_type_null)
529+
end
492530
end
493531
rescue GraphQL::Schema::InvalidDocumentError, MissingReturnTypeError => err
494532
# Let this propagate up
@@ -618,14 +656,14 @@ def public_send_field(unextended_obj, unextended_ruby_kwargs, query_ctx)
618656
# - A method on the wrapped object;
619657
# - Or, raise not implemented.
620658
#
621-
if obj.respond_to?(@resolver_method)
622-
method_to_call = @resolver_method
659+
if obj.respond_to?(resolver_method)
660+
method_to_call = resolver_method
623661
method_receiver = obj
624662
# Call the method with kwargs, if there are any
625663
if ruby_kwargs.any?
626-
obj.public_send(@resolver_method, **ruby_kwargs)
664+
obj.public_send(resolver_method, **ruby_kwargs)
627665
else
628-
obj.public_send(@resolver_method)
666+
obj.public_send(resolver_method)
629667
end
630668
elsif obj.object.is_a?(Hash)
631669
inner_object = obj.object
@@ -648,7 +686,7 @@ def public_send_field(unextended_obj, unextended_ruby_kwargs, query_ctx)
648686
raise <<-ERR
649687
Failed to implement #{@owner.graphql_name}.#{@name}, tried:
650688
651-
- `#{obj.class}##{@resolver_method}`, which did not exist
689+
- `#{obj.class}##{resolver_method}`, which did not exist
652690
- `#{obj.object.class}##{@method_sym}`, which did not exist
653691
- Looking up hash key `#{@method_sym.inspect}` or `#{@method_str.inspect}` on `#{obj.object}`, but it wasn't a Hash
654692

lib/graphql/schema/member/has_arguments.rb

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,23 +78,44 @@ def #{method_owner}load_#{arg_defn.keyword}(value, _context = nil)
7878
# @return [GraphQL::Schema::Argument]
7979
def add_argument(arg_defn)
8080
@own_arguments ||= {}
81-
prev_defn = own_arguments[arg_defn.name]
81+
prev_defn = @own_arguments[arg_defn.name]
8282
case prev_defn
8383
when nil
84-
own_arguments[arg_defn.name] = arg_defn
84+
@own_arguments[arg_defn.name] = arg_defn
8585
when Array
8686
prev_defn << arg_defn
8787
when GraphQL::Schema::Argument
88-
own_arguments[arg_defn.name] = [prev_defn, arg_defn]
88+
@own_arguments[arg_defn.name] = [prev_defn, arg_defn]
8989
else
9090
raise "Invariant: unexpected `@own_arguments[#{arg_defn.name.inspect}]`: #{prev_defn.inspect}"
9191
end
9292
arg_defn
9393
end
9494

95+
def remove_argument(arg_defn)
96+
prev_defn = @own_arguments[arg_defn.name]
97+
case prev_defn
98+
when nil
99+
# done
100+
when Array
101+
prev_defn.delete(arg_defn)
102+
when GraphQL::Schema::Argument
103+
@own_arguments.delete(arg_defn.name)
104+
else
105+
raise "Invariant: unexpected `@own_arguments[#{arg_defn.name.inspect}]`: #{prev_defn.inspect}"
106+
end
107+
nil
108+
end
109+
95110
# @return [Hash<String => GraphQL::Schema::Argument] Arguments defined on this thing, keyed by name. Includes inherited definitions
96111
def arguments(context = GraphQL::Query::NullContext)
97-
inherited_arguments = ((self.is_a?(Class) && superclass.respond_to?(:arguments)) ? superclass.arguments(context) : nil)
112+
inherited_arguments = if self.is_a?(Class) && superclass.respond_to?(:arguments)
113+
superclass.arguments(context)
114+
elsif defined?(@resolver_class) && @resolver_class
115+
@resolver_class.field_arguments(context)
116+
else
117+
nil
118+
end
98119
# Local definitions override inherited ones
99120
if own_arguments.any?
100121
own_arguments_that_apply = {}
@@ -125,6 +146,10 @@ def all_argument_definitions
125146
all_defns.merge!(ancestor.own_arguments)
126147
end
127148
end
149+
elsif defined?(@resolver_class) && @resolver_class
150+
all_defns = {}
151+
all_defns.merge!(@resolver_class.own_field_arguments)
152+
all_defns.merge!(own_arguments)
128153
else
129154
all_defns = own_arguments
130155
end
@@ -137,8 +162,13 @@ def all_argument_definitions
137162
def get_argument(argument_name, context = GraphQL::Query::NullContext)
138163
warden = Warden.from_context(context)
139164
if !self.is_a?(Class)
140-
a = own_arguments[argument_name]
141-
a && Warden.visible_entry?(:visible_argument?, a, context, warden)
165+
if (arg_config = own_arguments[argument_name]) && (visible_arg = Warden.visible_entry?(:visible_argument?, arg_config, context, warden))
166+
visible_arg
167+
elsif defined?(@resolver_class) && @resolver_class
168+
@resolver_class.get_field_argument(argument_name, context)
169+
else
170+
nil
171+
end
142172
else
143173
for ancestor in ancestors
144174
if ancestor.respond_to?(:own_arguments) &&

lib/graphql/schema/relay_classic_mutation.rb

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,32 @@ def resolve_with_support(**inputs)
7070
end
7171

7272
class << self
73+
def dummy
74+
@dummy ||= begin
75+
d = Class.new(GraphQL::Schema::Resolver)
76+
d.argument_class(self.argument_class)
77+
# TODO make this lazier?
78+
d.argument(:input, input_type, description: "Parameters for #{self.graphql_name}")
79+
d
80+
end
81+
end
82+
83+
def field_arguments(context = GraphQL::Query::NullContext)
84+
dummy.arguments(context)
85+
end
86+
87+
def get_field_argument(name, context = GraphQL::Query::NullContext)
88+
dummy.get_argument(name, context)
89+
end
90+
91+
def own_field_arguments
92+
dummy.own_arguments
93+
end
7394

7495
# Also apply this argument to the input type:
75-
def argument(*args, **kwargs, &block)
96+
def argument(*args, own_argument: false, **kwargs, &block)
7697
it = input_type # make sure any inherited arguments are already added to it
77-
arg = super
98+
arg = super(*args, **kwargs, &block)
7899

79100
# This definition might be overriding something inherited;
80101
# if it is, remove the inherited definition so it's not confused at runtime as having multiple definitions
@@ -114,27 +135,24 @@ def input_type(new_input_type = nil)
114135
@input_type ||= generate_input_type
115136
end
116137

117-
# Extend {Schema::Mutation.field_options} to add the `input` argument
118-
def field_options
119-
sig = super
120-
# Arguments were added at the root, but they should be nested
121-
sig[:arguments].clear
122-
sig[:arguments][:input] = { type: input_type, required: true, description: "Parameters for #{graphql_name}" }
123-
sig
124-
end
125-
126138
private
127139

128140
# Generate the input type for the `input:` argument
129141
# To customize how input objects are generated, override this method
130142
# @return [Class] a subclass of {.input_object_class}
131143
def generate_input_type
132144
mutation_args = all_argument_definitions
133-
mutation_name = graphql_name
134145
mutation_class = self
135146
Class.new(input_object_class) do
136-
graphql_name("#{mutation_name}Input")
137-
description("Autogenerated input type of #{mutation_name}")
147+
class << self
148+
def default_graphql_name
149+
"#{self.mutation.graphql_name}Input"
150+
end
151+
152+
def description(new_desc = nil)
153+
super || "Autogenerated input type of #{self.mutation.graphql_name}"
154+
end
155+
end
138156
mutation(mutation_class)
139157
# these might be inherited:
140158
mutation_args.each do |arg|

0 commit comments

Comments
 (0)