graphql 2.3.8 → 2.4.0

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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/install_generator.rb +46 -0
  3. data/lib/generators/graphql/orm_mutations_base.rb +1 -1
  4. data/lib/generators/graphql/templates/base_resolver.erb +2 -0
  5. data/lib/generators/graphql/type_generator.rb +1 -1
  6. data/lib/graphql/analysis.rb +1 -1
  7. data/lib/graphql/current.rb +52 -0
  8. data/lib/graphql/dataloader/async_dataloader.rb +3 -2
  9. data/lib/graphql/dataloader/source.rb +6 -3
  10. data/lib/graphql/dataloader.rb +35 -11
  11. data/lib/graphql/execution/interpreter/arguments_cache.rb +5 -10
  12. data/lib/graphql/execution/interpreter/resolve.rb +10 -6
  13. data/lib/graphql/execution/interpreter/runtime.rb +23 -19
  14. data/lib/graphql/execution/interpreter.rb +2 -0
  15. data/lib/graphql/introspection/entry_points.rb +2 -2
  16. data/lib/graphql/introspection/schema_type.rb +4 -19
  17. data/lib/graphql/invalid_null_error.rb +1 -1
  18. data/lib/graphql/language/comment.rb +18 -0
  19. data/lib/graphql/language/document_from_schema_definition.rb +38 -4
  20. data/lib/graphql/language/lexer.rb +15 -12
  21. data/lib/graphql/language/nodes.rb +24 -16
  22. data/lib/graphql/language/parser.rb +14 -1
  23. data/lib/graphql/language/printer.rb +23 -7
  24. data/lib/graphql/language.rb +6 -5
  25. data/lib/graphql/query/context.rb +4 -2
  26. data/lib/graphql/query/null_context.rb +1 -5
  27. data/lib/graphql/query.rb +49 -20
  28. data/lib/graphql/rubocop/graphql/field_type_in_block.rb +144 -0
  29. data/lib/graphql/rubocop/graphql/root_types_in_block.rb +38 -0
  30. data/lib/graphql/rubocop.rb +2 -0
  31. data/lib/graphql/schema/addition.rb +1 -0
  32. data/lib/graphql/schema/always_visible.rb +6 -3
  33. data/lib/graphql/schema/argument.rb +14 -1
  34. data/lib/graphql/schema/build_from_definition.rb +9 -1
  35. data/lib/graphql/schema/directive/flagged.rb +1 -1
  36. data/lib/graphql/schema/enum.rb +49 -15
  37. data/lib/graphql/schema/enum_value.rb +9 -1
  38. data/lib/graphql/schema/field/connection_extension.rb +1 -1
  39. data/lib/graphql/schema/field.rb +91 -36
  40. data/lib/graphql/schema/input_object.rb +20 -7
  41. data/lib/graphql/schema/interface.rb +21 -4
  42. data/lib/graphql/schema/introspection_system.rb +3 -2
  43. data/lib/graphql/schema/member/base_dsl_methods.rb +15 -0
  44. data/lib/graphql/schema/member/has_arguments.rb +9 -5
  45. data/lib/graphql/schema/member/has_fields.rb +5 -5
  46. data/lib/graphql/schema/member/has_unresolved_type_error.rb +5 -1
  47. data/lib/graphql/schema/printer.rb +1 -0
  48. data/lib/graphql/schema/resolver.rb +3 -4
  49. data/lib/graphql/schema/validator/all_validator.rb +2 -0
  50. data/lib/graphql/schema/validator/required_validator.rb +28 -4
  51. data/lib/graphql/schema/visibility/migration.rb +186 -0
  52. data/lib/graphql/schema/visibility/profile.rb +523 -0
  53. data/lib/graphql/schema/visibility.rb +75 -0
  54. data/lib/graphql/schema/warden.rb +80 -21
  55. data/lib/graphql/schema.rb +254 -85
  56. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +2 -1
  57. data/lib/graphql/static_validation/rules/directives_are_defined.rb +2 -1
  58. data/lib/graphql/static_validation/rules/directives_are_in_valid_locations.rb +2 -0
  59. data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +2 -1
  60. data/lib/graphql/static_validation/rules/fields_will_merge.rb +1 -0
  61. data/lib/graphql/static_validation/rules/fragment_types_exist.rb +11 -1
  62. data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +18 -27
  63. data/lib/graphql/static_validation/rules/variables_are_input_types.rb +10 -1
  64. data/lib/graphql/static_validation/validation_context.rb +15 -0
  65. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +2 -1
  66. data/lib/graphql/subscriptions.rb +3 -1
  67. data/lib/graphql/testing/helpers.rb +7 -3
  68. data/lib/graphql/tracing/notifications_trace.rb +2 -2
  69. data/lib/graphql/unauthorized_enum_value_error.rb +13 -0
  70. data/lib/graphql/version.rb +1 -1
  71. data/lib/graphql.rb +3 -0
  72. metadata +28 -7
  73. data/lib/graphql/schema/subset.rb +0 -397
@@ -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)
@@ -1,10 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphQL
3
3
  class Schema
4
- class AlwaysVisible
4
+ module AlwaysVisible
5
5
  def self.use(schema, **opts)
6
- schema.warden_class = GraphQL::Schema::Warden::NullWarden
7
- schema.subset_class = GraphQL::Schema::Warden::NullWarden::NullSubset
6
+ schema.extend(self)
7
+ end
8
+
9
+ def visible?(_member, _context)
10
+ true
8
11
  end
9
12
  end
10
13
  end
@@ -50,11 +50,12 @@ module GraphQL
50
50
  # @param deprecation_reason [String]
51
51
  # @param validates [Hash, nil] Options for building validators, if any should be applied
52
52
  # @param replace_null_with_default [Boolean] if `true`, incoming values of `null` will be replaced with the configured `default_value`
53
- def initialize(arg_name = nil, type_expr = nil, desc = nil, required: true, type: nil, name: nil, loads: nil, description: nil, ast_node: nil, default_value: NOT_CONFIGURED, as: nil, from_resolver: false, camelize: true, prepare: nil, owner:, validates: nil, directives: nil, deprecation_reason: nil, replace_null_with_default: false, &definition_block)
53
+ def initialize(arg_name = nil, type_expr = nil, desc = nil, required: true, type: nil, name: nil, loads: nil, description: nil, comment: nil, ast_node: nil, default_value: NOT_CONFIGURED, as: nil, from_resolver: false, camelize: true, prepare: nil, owner:, validates: nil, directives: nil, deprecation_reason: nil, replace_null_with_default: false, &definition_block)
54
54
  arg_name ||= name
55
55
  @name = -(camelize ? Member::BuildType.camelize(arg_name.to_s) : arg_name.to_s)
56
56
  @type_expr = type_expr || type
57
57
  @description = desc || description
58
+ @comment = comment
58
59
  @null = required != true
59
60
  @default_value = default_value
60
61
  if replace_null_with_default
@@ -129,6 +130,17 @@ module GraphQL
129
130
  end
130
131
  end
131
132
 
133
+ attr_writer :comment
134
+
135
+ # @return [String] Comment for this argument
136
+ def comment(text = nil)
137
+ if text
138
+ @comment = text
139
+ else
140
+ @comment
141
+ end
142
+ end
143
+
132
144
  # @return [String] Deprecation reason for this argument
133
145
  def deprecation_reason(text = nil)
134
146
  if text
@@ -352,6 +364,7 @@ module GraphQL
352
364
 
353
365
  # @api private
354
366
  def validate_default_value
367
+ return unless default_value?
355
368
  coerced_default_value = begin
356
369
  # This is weird, but we should accept single-item default values for list-type arguments.
357
370
  # If we used `coerce_isolated_input` below, it would do this for us, but it's not really
@@ -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)
@@ -181,6 +188,7 @@ module GraphQL
181
188
 
182
189
  def self.inherited(child_class)
183
190
  child_class.definition_default_resolve = self.definition_default_resolve
191
+ super
184
192
  end
185
193
  end
186
194
 
@@ -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)
@@ -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,11 @@ 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 [String] :comment, the GraphQL comment for this value, present in documentation
63
+ # @option kwargs [::Object] :value the translated Ruby value for this object (defaults to `graphql_name`)
64
+ # @option kwargs [String] :deprecation_reason if this object is deprecated, include a message here
50
65
  # @return [void]
51
66
  # @see {Schema::EnumValue} which handles these inputs by default
52
67
  def value(*args, **kwargs, &block)
@@ -138,35 +153,54 @@ module GraphQL
138
153
  else
139
154
  nil
140
155
  end
156
+ # rescue MissingValuesError
157
+ # nil
141
158
  end
142
159
 
160
+ # Called by the runtime when a field returns a value to give back to the client.
161
+ # This method checks that the incoming {value} matches one of the enum's defined values.
162
+ # @param value [Object] Any value matching the values for this enum.
163
+ # @param ctx [GraphQL::Query::Context]
164
+ # @raise [GraphQL::Schema::Enum::UnresolvedValueError] if {value} doesn't match a configured value or if the matching value isn't authorized.
165
+ # @return [String] The GraphQL-ready string for {value}
143
166
  def coerce_result(value, ctx)
144
167
  types = ctx.types
145
168
  all_values = types ? types.enum_values(self) : values.each_value
146
169
  enum_value = all_values.find { |val| val.value == value }
147
- if enum_value
170
+ if enum_value && (was_authed = enum_value.authorized?(ctx))
148
171
  enum_value.graphql_name
149
172
  else
150
- raise self::UnresolvedValueError.new(enum: self, value: value, context: ctx)
173
+ raise self::UnresolvedValueError.new(enum: self, value: value, context: ctx, authorized: was_authed)
151
174
  end
152
175
  end
153
176
 
177
+ # Called by the runtime with incoming string representations from a query.
178
+ # It will match the string to a configured by name or by Ruby value.
179
+ # @param value_name [String, Object] A string from a GraphQL query, or a Ruby value matching a `value(..., value: ...)` configuration
180
+ # @param ctx [GraphQL::Query::Context]
181
+ # @raise [GraphQL::UnauthorizedEnumValueError] if an {EnumValue} matches but returns false for `.authorized?`. Goes to {Schema.unauthorized_object}.
182
+ # @return [Object] The Ruby value for the matched {GraphQL::Schema::EnumValue}
154
183
  def coerce_input(value_name, ctx)
155
184
  all_values = ctx.types ? ctx.types.enum_values(self) : values.each_value
156
185
 
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
186
+ # This tries matching by incoming GraphQL string, then checks Ruby-defined values
187
+ if v = (all_values.find { |val| val.graphql_name == value_name } || all_values.find { |val| val.value == value_name })
188
+ if v.authorized?(ctx)
189
+ v.value
190
+ else
191
+ raise GraphQL::UnauthorizedEnumValueError.new(type: self, enum_value: v, context: ctx)
192
+ end
163
193
  else
164
194
  nil
165
195
  end
166
196
  end
167
197
 
168
198
  def inherited(child_class)
169
- child_class.const_set(:UnresolvedValueError, Class.new(Schema::Enum::UnresolvedValueError))
199
+ if child_class.name
200
+ # Don't assign a custom error class to anonymous classes
201
+ # because they would end up with names like `#<Class0x1234>::UnresolvedValueError` which messes up bug trackers
202
+ child_class.const_set(:UnresolvedValueError, Class.new(Schema::Enum::UnresolvedValueError))
203
+ end
170
204
  super
171
205
  end
172
206
 
@@ -30,10 +30,11 @@ module GraphQL
30
30
  # @return [Class] The enum type that owns this value
31
31
  attr_reader :owner
32
32
 
33
- def initialize(graphql_name, desc = nil, owner:, ast_node: nil, directives: nil, description: nil, value: NOT_CONFIGURED, deprecation_reason: nil, &block)
33
+ def initialize(graphql_name, desc = nil, owner:, ast_node: nil, directives: nil, description: nil, comment: nil, value: NOT_CONFIGURED, deprecation_reason: nil, &block)
34
34
  @graphql_name = graphql_name.to_s
35
35
  GraphQL::NameValidator.validate!(@graphql_name)
36
36
  @description = desc || description
37
+ @comment = comment
37
38
  @value = value == NOT_CONFIGURED ? @graphql_name : value
38
39
  if deprecation_reason
39
40
  self.deprecation_reason = deprecation_reason
@@ -58,6 +59,13 @@ module GraphQL
58
59
  @description
59
60
  end
60
61
 
62
+ def comment(new_comment = nil)
63
+ if new_comment
64
+ @comment = new_comment
65
+ end
66
+ @comment
67
+ end
68
+
61
69
  def value(new_val = nil)
62
70
  unless new_val.nil?
63
71
  @value = new_val
@@ -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
@@ -106,7 +106,7 @@ module GraphQL
106
106
  # @param subscription [Class] A {GraphQL::Schema::Subscription} class to use for field configuration
107
107
  # @return [GraphQL::Schema:Field] an instance of `self`
108
108
  # @see {.initialize} for other options
109
- def self.from_options(name = nil, type = nil, desc = nil, resolver: nil, mutation: nil, subscription: nil,**kwargs, &block)
109
+ def self.from_options(name = nil, type = nil, desc = nil, comment: nil, resolver: nil, mutation: nil, subscription: nil,**kwargs, &block)
110
110
  if (resolver_class = resolver || mutation || subscription)
111
111
  # Add a reference to that parent class
112
112
  kwargs[:resolver_class] = resolver_class
@@ -116,6 +116,10 @@ module GraphQL
116
116
  kwargs[:name] = name
117
117
  end
118
118
 
119
+ if comment
120
+ kwargs[:comment] = comment
121
+ end
122
+
119
123
  if !type.nil?
120
124
  if desc
121
125
  if kwargs[:description]
@@ -146,11 +150,16 @@ module GraphQL
146
150
  Member::BuildType.to_type_name(@return_type_expr)
147
151
  elsif @resolver_class && @resolver_class.type
148
152
  Member::BuildType.to_type_name(@resolver_class.type)
149
- else
153
+ elsif type
150
154
  # As a last ditch, try to force loading the return type:
151
155
  type.unwrap.name
152
156
  end
153
- @connection = return_type_name.end_with?("Connection") && return_type_name != "Connection"
157
+ if return_type_name
158
+ @connection = return_type_name.end_with?("Connection") && return_type_name != "Connection"
159
+ else
160
+ # TODO set this when type is set by method
161
+ false # not loaded yet?
162
+ end
154
163
  else
155
164
  @connection
156
165
  end
@@ -207,6 +216,7 @@ module GraphQL
207
216
  # @param owner [Class] The type that this field belongs to
208
217
  # @param null [Boolean] (defaults to `true`) `true` if this field may return `null`, `false` if it is never `null`
209
218
  # @param description [String] Field description
219
+ # @param comment [String] Field comment
210
220
  # @param deprecation_reason [String] If present, the field is marked "deprecated" with this message
211
221
  # @param method [Symbol] The method to call on the underlying object to resolve this field (defaults to `name`)
212
222
  # @param hash_key [String, Symbol] The hash key to lookup on the underlying object (if its a Hash) to resolve this field (defaults to `name` or `name.to_s`)
@@ -231,13 +241,13 @@ module GraphQL
231
241
  # @param method_conflict_warning [Boolean] If false, skip the warning if this field's method conflicts with a built-in method
232
242
  # @param validates [Array<Hash>] Configurations for validating this field
233
243
  # @param fallback_value [Object] A fallback value if the method is not defined
234
- def initialize(type: nil, name: nil, owner: nil, null: nil, description: NOT_CONFIGURED, deprecation_reason: nil, method: nil, hash_key: nil, dig: nil, resolver_method: nil, connection: nil, max_page_size: NOT_CONFIGURED, default_page_size: NOT_CONFIGURED, 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: NOT_CONFIGURED, arguments: EMPTY_HASH, directives: EMPTY_HASH, validates: EMPTY_ARRAY, fallback_value: NOT_CONFIGURED, dynamic_introspection: false, &definition_block)
244
+ def initialize(type: nil, name: nil, owner: nil, null: nil, description: NOT_CONFIGURED, comment: NOT_CONFIGURED, deprecation_reason: nil, method: nil, hash_key: nil, dig: nil, resolver_method: nil, connection: nil, max_page_size: NOT_CONFIGURED, default_page_size: NOT_CONFIGURED, 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: NOT_CONFIGURED, arguments: EMPTY_HASH, directives: EMPTY_HASH, validates: EMPTY_ARRAY, fallback_value: NOT_CONFIGURED, dynamic_introspection: false, &definition_block)
235
245
  if name.nil?
236
246
  raise ArgumentError, "missing first `name` argument or keyword `name:`"
237
247
  end
238
248
  if !(resolver_class)
239
- if type.nil?
240
- raise ArgumentError, "missing second `type` argument or keyword `type:`"
249
+ if type.nil? && !block_given?
250
+ raise ArgumentError, "missing second `type` argument, keyword `type:`, or a block containing `type(...)`"
241
251
  end
242
252
  end
243
253
  @original_name = name
@@ -247,6 +257,7 @@ module GraphQL
247
257
  @name = -(camelize ? Member::BuildType.camelize(name_s) : name_s)
248
258
 
249
259
  @description = description
260
+ @comment = comment
250
261
  @type = @owner_type = @own_validators = @own_directives = @own_arguments = @arguments_statically_coercible = nil # these will be prepared later if necessary
251
262
 
252
263
  self.deprecation_reason = deprecation_reason
@@ -302,6 +313,7 @@ module GraphQL
302
313
  @ast_node = ast_node
303
314
  @method_conflict_warning = method_conflict_warning
304
315
  @fallback_value = fallback_value
316
+ @definition_block = definition_block
305
317
 
306
318
  arguments.each do |name, arg|
307
319
  case arg
@@ -321,18 +333,7 @@ module GraphQL
321
333
 
322
334
  @extensions = EMPTY_ARRAY
323
335
  @call_after_define = false
324
- # This should run before connection extension,
325
- # but should it run after the definition block?
326
- if scoped?
327
- self.extension(ScopeExtension)
328
- end
329
-
330
- # The problem with putting this after the definition_block
331
- # is that it would override arguments
332
- if connection? && connection_extension
333
- self.extension(connection_extension)
334
- end
335
-
336
+ set_pagination_extensions(connection_extension: connection_extension)
336
337
  # Do this last so we have as much context as possible when initializing them:
337
338
  if extensions.any?
338
339
  self.extensions(extensions)
@@ -352,16 +353,29 @@ module GraphQL
352
353
  self.validates(validates)
353
354
  end
354
355
 
355
- if block_given?
356
- if definition_block.arity == 1
357
- yield self
356
+ if @definition_block.nil?
357
+ self.extensions.each(&:after_define_apply)
358
+ @call_after_define = true
359
+ end
360
+ end
361
+
362
+ # Calls the definition block, if one was given.
363
+ # This is deferred so that references to the return type
364
+ # can be lazily evaluated, reducing Rails boot time.
365
+ # @return [self]
366
+ # @api private
367
+ def ensure_loaded
368
+ if @definition_block
369
+ if @definition_block.arity == 1
370
+ @definition_block.call(self)
358
371
  else
359
- instance_eval(&definition_block)
372
+ instance_eval(&@definition_block)
360
373
  end
374
+ self.extensions.each(&:after_define_apply)
375
+ @call_after_define = true
376
+ @definition_block = nil
361
377
  end
362
-
363
- self.extensions.each(&:after_define_apply)
364
- @call_after_define = true
378
+ self
365
379
  end
366
380
 
367
381
  attr_accessor :dynamic_introspection
@@ -393,6 +407,20 @@ module GraphQL
393
407
  end
394
408
  end
395
409
 
410
+ # @param text [String]
411
+ # @return [String, nil]
412
+ def comment(text = nil)
413
+ if text
414
+ @comment = text
415
+ elsif !NOT_CONFIGURED.equal?(@comment)
416
+ @comment
417
+ elsif @resolver_class
418
+ @resolver_class.comment
419
+ else
420
+ nil
421
+ end
422
+ end
423
+
396
424
  # Read extension instances from this field,
397
425
  # or add new classes/options to be initialized on this field.
398
426
  # Extensions are executed in the order they are added.
@@ -413,7 +441,7 @@ module GraphQL
413
441
  new_extensions.each do |extension_config|
414
442
  if extension_config.is_a?(Hash)
415
443
  extension_class, options = *extension_config.to_a[0]
416
- self.extension(extension_class, options)
444
+ self.extension(extension_class, **options)
417
445
  else
418
446
  self.extension(extension_config)
419
447
  end
@@ -433,7 +461,7 @@ module GraphQL
433
461
  # @param extension_class [Class] subclass of {Schema::FieldExtension}
434
462
  # @param options [Hash] if provided, given as `options:` when initializing `extension`.
435
463
  # @return [void]
436
- def extension(extension_class, options = nil)
464
+ def extension(extension_class, **options)
437
465
  extension_inst = extension_class.new(field: self, options: options)
438
466
  if @extensions.frozen?
439
467
  @extensions = @extensions.dup
@@ -483,7 +511,7 @@ module GraphQL
483
511
  if arguments[:last] && (max_possible_page_size.nil? || arguments[:last] > max_possible_page_size)
484
512
  max_possible_page_size = arguments[:last]
485
513
  end
486
- elsif arguments.is_a?(GraphQL::UnauthorizedError)
514
+ elsif arguments.is_a?(GraphQL::ExecutionError) || arguments.is_a?(GraphQL::UnauthorizedError)
487
515
  raise arguments
488
516
  end
489
517
 
@@ -577,16 +605,29 @@ module GraphQL
577
605
  class MissingReturnTypeError < GraphQL::Error; end
578
606
  attr_writer :type
579
607
 
580
- def type
581
- if @resolver_class
582
- return_type = @return_type_expr || @resolver_class.type_expr
583
- if return_type.nil?
584
- 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)"
608
+ # Get or set the return type of this field.
609
+ #
610
+ # It may return nil if no type was configured or if the given definition block wasn't called yet.
611
+ # @param new_type [Module, GraphQL::Schema::NonNull, GraphQL::Schema::List] A GraphQL return type
612
+ # @return [Module, GraphQL::Schema::NonNull, GraphQL::Schema::List, nil] the configured type for this field
613
+ def type(new_type = NOT_CONFIGURED)
614
+ if NOT_CONFIGURED.equal?(new_type)
615
+ if @resolver_class
616
+ return_type = @return_type_expr || @resolver_class.type_expr
617
+ if return_type.nil?
618
+ 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)"
619
+ end
620
+ nullable = @return_type_null.nil? ? @resolver_class.null : @return_type_null
621
+ Member::BuildType.parse_type(return_type, null: nullable)
622
+ elsif !@return_type_expr.nil?
623
+ @type ||= Member::BuildType.parse_type(@return_type_expr, null: @return_type_null)
585
624
  end
586
- nullable = @return_type_null.nil? ? @resolver_class.null : @return_type_null
587
- Member::BuildType.parse_type(return_type, null: nullable)
588
625
  else
589
- @type ||= Member::BuildType.parse_type(@return_type_expr, null: @return_type_null)
626
+ @return_type_expr = new_type
627
+ # If `type` is set in the definition block, then the `connection_extension: ...` given as a keyword won't be used, hmm...
628
+ # Also, arguments added by `connection_extension` will clobber anything previously defined,
629
+ # so `type(...)` should go first.
630
+ set_pagination_extensions(connection_extension: self.class.connection_extension)
590
631
  end
591
632
  rescue GraphQL::Schema::InvalidDocumentError, MissingReturnTypeError => err
592
633
  # Let this propagate up
@@ -897,6 +938,20 @@ ERR
897
938
  raise ArgumentError, "Invalid complexity for #{self.path}: #{own_complexity.inspect}"
898
939
  end
899
940
  end
941
+
942
+ def set_pagination_extensions(connection_extension:)
943
+ # This should run before connection extension,
944
+ # but should it run after the definition block?
945
+ if scoped?
946
+ self.extension(ScopeExtension, call_after_define: false)
947
+ end
948
+
949
+ # The problem with putting this after the definition_block
950
+ # is that it would override arguments
951
+ if connection? && connection_extension
952
+ self.extension(connection_extension, call_after_define: false)
953
+ end
954
+ end
900
955
  end
901
956
  end
902
957
  end
@@ -133,12 +133,14 @@ module GraphQL
133
133
  end
134
134
  # Add a method access
135
135
  method_name = argument_defn.keyword
136
- class_eval <<-RUBY, __FILE__, __LINE__
137
- def #{method_name}
138
- self[#{method_name.inspect}]
139
- end
140
- alias_method :#{method_name}, :#{method_name}
141
- RUBY
136
+ suppress_redefinition_warning do
137
+ class_eval <<-RUBY, __FILE__, __LINE__
138
+ def #{method_name}
139
+ self[#{method_name.inspect}]
140
+ end
141
+ alias_method :#{method_name}, :#{method_name}
142
+ RUBY
143
+ end
142
144
  argument_defn
143
145
  end
144
146
 
@@ -163,7 +165,7 @@ module GraphQL
163
165
 
164
166
  # Inject missing required arguments
165
167
  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)
168
+ if !input.key?(argument.graphql_name) && argument.type.non_null? && !argument.default_value? && types.argument(self, argument.graphql_name)
167
169
  m[argument.graphql_name] = nil
168
170
  end
169
171
 
@@ -243,6 +245,17 @@ module GraphQL
243
245
 
244
246
  result
245
247
  end
248
+
249
+ private
250
+
251
+ # Suppress redefinition warning for objectId arguments
252
+ def suppress_redefinition_warning
253
+ verbose = $VERBOSE
254
+ $VERBOSE = nil
255
+ yield
256
+ ensure
257
+ $VERBOSE = verbose
258
+ end
246
259
  end
247
260
 
248
261
  private
@@ -63,6 +63,7 @@ module GraphQL
63
63
 
64
64
  child_class.introspection(introspection)
65
65
  child_class.description(description)
66
+ child_class.comment(nil)
66
67
  # If interfaces are mixed into each other, only define this class once
67
68
  if !child_class.const_defined?(:UnresolvedTypeError, false)
68
69
  add_unresolved_type_error(child_class)
@@ -82,13 +83,29 @@ module GraphQL
82
83
  super
83
84
  end
84
85
 
86
+ # Register other Interface or Object types as implementers of this Interface.
87
+ #
88
+ # When those Interfaces or Objects aren't used as the return values of fields,
89
+ # they may have to be registered using this method so that GraphQL-Ruby can find them.
90
+ # @param types [Class, Module]
91
+ # @return [Array<Module, Class>] Implementers of this interface, if they're registered
85
92
  def orphan_types(*types)
86
93
  if types.any?
87
- @orphan_types = types
94
+ @orphan_types ||= []
95
+ @orphan_types.concat(types)
88
96
  else
89
- all_orphan_types = @orphan_types || []
90
- all_orphan_types += super if defined?(super)
91
- all_orphan_types.uniq
97
+ if defined?(@orphan_types)
98
+ all_orphan_types = @orphan_types.dup
99
+ if defined?(super)
100
+ all_orphan_types += super
101
+ all_orphan_types.uniq!
102
+ end
103
+ all_orphan_types
104
+ elsif defined?(super)
105
+ super
106
+ else
107
+ EmptyObjects::EMPTY_ARRAY
108
+ end
92
109
  end
93
110
  end
94
111
 
@@ -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]
@@ -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