graphql 2.3.5 → 2.3.14

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/install_generator.rb +46 -0
  3. data/lib/graphql/analysis/analyzer.rb +89 -0
  4. data/lib/graphql/analysis/field_usage.rb +82 -0
  5. data/lib/graphql/analysis/max_query_complexity.rb +20 -0
  6. data/lib/graphql/analysis/max_query_depth.rb +20 -0
  7. data/lib/graphql/analysis/query_complexity.rb +183 -0
  8. data/lib/graphql/analysis/{ast/query_depth.rb → query_depth.rb} +23 -25
  9. data/lib/graphql/analysis/visitor.rb +283 -0
  10. data/lib/graphql/analysis.rb +92 -1
  11. data/lib/graphql/current.rb +52 -0
  12. data/lib/graphql/dataloader/async_dataloader.rb +2 -0
  13. data/lib/graphql/dataloader/source.rb +5 -2
  14. data/lib/graphql/dataloader.rb +4 -1
  15. data/lib/graphql/execution/interpreter/arguments_cache.rb +5 -10
  16. data/lib/graphql/execution/interpreter/runtime.rb +29 -25
  17. data/lib/graphql/execution/interpreter.rb +3 -1
  18. data/lib/graphql/execution/lookahead.rb +10 -10
  19. data/lib/graphql/introspection/directive_type.rb +1 -1
  20. data/lib/graphql/introspection/entry_points.rb +2 -2
  21. data/lib/graphql/introspection/field_type.rb +1 -1
  22. data/lib/graphql/introspection/schema_type.rb +6 -11
  23. data/lib/graphql/introspection/type_type.rb +5 -5
  24. data/lib/graphql/language/document_from_schema_definition.rb +19 -26
  25. data/lib/graphql/language/lexer.rb +0 -3
  26. data/lib/graphql/language/nodes.rb +2 -2
  27. data/lib/graphql/language/parser.rb +9 -1
  28. data/lib/graphql/language/sanitized_printer.rb +1 -1
  29. data/lib/graphql/language.rb +0 -1
  30. data/lib/graphql/query/context.rb +7 -1
  31. data/lib/graphql/query/null_context.rb +2 -2
  32. data/lib/graphql/query/validation_pipeline.rb +2 -2
  33. data/lib/graphql/query.rb +26 -7
  34. data/lib/graphql/rubocop/graphql/field_type_in_block.rb +129 -0
  35. data/lib/graphql/rubocop/graphql/root_types_in_block.rb +38 -0
  36. data/lib/graphql/rubocop.rb +2 -0
  37. data/lib/graphql/schema/addition.rb +1 -0
  38. data/lib/graphql/schema/always_visible.rb +1 -0
  39. data/lib/graphql/schema/argument.rb +19 -5
  40. data/lib/graphql/schema/build_from_definition.rb +8 -1
  41. data/lib/graphql/schema/directive/flagged.rb +1 -1
  42. data/lib/graphql/schema/directive.rb +2 -0
  43. data/lib/graphql/schema/enum.rb +51 -20
  44. data/lib/graphql/schema/field/connection_extension.rb +1 -1
  45. data/lib/graphql/schema/field.rb +85 -39
  46. data/lib/graphql/schema/has_single_input_argument.rb +2 -1
  47. data/lib/graphql/schema/input_object.rb +8 -7
  48. data/lib/graphql/schema/interface.rb +20 -4
  49. data/lib/graphql/schema/introspection_system.rb +5 -16
  50. data/lib/graphql/schema/member/has_arguments.rb +14 -9
  51. data/lib/graphql/schema/member/has_fields.rb +8 -6
  52. data/lib/graphql/schema/member/has_unresolved_type_error.rb +5 -1
  53. data/lib/graphql/schema/resolver.rb +5 -5
  54. data/lib/graphql/schema/subset.rb +509 -0
  55. data/lib/graphql/schema/type_expression.rb +2 -2
  56. data/lib/graphql/schema/types_migration.rb +187 -0
  57. data/lib/graphql/schema/validator/all_validator.rb +62 -0
  58. data/lib/graphql/schema/validator.rb +2 -0
  59. data/lib/graphql/schema/warden.rb +89 -5
  60. data/lib/graphql/schema.rb +109 -53
  61. data/lib/graphql/static_validation/base_visitor.rb +6 -5
  62. data/lib/graphql/static_validation/literal_validator.rb +4 -4
  63. data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +1 -1
  64. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +1 -1
  65. data/lib/graphql/static_validation/rules/directives_are_defined.rb +1 -2
  66. data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +1 -1
  67. data/lib/graphql/static_validation/rules/fields_will_merge.rb +7 -7
  68. data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +3 -3
  69. data/lib/graphql/static_validation/rules/fragment_types_exist.rb +1 -1
  70. data/lib/graphql/static_validation/rules/fragments_are_on_composite_types.rb +1 -1
  71. data/lib/graphql/static_validation/rules/mutation_root_exists.rb +1 -1
  72. data/lib/graphql/static_validation/rules/query_root_exists.rb +1 -1
  73. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +3 -3
  74. data/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb +3 -3
  75. data/lib/graphql/static_validation/rules/subscription_root_exists.rb +1 -1
  76. data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +18 -27
  77. data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +1 -1
  78. data/lib/graphql/static_validation/rules/variables_are_input_types.rb +1 -1
  79. data/lib/graphql/static_validation/validation_context.rb +2 -2
  80. data/lib/graphql/subscriptions/broadcast_analyzer.rb +11 -5
  81. data/lib/graphql/subscriptions/event.rb +1 -1
  82. data/lib/graphql/subscriptions.rb +3 -3
  83. data/lib/graphql/testing/helpers.rb +8 -5
  84. data/lib/graphql/types/relay/connection_behaviors.rb +10 -0
  85. data/lib/graphql/types/relay/edge_behaviors.rb +10 -0
  86. data/lib/graphql/types/relay/page_info_behaviors.rb +4 -0
  87. data/lib/graphql/unauthorized_enum_value_error.rb +13 -0
  88. data/lib/graphql/version.rb +1 -1
  89. data/lib/graphql.rb +3 -0
  90. metadata +31 -13
  91. data/lib/graphql/analysis/ast/analyzer.rb +0 -91
  92. data/lib/graphql/analysis/ast/field_usage.rb +0 -84
  93. data/lib/graphql/analysis/ast/max_query_complexity.rb +0 -22
  94. data/lib/graphql/analysis/ast/max_query_depth.rb +0 -22
  95. data/lib/graphql/analysis/ast/query_complexity.rb +0 -185
  96. data/lib/graphql/analysis/ast/visitor.rb +0 -284
  97. data/lib/graphql/analysis/ast.rb +0 -94
  98. data/lib/graphql/language/token.rb +0 -34
  99. data/lib/graphql/schema/invalid_type_error.rb +0 -7
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+ require_relative "./base_cop"
3
+
4
+ module GraphQL
5
+ module Rubocop
6
+ module GraphQL
7
+ # Identify (and auto-correct) any root types in your schema file.
8
+ #
9
+ # @example
10
+ # # bad, immediately causes Rails to load `app/graphql/types/query.rb`
11
+ # query Types::Query
12
+ #
13
+ # # good, defers loading until the file is needed
14
+ # query { Types::Query }
15
+ #
16
+ class RootTypesInBlock < BaseCop
17
+ MSG = "type configuration can be moved to a block to defer loading the type's file"
18
+
19
+ def_node_matcher :root_type_config_without_block, <<-Pattern
20
+ (
21
+ send nil? {:query :mutation :subscription} const
22
+ )
23
+ Pattern
24
+
25
+ def on_send(node)
26
+ root_type_config_without_block(node) do
27
+ add_offense(node) do |corrector|
28
+ new_node_source = node.source_range.source
29
+ new_node_source.sub!(/(query|mutation|subscription)/, '\1 {')
30
+ new_node_source << " }"
31
+ corrector.replace(node, new_node_source)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -2,3 +2,5 @@
2
2
 
3
3
  require "graphql/rubocop/graphql/default_null_true"
4
4
  require "graphql/rubocop/graphql/default_required_true"
5
+ require "graphql/rubocop/graphql/field_type_in_block"
6
+ require "graphql/rubocop/graphql/root_types_in_block"
@@ -189,6 +189,7 @@ module GraphQL
189
189
  add_directives_from(type)
190
190
  if type.kind.fields?
191
191
  type.all_field_definitions.each do |field|
192
+ field.ensure_loaded
192
193
  name = field.graphql_name
193
194
  field_type = field.type.unwrap
194
195
  if !field_type.is_a?(GraphQL::Schema::LateBoundType)
@@ -4,6 +4,7 @@ module GraphQL
4
4
  class AlwaysVisible
5
5
  def self.use(schema, **opts)
6
6
  schema.warden_class = GraphQL::Schema::Warden::NullWarden
7
+ schema.subset_class = GraphQL::Schema::Warden::NullWarden::NullSubset
7
8
  end
8
9
  end
9
10
  end
@@ -312,10 +312,15 @@ module GraphQL
312
312
  context.query.after_lazy(custom_loaded_value) do |custom_value|
313
313
  if loads
314
314
  if type.list?
315
- loaded_values = custom_value.each_with_index.map { |custom_val, idx|
316
- id = coerced_value[idx]
317
- load_method_owner.authorize_application_object(self, id, context, custom_val)
318
- }
315
+ loaded_values = []
316
+ context.dataloader.run_isolated do
317
+ custom_value.each_with_index.map { |custom_val, idx|
318
+ id = coerced_value[idx]
319
+ context.dataloader.append_job do
320
+ loaded_values[idx] = load_method_owner.authorize_application_object(self, id, context, custom_val)
321
+ end
322
+ }
323
+ end
319
324
  context.schema.after_any_lazies(loaded_values, &:itself)
320
325
  else
321
326
  load_method_owner.authorize_application_object(self, coerced_value, context, custom_loaded_value)
@@ -326,7 +331,16 @@ module GraphQL
326
331
  end
327
332
  elsif loads
328
333
  if type.list?
329
- loaded_values = coerced_value.map { |val| load_method_owner.load_and_authorize_application_object(self, val, context) }
334
+ loaded_values = []
335
+ # We want to run these list items all together,
336
+ # but we also need to wait for the result so we can return it :S
337
+ context.dataloader.run_isolated do
338
+ coerced_value.each_with_index { |val, idx|
339
+ context.dataloader.append_job do
340
+ loaded_values[idx] = load_method_owner.load_and_authorize_application_object(self, val, context)
341
+ end
342
+ }
343
+ end
330
344
  context.schema.after_any_lazies(loaded_values, &:itself)
331
345
  else
332
346
  load_method_owner.load_and_authorize_application_object(self, coerced_value, context)
@@ -127,11 +127,12 @@ module GraphQL
127
127
  builder = self
128
128
 
129
129
  found_types = types.values
130
+ object_types = found_types.select { |t| t.respond_to?(:kind) && t.kind.object? }
130
131
  schema_class = Class.new(schema_superclass) do
131
132
  begin
132
133
  # Add these first so that there's some chance of resolving late-bound types
133
134
  add_type_and_traverse(found_types, root: false)
134
- orphan_types(found_types.select { |t| t.respond_to?(:kind) && t.kind.object? })
135
+ orphan_types(object_types)
135
136
  query query_root_type
136
137
  mutation mutation_root_type
137
138
  subscription subscription_root_type
@@ -141,6 +142,12 @@ module GraphQL
141
142
  raise InvalidDocumentError, "Type \"#{type_name}\" not found in document.", err_backtrace
142
143
  end
143
144
 
145
+ object_types.each do |t|
146
+ t.interfaces.each do |int_t|
147
+ int_t.orphan_types(t)
148
+ end
149
+ end
150
+
144
151
  if default_resolve.respond_to?(:resolve_type)
145
152
  def self.resolve_type(*args)
146
153
  self.definition_default_resolve.resolve_type(*args)
@@ -7,7 +7,7 @@ module GraphQL
7
7
  # In this case, the server hides types and fields _entirely_, unless the current context has certain `:flags` present.
8
8
  class Flagged < GraphQL::Schema::Directive
9
9
  def initialize(target, **options)
10
- if target.is_a?(Module) && !target.ancestors.include?(VisibleByFlag)
10
+ if target.is_a?(Module)
11
11
  # This is type class of some kind, `include` will put this module
12
12
  # in between the type class itself and its super class, so `super` will work fine
13
13
  target.include(VisibleByFlag)
@@ -188,6 +188,8 @@ module GraphQL
188
188
  assert_has_location(SCALAR)
189
189
  elsif @owner < GraphQL::Schema
190
190
  assert_has_location(SCHEMA)
191
+ elsif @owner < GraphQL::Schema::Resolver
192
+ assert_has_location(FIELD_DEFINITION)
191
193
  else
192
194
  raise "Unexpected directive owner class: #{@owner}"
193
195
  end
@@ -22,9 +22,21 @@ module GraphQL
22
22
  class Enum < GraphQL::Schema::Member
23
23
  extend GraphQL::Schema::Member::ValidatesInput
24
24
 
25
+ # This is raised when either:
26
+ #
27
+ # - A resolver returns a value which doesn't match any of the enum's configured values;
28
+ # - Or, the resolver returns a value which matches a value, but that value's `authorized?` check returns false.
29
+ #
30
+ # In either case, the field should be modified so that the invalid value isn't returned.
31
+ #
32
+ # {GraphQL::Schema::Enum} subclasses get their own subclass of this error, so that bug trackers can better show where they came from.
25
33
  class UnresolvedValueError < GraphQL::Error
26
- def initialize(value:, enum:, context:)
27
- fix_message = ", but this isn't a valid value for `#{enum.graphql_name}`. Update the field or resolver to return one of `#{enum.graphql_name}`'s values instead."
34
+ def initialize(value:, enum:, context:, authorized:)
35
+ fix_message = if authorized == false
36
+ ", but this value was unauthorized. Update the field or resolver to return a different value in this case (or return `nil`)."
37
+ else
38
+ ", but this isn't a valid value for `#{enum.graphql_name}`. Update the field or resolver to return one of `#{enum.graphql_name}`'s values instead."
39
+ end
28
40
  message = if (cp = context[:current_path]) && (cf = context[:current_field])
29
41
  "`#{cf.path}` returned `#{value.inspect}` at `#{cp.join(".")}`#{fix_message}"
30
42
  else
@@ -34,6 +46,8 @@ module GraphQL
34
46
  end
35
47
  end
36
48
 
49
+ # Raised when a {GraphQL::Schema::Enum} is defined to have no values.
50
+ # This can also happen when all values return false for `.visible?`.
37
51
  class MissingValuesError < GraphQL::Error
38
52
  def initialize(enum_type)
39
53
  @enum_type = enum_type
@@ -43,10 +57,10 @@ module GraphQL
43
57
 
44
58
  class << self
45
59
  # Define a value for this enum
46
- # @param graphql_name [String, Symbol] the GraphQL value for this, usually `SCREAMING_CASE`
47
- # @param description [String], the GraphQL description for this value, present in documentation
48
- # @param value [Object], the translated Ruby value for this object (defaults to `graphql_name`)
49
- # @param deprecation_reason [String] if this object is deprecated, include a message here
60
+ # @option kwargs [String, Symbol] :graphql_name the GraphQL value for this, usually `SCREAMING_CASE`
61
+ # @option kwargs [String] :description, the GraphQL description for this value, present in documentation
62
+ # @option kwargs [::Object] :value the translated Ruby value for this object (defaults to `graphql_name`)
63
+ # @option kwargs [String] :deprecation_reason if this object is deprecated, include a message here
50
64
  # @return [void]
51
65
  # @see {Schema::EnumValue} which handles these inputs by default
52
66
  def value(*args, **kwargs, &block)
@@ -130,7 +144,7 @@ module GraphQL
130
144
  end
131
145
 
132
146
  def validate_non_null_input(value_name, ctx, max_errors: nil)
133
- allowed_values = ctx.warden.enum_values(self)
147
+ allowed_values = ctx.types.enum_values(self)
134
148
  matching_value = allowed_values.find { |v| v.graphql_name == value_name }
135
149
 
136
150
  if matching_value.nil?
@@ -140,33 +154,50 @@ module GraphQL
140
154
  end
141
155
  end
142
156
 
157
+ # Called by the runtime when a field returns a value to give back to the client.
158
+ # This method checks that the incoming {value} matches one of the enum's defined values.
159
+ # @param value [Object] Any value matching the values for this enum.
160
+ # @param ctx [GraphQL::Query::Context]
161
+ # @raise [GraphQL::Schema::Enum::UnresolvedValueError] if {value} doesn't match a configured value or if the matching value isn't authorized.
162
+ # @return [String] The GraphQL-ready string for {value}
143
163
  def coerce_result(value, ctx)
144
- warden = ctx.warden
145
- all_values = warden ? warden.enum_values(self) : values.each_value
164
+ types = ctx.types
165
+ all_values = types ? types.enum_values(self) : values.each_value
146
166
  enum_value = all_values.find { |val| val.value == value }
147
- if enum_value
167
+ if enum_value && (was_authed = enum_value.authorized?(ctx))
148
168
  enum_value.graphql_name
149
169
  else
150
- raise self::UnresolvedValueError.new(enum: self, value: value, context: ctx)
170
+ raise self::UnresolvedValueError.new(enum: self, value: value, context: ctx, authorized: was_authed)
151
171
  end
152
172
  end
153
173
 
174
+ # Called by the runtime with incoming string representations from a query.
175
+ # It will match the string to a configured by name or by Ruby value.
176
+ # @param value_name [String, Object] A string from a GraphQL query, or a Ruby value matching a `value(..., value: ...)` configuration
177
+ # @param ctx [GraphQL::Query::Context]
178
+ # @raise [GraphQL::UnauthorizedEnumValueError] if an {EnumValue} matches but returns false for `.authorized?`. Goes to {Schema.unauthorized_object}.
179
+ # @return [Object] The Ruby value for the matched {GraphQL::Schema::EnumValue}
154
180
  def coerce_input(value_name, ctx)
155
- all_values = ctx.warden ? ctx.warden.enum_values(self) : values.each_value
156
-
157
- if v = all_values.find { |val| val.graphql_name == value_name }
158
- v.value
159
- elsif v = all_values.find { |val| val.value == value_name }
160
- # this is for matching default values, which are "inputs", but they're
161
- # the Ruby value, not the GraphQL string.
162
- v.value
181
+ all_values = ctx.types ? ctx.types.enum_values(self) : values.each_value
182
+
183
+ # This tries matching by incoming GraphQL string, then checks Ruby-defined values
184
+ if v = (all_values.find { |val| val.graphql_name == value_name } || all_values.find { |val| val.value == value_name })
185
+ if v.authorized?(ctx)
186
+ v.value
187
+ else
188
+ raise GraphQL::UnauthorizedEnumValueError.new(type: self, enum_value: v, context: ctx)
189
+ end
163
190
  else
164
191
  nil
165
192
  end
166
193
  end
167
194
 
168
195
  def inherited(child_class)
169
- child_class.const_set(:UnresolvedValueError, Class.new(Schema::Enum::UnresolvedValueError))
196
+ if child_class.name
197
+ # Don't assign a custom error class to anonymous classes
198
+ # because they would end up with names like `#<Class0x1234>::UnresolvedValueError` which messes up bug trackers
199
+ child_class.const_set(:UnresolvedValueError, Class.new(Schema::Enum::UnresolvedValueError))
200
+ end
170
201
  super
171
202
  end
172
203
 
@@ -50,7 +50,7 @@ module GraphQL
50
50
  if field.has_default_page_size? && !value.has_default_page_size_override?
51
51
  value.default_page_size = field.default_page_size
52
52
  end
53
- if context.schema.new_connections? && (custom_t = context.schema.connections.edge_class_for_field(@field))
53
+ if (custom_t = context.schema.connections.edge_class_for_field(@field))
54
54
  value.edge_class = custom_t
55
55
  end
56
56
  value
@@ -41,6 +41,18 @@ module GraphQL
41
41
  end
42
42
  end
43
43
 
44
+ def directives
45
+ if @resolver_class && (r_dirs = @resolver_class.directives).any?
46
+ if (own_dirs = super).any?
47
+ own_dirs + r_dirs
48
+ else
49
+ r_dirs
50
+ end
51
+ else
52
+ super
53
+ end
54
+ end
55
+
44
56
  # @return [Class] The thing this field was defined on (type, mutation, resolver)
45
57
  attr_accessor :owner
46
58
 
@@ -134,11 +146,16 @@ module GraphQL
134
146
  Member::BuildType.to_type_name(@return_type_expr)
135
147
  elsif @resolver_class && @resolver_class.type
136
148
  Member::BuildType.to_type_name(@resolver_class.type)
137
- else
149
+ elsif type
138
150
  # As a last ditch, try to force loading the return type:
139
151
  type.unwrap.name
140
152
  end
141
- @connection = return_type_name.end_with?("Connection") && return_type_name != "Connection"
153
+ if return_type_name
154
+ @connection = return_type_name.end_with?("Connection") && return_type_name != "Connection"
155
+ else
156
+ # TODO set this when type is set by method
157
+ false # not loaded yet?
158
+ end
142
159
  else
143
160
  @connection
144
161
  end
@@ -224,8 +241,8 @@ module GraphQL
224
241
  raise ArgumentError, "missing first `name` argument or keyword `name:`"
225
242
  end
226
243
  if !(resolver_class)
227
- if type.nil?
228
- raise ArgumentError, "missing second `type` argument or keyword `type:`"
244
+ if type.nil? && !block_given?
245
+ raise ArgumentError, "missing second `type` argument, keyword `type:`, or a block containing `type(...)`"
229
246
  end
230
247
  end
231
248
  @original_name = name
@@ -290,6 +307,7 @@ module GraphQL
290
307
  @ast_node = ast_node
291
308
  @method_conflict_warning = method_conflict_warning
292
309
  @fallback_value = fallback_value
310
+ @definition_block = nil
293
311
 
294
312
  arguments.each do |name, arg|
295
313
  case arg
@@ -308,26 +326,14 @@ module GraphQL
308
326
  @subscription_scope = subscription_scope
309
327
 
310
328
  @extensions = EMPTY_ARRAY
311
- @call_after_define = false
312
- # This should run before connection extension,
313
- # but should it run after the definition block?
314
- if scoped?
315
- self.extension(ScopeExtension)
316
- end
317
-
318
- # The problem with putting this after the definition_block
319
- # is that it would override arguments
320
- if connection? && connection_extension
321
- self.extension(connection_extension)
322
- end
323
-
329
+ set_pagination_extensions(connection_extension: connection_extension)
324
330
  # Do this last so we have as much context as possible when initializing them:
325
331
  if extensions.any?
326
- self.extensions(extensions)
332
+ self.extensions(extensions, call_after_define: false)
327
333
  end
328
334
 
329
335
  if resolver_class && resolver_class.extensions.any?
330
- self.extensions(resolver_class.extensions)
336
+ self.extensions(resolver_class.extensions, call_after_define: false)
331
337
  end
332
338
 
333
339
  if directives.any?
@@ -341,15 +347,28 @@ module GraphQL
341
347
  end
342
348
 
343
349
  if block_given?
344
- if definition_block.arity == 1
345
- yield self
350
+ @definition_block = definition_block
351
+ else
352
+ self.extensions.each(&:after_define_apply)
353
+ end
354
+ end
355
+
356
+ # Calls the definition block, if one was given.
357
+ # This is deferred so that references to the return type
358
+ # can be lazily evaluated, reducing Rails boot time.
359
+ # @return [self]
360
+ # @api private
361
+ def ensure_loaded
362
+ if @definition_block
363
+ if @definition_block.arity == 1
364
+ @definition_block.call(self)
346
365
  else
347
- instance_eval(&definition_block)
366
+ instance_eval(&@definition_block)
348
367
  end
368
+ self.extensions.each(&:after_define_apply)
369
+ @definition_block = nil
349
370
  end
350
-
351
- self.extensions.each(&:after_define_apply)
352
- @call_after_define = true
371
+ self
353
372
  end
354
373
 
355
374
  attr_accessor :dynamic_introspection
@@ -396,14 +415,14 @@ module GraphQL
396
415
  #
397
416
  # @param extensions [Array<Class, Hash<Class => Hash>>] Add extensions to this field. For hash elements, only the first key/value is used.
398
417
  # @return [Array<GraphQL::Schema::FieldExtension>] extensions to apply to this field
399
- def extensions(new_extensions = nil)
418
+ def extensions(new_extensions = nil, call_after_define: !@definition_block)
400
419
  if new_extensions
401
420
  new_extensions.each do |extension_config|
402
421
  if extension_config.is_a?(Hash)
403
422
  extension_class, options = *extension_config.to_a[0]
404
- self.extension(extension_class, options)
423
+ self.extension(extension_class, call_after_define: call_after_define, **options)
405
424
  else
406
- self.extension(extension_config)
425
+ self.extension(extension_config, call_after_define: call_after_define)
407
426
  end
408
427
  end
409
428
  end
@@ -421,12 +440,12 @@ module GraphQL
421
440
  # @param extension_class [Class] subclass of {Schema::FieldExtension}
422
441
  # @param options [Hash] if provided, given as `options:` when initializing `extension`.
423
442
  # @return [void]
424
- def extension(extension_class, options = nil)
443
+ def extension(extension_class, call_after_define: !@definition_block, **options)
425
444
  extension_inst = extension_class.new(field: self, options: options)
426
445
  if @extensions.frozen?
427
446
  @extensions = @extensions.dup
428
447
  end
429
- if @call_after_define
448
+ if call_after_define
430
449
  extension_inst.after_define_apply
431
450
  end
432
451
  @extensions << extension_inst
@@ -565,16 +584,29 @@ module GraphQL
565
584
  class MissingReturnTypeError < GraphQL::Error; end
566
585
  attr_writer :type
567
586
 
568
- def type
569
- if @resolver_class
570
- return_type = @return_type_expr || @resolver_class.type_expr
571
- if return_type.nil?
572
- raise MissingReturnTypeError, "Can't determine the return type for #{self.path} (it has `resolver: #{@resolver_class}`, perhaps that class is missing a `type ...` declaration, or perhaps its type causes a cyclical loading issue)"
587
+ # Get or set the return type of this field.
588
+ #
589
+ # It may return nil if no type was configured or if the given definition block wasn't called yet.
590
+ # @param new_type [Module, GraphQL::Schema::NonNull, GraphQL::Schema::List] A GraphQL return type
591
+ # @return [Module, GraphQL::Schema::NonNull, GraphQL::Schema::List, nil] the configured type for this field
592
+ def type(new_type = NOT_CONFIGURED)
593
+ if NOT_CONFIGURED.equal?(new_type)
594
+ if @resolver_class
595
+ return_type = @return_type_expr || @resolver_class.type_expr
596
+ if return_type.nil?
597
+ raise MissingReturnTypeError, "Can't determine the return type for #{self.path} (it has `resolver: #{@resolver_class}`, perhaps that class is missing a `type ...` declaration, or perhaps its type causes a cyclical loading issue)"
598
+ end
599
+ nullable = @return_type_null.nil? ? @resolver_class.null : @return_type_null
600
+ Member::BuildType.parse_type(return_type, null: nullable)
601
+ elsif !@return_type_expr.nil?
602
+ @type ||= Member::BuildType.parse_type(@return_type_expr, null: @return_type_null)
573
603
  end
574
- nullable = @return_type_null.nil? ? @resolver_class.null : @return_type_null
575
- Member::BuildType.parse_type(return_type, null: nullable)
576
604
  else
577
- @type ||= Member::BuildType.parse_type(@return_type_expr, null: @return_type_null)
605
+ @return_type_expr = new_type
606
+ # If `type` is set in the definition block, then the `connection_extension: ...` given as a keyword won't be used, hmm...
607
+ # Also, arguments added by `connection_extension` will clobber anything previously defined,
608
+ # so `type(...)` should go first.
609
+ set_pagination_extensions(connection_extension: self.class.connection_extension)
578
610
  end
579
611
  rescue GraphQL::Schema::InvalidDocumentError, MissingReturnTypeError => err
580
612
  # Let this propagate up
@@ -606,7 +638,7 @@ module GraphQL
606
638
  using_arg_values = false
607
639
  end
608
640
 
609
- args = context.warden.arguments(self)
641
+ args = context.types.arguments(self)
610
642
  args.each do |arg|
611
643
  arg_key = arg.keyword
612
644
  if arg_values.key?(arg_key)
@@ -885,6 +917,20 @@ ERR
885
917
  raise ArgumentError, "Invalid complexity for #{self.path}: #{own_complexity.inspect}"
886
918
  end
887
919
  end
920
+
921
+ def set_pagination_extensions(connection_extension:)
922
+ # This should run before connection extension,
923
+ # but should it run after the definition block?
924
+ if scoped?
925
+ self.extension(ScopeExtension, call_after_define: false)
926
+ end
927
+
928
+ # The problem with putting this after the definition_block
929
+ # is that it would override arguments
930
+ if connection? && connection_extension
931
+ self.extension(connection_extension, call_after_define: false)
932
+ end
933
+ end
888
934
  end
889
935
  end
890
936
  end
@@ -149,7 +149,8 @@ module GraphQL
149
149
 
150
150
  def authorize_arguments(args, values)
151
151
  # remove the `input` wrapper to match values
152
- input_args = args["input"].type.unwrap.arguments(context)
152
+ input_type = args.find { |a| a.graphql_name == "input" }.type.unwrap
153
+ input_args = context.types.arguments(input_type)
153
154
  super(input_args, values)
154
155
  end
155
156
  end
@@ -23,7 +23,8 @@ module GraphQL
23
23
  @ruby_style_hash = ruby_kwargs
24
24
  @arguments = arguments
25
25
  # Apply prepares, not great to have it duplicated here.
26
- self.class.arguments(context).each_value do |arg_defn|
26
+ arg_defns = context ? context.types.arguments(self.class) : self.class.arguments(context).each_value
27
+ arg_defns.each do |arg_defn|
27
28
  ruby_kwargs_key = arg_defn.keyword
28
29
  if @ruby_style_hash.key?(ruby_kwargs_key)
29
30
  # Weirdly, procs are applied during coercion, but not methods.
@@ -58,7 +59,7 @@ module GraphQL
58
59
  def self.authorized?(obj, value, ctx)
59
60
  # Authorize each argument (but this doesn't apply if `prepare` is implemented):
60
61
  if value.respond_to?(:key?)
61
- arguments(ctx).each do |_name, input_obj_arg|
62
+ ctx.types.arguments(self).each do |input_obj_arg|
62
63
  if value.key?(input_obj_arg.keyword) &&
63
64
  !input_obj_arg.authorized?(obj, value[input_obj_arg.keyword], ctx)
64
65
  return false
@@ -149,7 +150,7 @@ module GraphQL
149
150
  INVALID_OBJECT_MESSAGE = "Expected %{object} to be a key-value object."
150
151
 
151
152
  def validate_non_null_input(input, ctx, max_errors: nil)
152
- warden = ctx.warden
153
+ types = ctx.types
153
154
 
154
155
  if input.is_a?(Array)
155
156
  return GraphQL::Query::InputValidationResult.from_problem(INVALID_OBJECT_MESSAGE % { object: JSON.generate(input, quirks_mode: true) })
@@ -161,9 +162,9 @@ module GraphQL
161
162
  end
162
163
 
163
164
  # Inject missing required arguments
164
- missing_required_inputs = self.arguments(ctx).reduce({}) do |m, (argument_name, argument)|
165
- if !input.key?(argument_name) && argument.type.non_null? && warden.get_argument(self, argument_name)
166
- m[argument_name] = nil
165
+ missing_required_inputs = ctx.types.arguments(self).reduce({}) do |m, (argument)|
166
+ if !input.key?(argument.graphql_name) && argument.type.non_null? && types.argument(self, argument.graphql_name)
167
+ m[argument.graphql_name] = nil
167
168
  end
168
169
 
169
170
  m
@@ -172,7 +173,7 @@ module GraphQL
172
173
  result = nil
173
174
  [input, missing_required_inputs].each do |args_to_validate|
174
175
  args_to_validate.each do |argument_name, value|
175
- argument = warden.get_argument(self, argument_name)
176
+ argument = types.argument(self, argument_name)
176
177
  # Items in the input that are unexpected
177
178
  if argument.nil?
178
179
  result ||= Query::InputValidationResult.new
@@ -82,13 +82,29 @@ module GraphQL
82
82
  super
83
83
  end
84
84
 
85
+ # Register other Interface or Object types as implementers of this Interface.
86
+ #
87
+ # When those Interfaces or Objects aren't used as the return values of fields,
88
+ # they may have to be registered using this method so that GraphQL-Ruby can find them.
89
+ # @param types [Class, Module]
90
+ # @return [Array<Module, Class>] Implementers of this interface, if they're registered
85
91
  def orphan_types(*types)
86
92
  if types.any?
87
- @orphan_types = types
93
+ @orphan_types ||= []
94
+ @orphan_types.concat(types)
88
95
  else
89
- all_orphan_types = @orphan_types || []
90
- all_orphan_types += super if defined?(super)
91
- all_orphan_types.uniq
96
+ if defined?(@orphan_types)
97
+ all_orphan_types = @orphan_types.dup
98
+ if defined?(super)
99
+ all_orphan_types += super
100
+ all_orphan_types.uniq!
101
+ end
102
+ all_orphan_types
103
+ elsif defined?(super)
104
+ super
105
+ else
106
+ EmptyObjects::EMPTY_ARRAY
107
+ end
92
108
  end
93
109
  end
94
110
 
@@ -25,7 +25,7 @@ module GraphQL
25
25
  load_constant(:DirectiveLocationEnum)
26
26
  ]
27
27
  @types = {}
28
- @possible_types = {}.tap(&:compare_by_identity)
28
+ @possible_types = {}.compare_by_identity
29
29
  type_defns.each do |t|
30
30
  @types[t.graphql_name] = t
31
31
  @possible_types[t] = [t]
@@ -69,7 +69,7 @@ module GraphQL
69
69
  def resolve_late_bindings
70
70
  @types.each do |name, t|
71
71
  if t.kind.fields?
72
- t.fields.each do |_name, field_defn|
72
+ t.all_field_definitions.each do |field_defn|
73
73
  field_defn.type = resolve_late_binding(field_defn.type)
74
74
  end
75
75
  end
@@ -90,7 +90,8 @@ module GraphQL
90
90
  def resolve_late_binding(late_bound_type)
91
91
  case late_bound_type
92
92
  when GraphQL::Schema::LateBoundType
93
- @schema.get_type(late_bound_type.name)
93
+ type_name = late_bound_type.name
94
+ @types[type_name] || @schema.get_type(type_name)
94
95
  when GraphQL::Schema::List
95
96
  resolve_late_binding(late_bound_type.of_type).to_list_type
96
97
  when GraphQL::Schema::NonNull
@@ -113,19 +114,7 @@ module GraphQL
113
114
 
114
115
  def get_fields_from_class(class_sym:)
115
116
  object_type_defn = load_constant(class_sym)
116
-
117
- if object_type_defn.is_a?(Module)
118
- object_type_defn.fields
119
- else
120
- extracted_field_defns = {}
121
- object_class = object_type_defn.metadata[:type_class]
122
- object_type_defn.all_fields.each do |field_defn|
123
- inner_resolve = field_defn.resolve_proc
124
- resolve_with_instantiate = PerFieldProxyResolve.new(object_class: object_class, inner_resolve: inner_resolve)
125
- extracted_field_defns[field_defn.name] = field_defn.redefine(resolve: resolve_with_instantiate)
126
- end
127
- extracted_field_defns
128
- end
117
+ object_type_defn.fields
129
118
  end
130
119
 
131
120
  # This is probably not 100% robust -- but it has to be good enough to avoid modifying the built-in introspection types