graphql 2.3.5 → 2.3.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of graphql might be problematic. Click here for more details.

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