graphql 2.6.14 → 2.6.15

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 (122) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/templates/graphql_controller.erb +7 -1
  3. data/lib/graphql/analysis/max_query_complexity.rb +2 -1
  4. data/lib/graphql/analysis/max_query_depth.rb +2 -1
  5. data/lib/graphql/analysis/query_complexity.rb +2 -1
  6. data/lib/graphql/analysis/query_depth.rb +1 -1
  7. data/lib/graphql/analysis/visitor.rb +14 -2
  8. data/lib/graphql/analysis.rb +4 -5
  9. data/lib/graphql/analysis_error.rb +1 -1
  10. data/lib/graphql/coercion_error.rb +1 -1
  11. data/lib/graphql/current.rb +5 -1
  12. data/lib/graphql/dashboard.rb +1 -1
  13. data/lib/graphql/date_encoding_error.rb +3 -1
  14. data/lib/graphql/duration_encoding_error.rb +3 -1
  15. data/lib/graphql/execution/directive_checks.rb +2 -1
  16. data/lib/graphql/execution/errors.rb +18 -7
  17. data/lib/graphql/execution/field_resolve_step.rb +3 -3
  18. data/lib/graphql/execution/input_values.rb +160 -120
  19. data/lib/graphql/execution/interpreter/runtime.rb +9 -5
  20. data/lib/graphql/execution/lookahead.rb +19 -8
  21. data/lib/graphql/execution/next.rb +1 -0
  22. data/lib/graphql/execution/runner.rb +19 -12
  23. data/lib/graphql/execution/selections_step.rb +1 -1
  24. data/lib/graphql/execution/variable_values.rb +248 -0
  25. data/lib/graphql/float_decoding_error.rb +3 -1
  26. data/lib/graphql/integer_decoding_error.rb +3 -1
  27. data/lib/graphql/introspection/base_object.rb +1 -1
  28. data/lib/graphql/introspection/directive_location_enum.rb +1 -1
  29. data/lib/graphql/introspection/directive_type.rb +3 -1
  30. data/lib/graphql/introspection/dynamic_fields.rb +3 -1
  31. data/lib/graphql/introspection/enum_value_type.rb +3 -1
  32. data/lib/graphql/introspection/field_type.rb +5 -1
  33. data/lib/graphql/introspection/type_kind_enum.rb +1 -1
  34. data/lib/graphql/introspection.rb +2 -1
  35. data/lib/graphql/language/comment.rb +2 -1
  36. data/lib/graphql/language/generation.rb +2 -1
  37. data/lib/graphql/language/lexer.rb +29 -21
  38. data/lib/graphql/language/parser.rb +10 -7
  39. data/lib/graphql/language.rb +1 -1
  40. data/lib/graphql/name_validator.rb +2 -1
  41. data/lib/graphql/pagination.rb +1 -1
  42. data/lib/graphql/query/fingerprint.rb +2 -1
  43. data/lib/graphql/query/partial.rb +4 -0
  44. data/lib/graphql/query.rb +8 -12
  45. data/lib/graphql/railtie.rb +1 -1
  46. data/lib/graphql/relay.rb +1 -1
  47. data/lib/graphql/rubocop.rb +1 -1
  48. data/lib/graphql/runtime_error.rb +1 -1
  49. data/lib/graphql/runtime_type_error.rb +1 -1
  50. data/lib/graphql/schema/always_visible.rb +3 -1
  51. data/lib/graphql/schema/base_64_encoder.rb +3 -1
  52. data/lib/graphql/schema/built_in_types.rb +2 -2
  53. data/lib/graphql/schema/directive/deprecated.rb +1 -1
  54. data/lib/graphql/schema/directive/include.rb +2 -1
  55. data/lib/graphql/schema/directive/skip.rb +2 -1
  56. data/lib/graphql/schema/directive/specified_by.rb +1 -1
  57. data/lib/graphql/schema/field/scope_extension.rb +2 -1
  58. data/lib/graphql/schema/field.rb +3 -0
  59. data/lib/graphql/schema/find_inherited_value.rb +5 -1
  60. data/lib/graphql/schema/member/graphql_type_names.rb +1 -1
  61. data/lib/graphql/schema/member/has_directives.rb +20 -22
  62. data/lib/graphql/schema/member/has_path.rb +2 -1
  63. data/lib/graphql/schema/member/has_unresolved_type_error.rb +2 -1
  64. data/lib/graphql/schema/member.rb +3 -2
  65. data/lib/graphql/schema/mutation.rb +6 -1
  66. data/lib/graphql/schema/object.rb +1 -1
  67. data/lib/graphql/schema/resolver.rb +2 -2
  68. data/lib/graphql/schema/type_expression.rb +3 -1
  69. data/lib/graphql/schema/validator/all_validator.rb +6 -3
  70. data/lib/graphql/static_validation/all_rules.rb +1 -1
  71. data/lib/graphql/static_validation/base_visitor.rb +125 -53
  72. data/lib/graphql/static_validation/definition_dependencies.rb +14 -2
  73. data/lib/graphql/static_validation/interpreter_visitor.rb +1 -1
  74. data/lib/graphql/static_validation/rules/argument_names_are_unique.rb +4 -1
  75. data/lib/graphql/static_validation/rules/directives_are_in_valid_locations.rb +2 -0
  76. data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +8 -4
  77. data/lib/graphql/static_validation/rules/fragments_are_finite.rb +2 -1
  78. data/lib/graphql/static_validation/rules/fragments_are_named.rb +2 -1
  79. data/lib/graphql/static_validation/rules/fragments_are_named_error.rb +4 -1
  80. data/lib/graphql/static_validation/rules/fragments_are_on_composite_types.rb +6 -3
  81. data/lib/graphql/static_validation/rules/fragments_are_used.rb +2 -1
  82. data/lib/graphql/static_validation/rules/input_object_names_are_unique.rb +3 -1
  83. data/lib/graphql/static_validation/rules/mutation_root_exists.rb +3 -2
  84. data/lib/graphql/static_validation/rules/mutation_root_exists_error.rb +4 -1
  85. data/lib/graphql/static_validation/rules/no_definitions_are_present_error.rb +4 -1
  86. data/lib/graphql/static_validation/rules/not_single_subscription_error.rb +4 -1
  87. data/lib/graphql/static_validation/rules/one_of_input_objects_are_valid.rb +9 -2
  88. data/lib/graphql/static_validation/rules/query_root_exists.rb +3 -2
  89. data/lib/graphql/static_validation/rules/query_root_exists_error.rb +4 -1
  90. data/lib/graphql/static_validation/rules/required_input_object_attributes_are_present_error.rb +10 -4
  91. data/lib/graphql/static_validation/rules/subscription_root_exists_and_single_subscription_selection.rb +8 -6
  92. data/lib/graphql/static_validation/rules/subscription_root_exists_error.rb +4 -1
  93. data/lib/graphql/static_validation/rules/unique_directives_per_location.rb +5 -4
  94. data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +2 -1
  95. data/lib/graphql/static_validation/rules/variable_names_are_unique.rb +2 -1
  96. data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +1 -1
  97. data/lib/graphql/static_validation/rules/variables_are_input_types.rb +1 -1
  98. data/lib/graphql/static_validation/validation_context.rb +49 -14
  99. data/lib/graphql/static_validation/validation_timeout_error.rb +4 -1
  100. data/lib/graphql/static_validation.rb +1 -2
  101. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +14 -13
  102. data/lib/graphql/testing.rb +1 -1
  103. data/lib/graphql/tracing/legacy_trace.rb +1 -1
  104. data/lib/graphql/tracing/null_trace.rb +1 -1
  105. data/lib/graphql/types/big_int.rb +4 -1
  106. data/lib/graphql/types/boolean.rb +3 -1
  107. data/lib/graphql/types/id.rb +3 -1
  108. data/lib/graphql/types/iso_8601_date.rb +1 -0
  109. data/lib/graphql/types/iso_8601_date_time.rb +6 -2
  110. data/lib/graphql/types/iso_8601_duration.rb +3 -0
  111. data/lib/graphql/types/relay/base_connection.rb +1 -1
  112. data/lib/graphql/types/relay/base_edge.rb +1 -1
  113. data/lib/graphql/types/relay/node.rb +1 -1
  114. data/lib/graphql/types/relay/page_info.rb +1 -1
  115. data/lib/graphql/types/relay/page_info_behaviors.rb +4 -1
  116. data/lib/graphql/types/relay.rb +1 -1
  117. data/lib/graphql/types/string.rb +1 -0
  118. data/lib/graphql/types.rb +1 -1
  119. data/lib/graphql/version.rb +2 -2
  120. metadata +3 -4
  121. data/lib/graphql/query/variables.rb +0 -99
  122. data/lib/graphql/static_validation/literal_validator.rb +0 -157
@@ -2,44 +2,33 @@
2
2
  # typed: true
3
3
  module GraphQL
4
4
  module Execution
5
+ # Coerces input values (variables and arguments) for a query.
6
+ #
7
+ # Static validation uses [value_from_ast](rdoc-ref:#value_from_ast) to check literal values,
8
+ # and the runtime uses it to coerce them.
5
9
  class InputValues
6
- def initialize(query, runner)
10
+ def initialize(query)
7
11
  @query = query
8
- @runner = runner
12
+ @schema = query.schema
9
13
  @variable_values = nil
14
+ @coerced_literals = nil
10
15
  end
11
16
 
12
17
  def variable_values
13
- @variable_values ||= begin
14
- variable_nodes = @query.selected_operation.variables
15
- if variable_nodes.empty?
16
- EmptyObjects::EMPTY_HASH
17
- else
18
- raw_values = @query.provided_variables
19
- values = {}
20
- variable_nodes.each do |var_node|
21
- var_ast_value = if raw_values.key?(var_node.name)
22
- raw_values[var_node.name]
23
- elsif raw_values.key?(sym_name = var_node.name.to_sym)
24
- raw_values[sym_name]
25
- elsif !var_node.default_value.nil?
26
- var_node.default_value
27
- else
28
- next
29
- end
30
-
31
- var_type = @runner.schema.type_from_ast(var_node.type, context: @query.context)
32
- values[var_node.name] = variable_value(var_ast_value, var_type)
33
- end
34
- values
35
- end
36
- end
18
+ @variable_values ||= GraphQL::Execution::VariableValues.new(
19
+ @query,
20
+ @query.selected_operation&.variables || EmptyObjects::EMPTY_ARRAY,
21
+ @query.provided_variables,
22
+ )
37
23
  end
38
24
 
39
25
  def argument_values(owner_defn, argument_nodes, field_resolve_step)
40
26
  arg_defns = @query.types.arguments(owner_defn)
41
- argument_values = {}
27
+ if arg_defns.empty?
28
+ return [EmptyObjects::EMPTY_HASH, nil]
29
+ end
42
30
  errors = nil #: untyped
31
+ argument_values = {}
43
32
 
44
33
  arg_defns.each do |argument_definition|
45
34
  arg_ruby_key = argument_definition.keyword
@@ -49,6 +38,8 @@ module GraphQL
49
38
  if argument_definition.default_value?
50
39
  arg_value = value_from_ast(argument_definition.default_value, argument_definition.type)
51
40
  argument_value(argument_values, arg_ruby_key, argument_definition, arg_value, nil, field_resolve_step)
41
+ elsif argument_definition.type.non_null?
42
+ # Add an error
52
43
  end
53
44
  else
54
45
  arg_value = value_from_ast(arg_node.value, argument_definition.type)
@@ -64,74 +55,6 @@ module GraphQL
64
55
 
65
56
  private
66
57
 
67
- def variable_value(value, type)
68
- if type.non_null?
69
- type = type.of_type
70
- end
71
-
72
- if value.is_a?(Language::Nodes::Enum)
73
- value = value.name
74
- end
75
-
76
- if value.nil?
77
- nil
78
- elsif type.list?
79
- inner_type = type.of_type
80
- if value.is_a?(Array)
81
- value.map { |v| variable_value(v, inner_type) }.freeze
82
- else
83
- [variable_value(value, inner_type)].freeze
84
- end
85
- elsif type.kind.input_object?
86
- coerced_obj = {}
87
-
88
- if value.is_a?(Hash)
89
- @query.types.arguments(type).each do |arg|
90
- arg_key = arg.keyword
91
- if value.key?(arg.graphql_name)
92
- arg_value = value[arg.graphql_name]
93
- elsif value.key?(sym_name = arg.graphql_name.to_sym)
94
- arg_value = value[sym_name]
95
- elsif arg.default_value?
96
- coerced_obj[arg_key] = arg.default_value
97
- next
98
- else
99
- next
100
- end
101
-
102
- if arg_value.nil? && arg.replace_null_with_default?
103
- arg_value = arg.default_value
104
- end
105
-
106
- coerced_obj[arg_key] = variable_value(arg_value, arg.type)
107
- end
108
- else
109
- @query.types.arguments(type).each do |arg|
110
- arg_key = arg.keyword
111
- arg_name = arg.graphql_name
112
- if (v_node = value.arguments.find { |a| a.name == arg_name }) # rubocop:disable Development/ContextIsPassedCop
113
- arg_value = v_node.value
114
- coerced_obj[arg_key] = if arg_value.nil? && arg.replace_null_with_default?
115
- arg.default_value
116
- else
117
- variable_value(arg_value, arg.type)
118
- end
119
- elsif arg.default_value?
120
- coerced_obj[arg_key] = arg.default_value
121
- else
122
- # Nothing
123
- end
124
- end
125
- end
126
-
127
- coerced_obj
128
- elsif type.kind.leaf?
129
- type.coerce_input(value, @query.context)
130
- else
131
- raise GraphQL::Error, "Unexpected input type: #{type.graphql_name}."
132
- end
133
- end
134
-
135
58
  def argument_value(argument_values, argument_key, argument_definition, arg_value, override_type, field_resolve_step)
136
59
  treat_as_type = override_type || argument_definition.type
137
60
  if treat_as_type.non_null?
@@ -183,7 +106,7 @@ module GraphQL
183
106
  arg_value = begin
184
107
  argument_definition.prepare_value(nil, arg_value, context: @query.context)
185
108
  rescue StandardError => err
186
- handled_err = @runner.schema.handle_or_reraise(@query.context, err, object: nil, arguments: argument_values, field: field_resolve_step&.field_definition)
109
+ handled_err = @schema.handle_or_reraise(@query.context, err, object: nil, arguments: argument_values, field: field_resolve_step&.field_definition)
187
110
  raise handled_err if handled_err.is_a?(GraphQL::RuntimeError)
188
111
  handled_err
189
112
  end
@@ -211,7 +134,7 @@ module GraphQL
211
134
  argument_key: idx,
212
135
  )
213
136
  ps.push(loads_step)
214
- @runner.add_step(loads_step)
137
+ field_resolve_step.runner.add_step(loads_step)
215
138
  end
216
139
  else
217
140
  loads_step = LoadArgumentStep.new(
@@ -223,7 +146,7 @@ module GraphQL
223
146
  argument_key: argument_key,
224
147
  )
225
148
  ps.push(loads_step)
226
- @runner.add_step(loads_step)
149
+ field_resolve_step.runner.add_step(loads_step)
227
150
  end
228
151
  else
229
152
  argument_values[argument_key] = arg_value
@@ -231,31 +154,57 @@ module GraphQL
231
154
  nil
232
155
  end
233
156
 
234
- def value_from_ast(value_node, type)
157
+ public
158
+
159
+ # Raised by [value_from_ast](rdoc-ref:#value_from_ast) with `validate: true` when a literal value isn't valid for its type.
160
+ class InvalidLiteralError < GraphQL::Error # :nodoc:
161
+ # **Returns**
162
+ #
163
+ # - `String, nil` — A description of the problem, if there is one
164
+ attr_reader :explanation
165
+
166
+ def initialize(explanation = nil)
167
+ @explanation = explanation
168
+ super(explanation || "Invalid literal value")
169
+ end
170
+ end
171
+
172
+ # Coerce `value_node` (a literal from a query string or a default value) for `type`.
173
+ #
174
+ # **Parameters**
175
+ #
176
+ # - `value_node` (`GraphQL::Language::Nodes::AbstractNode, Object`)
177
+ # - `type` (`Class<GraphQL::Schema::Member>`)
178
+ # - `validate` (`Boolean`) — When true, raise `InvalidLiteralError` for invalid values and treat variable references as valid.
179
+ #
180
+ # :call-seq:
181
+ # value_from_ast(GraphQL::Language::Nodes::AbstractNode | Object value_node, Class[GraphQL::Schema::Member] type, bool validate:)
182
+ def value_from_ast(value_node, type, validate: false)
235
183
  if type.non_null?
236
184
  type = type.of_type
185
+ if validate && (value_node.nil? || value_node.is_a?(Language::Nodes::NullValue))
186
+ raise InvalidLiteralError
187
+ end
237
188
  end
238
189
 
239
- if value_node.nil?
190
+ if value_node.nil? || (value_node.is_a?(Language::Nodes::NullValue) && (validate || !type.kind.leaf?))
240
191
  nil
241
192
  elsif value_node.is_a?(GraphQL::Language::Nodes::VariableIdentifier)
242
- variable_values[value_node.name]
193
+ # Variables are checked by `VariableUsagesAreAllowed` during validation
194
+ validate ? nil : variable_values.coerced_value(value_node.name)
243
195
  elsif type.list?
244
196
  inner_type = type.of_type
245
197
  if value_node.is_a?(Array)
246
198
  coerced_items = value_node.map do |inner_value_node|
247
- value_from_ast(inner_value_node, inner_type)
199
+ value_from_ast(inner_value_node, inner_type, validate: validate)
248
200
  end
249
201
  coerced_items.freeze
250
- elsif value_node.is_a?(Language::Nodes::NullValue)
251
- nil
252
202
  else
253
- item_value = value_from_ast(value_node, inner_type)
203
+ item_value = value_from_ast(value_node, inner_type, validate: validate)
254
204
  [item_value].freeze
255
205
  end
256
206
  elsif type.kind.input_object?
257
207
  coerced_obj = {}
258
- # TODO manually handle NullValue here?
259
208
  if value_node.is_a?(Hash)
260
209
  @query.types.arguments(type).each do |arg|
261
210
  arg_value = value_node[arg.keyword]
@@ -267,9 +216,12 @@ module GraphQL
267
216
  next
268
217
  end
269
218
 
270
- coerced_obj[arg_key] = value_from_ast(arg_value, arg.type)
219
+ coerced_obj[arg_key] = value_from_ast(arg_value, arg.type, validate: validate)
220
+ end
221
+ elsif value_node.is_a?(Language::Nodes::InputObject)
222
+ if validate && type.one_of? && value_node.arguments.size != 1 # rubocop:disable Development/ContextIsPassedCop
223
+ raise InvalidLiteralError, "`#{type.graphql_name}` is a OneOf type, so only one argument may be given (instead of #{value_node.arguments.size})" # rubocop:disable Development/ContextIsPassedCop
271
224
  end
272
- else
273
225
  arg_nodes_by_name = value_node.arguments.each_with_object({}) do |arg_node, acc| # rubocop:disable Development/ContextIsPassedCop
274
226
  acc[arg_node.name] = arg_node
275
227
  end
@@ -277,30 +229,53 @@ module GraphQL
277
229
  @query.types.arguments(type).each do |arg|
278
230
  arg_node = arg_nodes_by_name[arg.graphql_name]
279
231
  arg_key = arg.keyword
280
- if arg_node.nil? || (arg_node.value.is_a?(Language::Nodes::VariableIdentifier) && !variable_values.key?(arg_node.value.name))
232
+ if arg_node.nil? || (!validate && arg_node.value.is_a?(Language::Nodes::VariableIdentifier) && !variable_values.key?(arg_node.value.name))
281
233
  if arg.default_value?
282
234
  coerced_obj[arg_key] = arg.default_value
235
+ elsif validate && arg.type.non_null?
236
+ raise InvalidLiteralError
283
237
  end
284
238
  next
285
239
  end
286
240
 
287
- arg_value = value_from_ast(arg_node.value, arg.type)
288
- coerced_obj[arg_key] = arg_value
241
+ coerced_obj[arg_key] = value_from_ast(arg_node.value, arg.type, validate: validate)
289
242
  end
290
- end
291
243
 
244
+ if validate
245
+ value_node.arguments.each do |arg_node| # rubocop:disable Development/ContextIsPassedCop
246
+ if @query.types.argument(type, arg_node.name).nil?
247
+ raise InvalidLiteralError
248
+ end
249
+ end
250
+ end
251
+ elsif validate
252
+ raise InvalidLiteralError
253
+ end
292
254
 
293
255
  coerced_obj
294
- elsif type.kind.leaf?
295
- if value_node.is_a?(Language::Nodes::AbstractNode) || value_node.is_a?(Array)
296
- value_node = coerce_untyped_input(value_node)
256
+ elsif type.kind.enum?
257
+ if validate
258
+ enum_value_from_ast(value_node, type)
259
+ else
260
+ if value_node.is_a?(Language::Nodes::Enum)
261
+ value_node = value_node.name
262
+ end
263
+ coerce_leaf_value(value_node, type)
297
264
  end
298
-
299
- begin
300
- type.coerce_input(value_node, @query.context)
301
- rescue GraphQL::UnauthorizedEnumValueError => enum_err
302
- @runner.schema.unauthorized_object(enum_err)
265
+ elsif type.kind.scalar?
266
+ if validate
267
+ scalar_value_from_ast(value_node, type)
268
+ elsif (literals = @coerced_literals&.[](type)) && literals.key?(value_node)
269
+ # This literal was already coerced during validation
270
+ literals[value_node]
271
+ else
272
+ if value_node.is_a?(Language::Nodes::AbstractNode) || value_node.is_a?(Array)
273
+ value_node = coerce_untyped_input(value_node)
274
+ end
275
+ coerce_leaf_value(value_node, type)
303
276
  end
277
+ elsif validate
278
+ raise InvalidLiteralError
304
279
  else
305
280
  raise "Unexpected input type: #{type.to_type_signature}."
306
281
  end
@@ -308,6 +283,69 @@ module GraphQL
308
283
 
309
284
  private
310
285
 
286
+ def coerce_leaf_value(value, type)
287
+ type.coerce_input(value, @query.context)
288
+ rescue GraphQL::UnauthorizedEnumValueError => enum_err
289
+ @schema.unauthorized_object(enum_err)
290
+ end
291
+
292
+ def enum_value_from_ast(value_node, type)
293
+ if !value_node.is_a?(Language::Nodes::Enum)
294
+ raise InvalidLiteralError
295
+ end
296
+ allowed_values = @query.types.enum_values(type)
297
+ if allowed_values.none? { |v| v.graphql_name == value_node.name }
298
+ raise InvalidLiteralError, "Expected #{GraphQL::Language.serialize(value_node.name)} to be one of: #{allowed_values.map(&:graphql_name).join(', ')}"
299
+ end
300
+ # Authorization is checked at runtime
301
+ nil
302
+ end
303
+
304
+ def scalar_value_from_ast(value_node, type)
305
+ literals = (@coerced_literals ||= {}.compare_by_identity)[type] ||= {}.compare_by_identity
306
+ if literals.key?(value_node)
307
+ return literals[value_node]
308
+ end
309
+
310
+ if contains_variable?(value_node)
311
+ # Variables aren't supported inside scalar literals
312
+ raise InvalidLiteralError
313
+ end
314
+
315
+ ruby_value = case value_node
316
+ when Language::Nodes::InputObject, Array
317
+ # Top-level enums are passed as AST nodes,
318
+ # but nested ones are converted to strings, to match previous behavior.
319
+ coerce_untyped_input(value_node)
320
+ else
321
+ value_node
322
+ end
323
+ coerced_value = begin
324
+ type.coerce_input(ruby_value, @query.context)
325
+ rescue GraphQL::CoercionError
326
+ raise
327
+ rescue StandardError => err
328
+ @query.handle_or_reraise(err)
329
+ end
330
+ if coerced_value.nil?
331
+ raise InvalidLiteralError, "Could not coerce value #{GraphQL::Language.serialize(ruby_value)} to #{type.graphql_name}"
332
+ end
333
+ literals[value_node] = coerced_value
334
+ end
335
+
336
+ def contains_variable?(value_node)
337
+ case value_node
338
+ when Language::Nodes::VariableIdentifier
339
+ true
340
+ when Array
341
+ value_node.any? { |v| contains_variable?(v) }
342
+ when Language::Nodes::InputObject
343
+ value_node.arguments.any? { |arg| contains_variable?(arg.value) } # rubocop:disable Development/ContextIsPassedCop
344
+ else
345
+ false
346
+ end
347
+ end
348
+
311
349
  def coerce_untyped_input(input_value)
312
350
  case input_value
313
351
  when Language::Nodes::AbstractNode
@@ -322,6 +360,8 @@ module GraphQL
322
360
  value_h[arg.name] = coerce_untyped_input(arg.value)
323
361
  end
324
362
  value_h
363
+ when Language::Nodes::VariableIdentifier
364
+ coerce_untyped_input(variable_values[input_value.name])
325
365
  else
326
366
  raise "Unhandled untyped input AST node: #{input_value.class}"
327
367
  end
@@ -221,7 +221,7 @@ module GraphQL
221
221
  end
222
222
 
223
223
  #: (untyped, untyped, untyped, untyped, Array[untyped]?, untyped, untyped) -> untyped
224
- def gather_selections(graphql_response, owner_object, owner_type, selections, selections_to_run, selections_by_name, ordered_result_keys)
224
+ def gather_selections(graphql_response, owner_object, owner_type, selections, selections_to_run, selections_by_name, ordered_result_keys, inlined_fragments: nil)
225
225
  selections.each do |node|
226
226
  if node.is_a?(GraphQL::Language::Nodes::Field)
227
227
  response_key = node.alias || node.name
@@ -271,14 +271,14 @@ module GraphQL
271
271
  )
272
272
 
273
273
  if query.types.possible_types(type_defn).include?(owner_type)
274
- result = gather_selections(graphql_response, owner_object, owner_type, node.selections, selections_to_run, next_selections, ordered_result_keys)
274
+ result = gather_selections(graphql_response, owner_object, owner_type, node.selections, selections_to_run, next_selections, ordered_result_keys, inlined_fragments: inlined_fragments)
275
275
  if !result.equal?(next_selections)
276
276
  selections_to_run = result
277
277
  end
278
278
  end
279
279
  else
280
280
  # it's an untyped fragment, definitely continue
281
- result = gather_selections(graphql_response, owner_object, owner_type, node.selections, selections_to_run, next_selections, ordered_result_keys)
281
+ result = gather_selections(graphql_response, owner_object, owner_type, node.selections, selections_to_run, next_selections, ordered_result_keys, inlined_fragments: inlined_fragments)
282
282
  if !result.equal?(next_selections)
283
283
  selections_to_run = result
284
284
  end
@@ -286,8 +286,12 @@ module GraphQL
286
286
  when GraphQL::Language::Nodes::FragmentSpread
287
287
  fragment_def = query.fragments[node.name]
288
288
  type_defn = query.types.type(fragment_def.type.name)
289
- if query.types.possible_types(type_defn).include?(owner_type)
290
- result = gather_selections(graphql_response, owner_object, owner_type, fragment_def.selections, selections_to_run, next_selections, ordered_result_keys)
289
+ # Spreading the same fragment again into these selections adds nothing, so only gather it once.
290
+ inlined_fragments ||= {}.compare_by_identity
291
+ inlined_names = (inlined_fragments[next_selections] ||= {})
292
+ if !inlined_names.key?(node.name) && query.types.possible_types(type_defn).include?(owner_type)
293
+ inlined_names[node.name] = true
294
+ result = gather_selections(graphql_response, owner_object, owner_type, fragment_def.selections, selections_to_run, next_selections, ordered_result_keys, inlined_fragments: inlined_fragments)
291
295
  if !result.equal?(next_selections)
292
296
  selections_to_run = result
293
297
  end
@@ -344,7 +344,7 @@ module GraphQL
344
344
  false
345
345
  end
346
346
 
347
- def find_selections(subselections_by_type, selections_on_type, selected_type, ast_selections, arguments)
347
+ def find_selections(subselections_by_type, selections_on_type, selected_type, ast_selections, arguments, inlined_fragments: nil)
348
348
  ast_selections.each do |ast_selection|
349
349
  next if skipped_by_directive?(ast_selection)
350
350
 
@@ -369,13 +369,19 @@ module GraphQL
369
369
  on_type = @query.types.type(t.name)
370
370
  subselections_on_type = subselections_by_type[on_type] ||= {}
371
371
  end
372
- find_selections(subselections_by_type, subselections_on_type, on_type, ast_selection.selections, arguments)
372
+ find_selections(subselections_by_type, subselections_on_type, on_type, ast_selection.selections, arguments, inlined_fragments: inlined_fragments)
373
373
  when GraphQL::Language::Nodes::FragmentSpread
374
374
  frag_defn = lookup_fragment(ast_selection)
375
375
  # Again, assuming a valid AST
376
376
  on_type = @query.types.type(frag_defn.type.name)
377
377
  subselections_on_type = subselections_by_type[on_type] ||= {}
378
- find_selections(subselections_by_type, subselections_on_type, on_type, frag_defn.selections, arguments)
378
+ # Spreading the same fragment again into these selections adds nothing, so only find its selections once.
379
+ inlined_fragments ||= {}.compare_by_identity
380
+ inlined_names = (inlined_fragments[subselections_on_type] ||= {})
381
+ if !inlined_names.key?(ast_selection.name)
382
+ inlined_names[ast_selection.name] = true
383
+ find_selections(subselections_by_type, subselections_on_type, on_type, frag_defn.selections, arguments, inlined_fragments: inlined_fragments)
384
+ end
379
385
  else
380
386
  raise "Invariant: Unexpected selection type: #{ast_selection.class}"
381
387
  end
@@ -384,7 +390,7 @@ module GraphQL
384
390
 
385
391
  # If a selection on `node` matches `field_name` (which is backed by `field_defn`)
386
392
  # and matches the `arguments:` constraints, then add that node to `matches`
387
- def find_selected_nodes(node, field_name, field_defn, arguments:, matches:, alias_name: NOT_CONFIGURED)
393
+ def find_selected_nodes(node, field_name, field_defn, arguments:, matches:, alias_name: NOT_CONFIGURED, inlined_fragments: nil)
388
394
  return if skipped_by_directive?(node)
389
395
  case node
390
396
  when GraphQL::Language::Nodes::Field
@@ -397,10 +403,14 @@ module GraphQL
397
403
  end
398
404
  end
399
405
  when GraphQL::Language::Nodes::InlineFragment
400
- node.selections.each { |s| find_selected_nodes(s, field_name, field_defn, arguments: arguments, matches: matches, alias_name: alias_name) }
406
+ node.selections.each { |s| find_selected_nodes(s, field_name, field_defn, arguments: arguments, matches: matches, alias_name: alias_name, inlined_fragments: inlined_fragments) }
401
407
  when GraphQL::Language::Nodes::FragmentSpread
402
- frag_defn = lookup_fragment(node)
403
- frag_defn.selections.each { |s| find_selected_nodes(s, field_name, field_defn, arguments: arguments, matches: matches, alias_name: alias_name) }
408
+ # A fragment spread again would only add the same nodes, so only search it once.
409
+ if inlined_fragments.nil? || !inlined_fragments.key?(node.name)
410
+ (inlined_fragments ||= {})[node.name] = true
411
+ frag_defn = lookup_fragment(node)
412
+ frag_defn.selections.each { |s| find_selected_nodes(s, field_name, field_defn, arguments: arguments, matches: matches, alias_name: alias_name, inlined_fragments: inlined_fragments) }
413
+ end
404
414
  else
405
415
  raise "Unexpected selection comparison on #{node.class.name} (#{node})"
406
416
  end
@@ -425,9 +435,10 @@ module GraphQL
425
435
 
426
436
  next_nodes = []
427
437
  field_name = field_defn.name
438
+ inlined_fragments = {}
428
439
  @ast_nodes.each do |ast_node|
429
440
  ast_node.selections.each do |selection|
430
- find_selected_nodes(selection, field_name, field_defn, arguments: arguments, matches: next_nodes, alias_name: alias_name)
441
+ find_selected_nodes(selection, field_name, field_defn, arguments: arguments, matches: next_nodes, alias_name: alias_name, inlined_fragments: inlined_fragments)
431
442
  end
432
443
  end
433
444
 
@@ -2,6 +2,7 @@
2
2
  # typed: true
3
3
  require "graphql/execution/prepare_object_step"
4
4
  require "graphql/execution/input_values"
5
+ require "graphql/execution/variable_values"
5
6
  require "graphql/execution/field_resolve_step"
6
7
  require "graphql/execution/finalize"
7
8
  require "graphql/execution/load_argument_step"
@@ -12,9 +12,6 @@ module GraphQL
12
12
  @selected_operation = nil
13
13
  @dataloader = multiplex.context[:dataloader] ||= @schema.dataloader_class.new
14
14
  @resolves_lazies = @schema.resolves_lazies?
15
- @input_values = Hash.new do |h, query|
16
- h[query] = InputValues.new(query, self)
17
- end.compare_by_identity
18
15
 
19
16
  @runtime_directives = nil #: Hash[String, singleton(GraphQL::Schema::Directive)]?
20
17
  @schema.directives.each do |name, dir_class|
@@ -53,7 +50,7 @@ module GraphQL
53
50
  @dataloader.append_job(step)
54
51
  end
55
52
 
56
- attr_reader :schema, :variables, :dataloader, :resolves_lazies, :authorizes, :static_type_at, :runtime_type_at, :finalizers, :input_values
53
+ attr_reader :schema, :variables, :dataloader, :resolves_lazies, :authorizes, :static_type_at, :runtime_type_at, :finalizers
57
54
 
58
55
  # **Returns**
59
56
  #
@@ -95,11 +92,15 @@ module GraphQL
95
92
 
96
93
  results = {}.compare_by_identity
97
94
  queries.each do |query|
98
- if query.validate && !query.valid?
95
+ # `valid?` also covers variable, operation name and analysis errors, which apply even with `validate: false`.
96
+ if !query.valid? || !query.context.errors.empty?
99
97
  results[query] = {
100
98
  "errors" => query.static_errors.map(&:to_h)
101
99
  }
102
100
  next
101
+ elsif query.selected_operation.nil?
102
+ results[query] = {}
103
+ next
103
104
  end
104
105
 
105
106
  root_type = query.root_type
@@ -176,7 +177,7 @@ module GraphQL
176
177
  Fiber[:__graphql_current_multiplex] = previous_multiplex
177
178
  end
178
179
 
179
- def gather_selections(type_defn, ast_selections, selections_step, query, all_selections, prototype_result, into:)
180
+ def gather_selections(type_defn, ast_selections, selections_step, query, all_selections, prototype_result, into:, inlined_fragments: nil)
180
181
  ast_selections.each do |ast_selection|
181
182
  next if !directives_include?(query, ast_selection)
182
183
 
@@ -201,7 +202,7 @@ module GraphQL
201
202
  all_selections << (into = { __node: ast_selection })
202
203
  all_selections << (prototype_result = {})
203
204
  end
204
- gather_selections(type_defn, ast_selection.selections, selections_step, query, all_selections, prototype_result, into: into)
205
+ gather_selections(type_defn, ast_selection.selections, selections_step, query, all_selections, prototype_result, into: into, inlined_fragments: inlined_fragments)
205
206
  end
206
207
  when GraphQL::Language::Nodes::FragmentSpread
207
208
  fragment_definition = query.fragments[ast_selection.name]
@@ -211,7 +212,13 @@ module GraphQL
211
212
  all_selections << (into = { __node: ast_selection })
212
213
  all_selections << (prototype_result = {})
213
214
  end
214
- gather_selections(type_defn, fragment_definition.selections, selections_step, query, all_selections, prototype_result, into: into)
215
+ # Spreading the same fragment again into these selections adds nothing, so only gather it once.
216
+ inlined_fragments ||= {}.compare_by_identity
217
+ inlined_names = (inlined_fragments[into] ||= {})
218
+ if !inlined_names.key?(ast_selection.name)
219
+ inlined_names[ast_selection.name] = true
220
+ gather_selections(type_defn, fragment_definition.selections, selections_step, query, all_selections, prototype_result, into: into, inlined_fragments: inlined_fragments)
221
+ end
215
222
  end
216
223
  else
217
224
  raise ArgumentError, "Unsupported graphql selection node: #{ast_selection.class} (#{ast_selection.inspect})"
@@ -285,7 +292,7 @@ module GraphQL
285
292
  continue_execution = true #: bool
286
293
  query_dirs.each do |dir_node|
287
294
  dir_defn = runtime_directives[dir_node.name] || raise(GraphQL::Error, "No directive definition found for: #{dir_node.name.inspect}")
288
- dir_args, errors = input_values[query].argument_values(dir_defn, dir_node.arguments, nil) # rubocop:disable Development/ContextIsPassedCop
295
+ dir_args, errors = query.input_values.argument_values(dir_defn, dir_node.arguments, nil) # rubocop:disable Development/ContextIsPassedCop
289
296
  if errors
290
297
  errors.each { |e|
291
298
  e.ast_node = dir_node
@@ -412,14 +419,14 @@ module GraphQL
412
419
  if ast_selection.directives.any? { |dir_node|
413
420
  case dir_node.name
414
421
  when "skip"
415
- skip_args, _errors = @input_values[query].argument_values(GraphQL::Schema::Directive::Skip, dir_node.arguments, nil) # rubocop:disable Development/ContextIsPassedCop
422
+ skip_args, _errors = query.input_values.argument_values(GraphQL::Schema::Directive::Skip, dir_node.arguments, nil) # rubocop:disable Development/ContextIsPassedCop
416
423
  skip_args[:if] == true
417
424
  when "include"
418
- include_args, _errors = @input_values[query].argument_values(GraphQL::Schema::Directive::Include, dir_node.arguments, nil) # rubocop:disable Development/ContextIsPassedCop
425
+ include_args, _errors = query.input_values.argument_values(GraphQL::Schema::Directive::Include, dir_node.arguments, nil) # rubocop:disable Development/ContextIsPassedCop
419
426
  include_args[:if] == false
420
427
  else
421
428
  dir_defn = runtime_directives[dir_node.name]
422
- dir_args, _errors = @input_values[query].argument_values(dir_defn, dir_node.arguments, nil) # rubocop:disable Development/ContextIsPassedCop
429
+ dir_args, _errors = query.input_values.argument_values(dir_defn, dir_node.arguments, nil) # rubocop:disable Development/ContextIsPassedCop
423
430
  !dir_defn.include?(nil, dir_args, query.context)
424
431
  end
425
432
  }
@@ -43,7 +43,7 @@ module GraphQL
43
43
  directives.each do |dir_node|
44
44
  dir_defn = @runner.runtime_directives[dir_node.name]
45
45
  if dir_defn # not present for `skip` or `include`
46
- dir_args, _errors = @runner.input_values[query].argument_values(dir_defn, dir_node.arguments, nil) # rubocop:disable Development/ContextIsPassedCop
46
+ dir_args, _errors = query.input_values.argument_values(dir_defn, dir_node.arguments, nil) # rubocop:disable Development/ContextIsPassedCop
47
47
  result = case directives_owner
48
48
  when Language::Nodes::FragmentSpread
49
49
  dir_defn.resolve_fragment_spread(directives_owner, @parent_type, @objects, dir_args, self.query.context)