graphql 1.11.4 → 1.11.8

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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/object_generator.rb +2 -0
  3. data/lib/generators/graphql/templates/union.erb +1 -1
  4. data/lib/graphql.rb +17 -0
  5. data/lib/graphql/argument.rb +3 -3
  6. data/lib/graphql/backtrace/tracer.rb +2 -1
  7. data/lib/graphql/define/assign_global_id_field.rb +2 -2
  8. data/lib/graphql/execution/interpreter.rb +10 -0
  9. data/lib/graphql/execution/interpreter/arguments.rb +21 -6
  10. data/lib/graphql/execution/interpreter/arguments_cache.rb +8 -0
  11. data/lib/graphql/execution/interpreter/runtime.rb +70 -42
  12. data/lib/graphql/integer_decoding_error.rb +17 -0
  13. data/lib/graphql/introspection.rb +96 -0
  14. data/lib/graphql/introspection/field_type.rb +7 -3
  15. data/lib/graphql/introspection/input_value_type.rb +6 -0
  16. data/lib/graphql/introspection/introspection_query.rb +6 -92
  17. data/lib/graphql/introspection/type_type.rb +7 -3
  18. data/lib/graphql/invalid_null_error.rb +1 -1
  19. data/lib/graphql/language/block_string.rb +24 -5
  20. data/lib/graphql/language/lexer.rb +7 -3
  21. data/lib/graphql/language/lexer.rl +7 -3
  22. data/lib/graphql/language/nodes.rb +1 -1
  23. data/lib/graphql/language/parser.rb +107 -103
  24. data/lib/graphql/language/parser.y +4 -0
  25. data/lib/graphql/language/sanitized_printer.rb +59 -26
  26. data/lib/graphql/name_validator.rb +6 -7
  27. data/lib/graphql/pagination/connections.rb +11 -3
  28. data/lib/graphql/query.rb +6 -3
  29. data/lib/graphql/query/context.rb +14 -3
  30. data/lib/graphql/query/validation_pipeline.rb +1 -1
  31. data/lib/graphql/relay/array_connection.rb +2 -2
  32. data/lib/graphql/relay/range_add.rb +14 -5
  33. data/lib/graphql/schema.rb +47 -19
  34. data/lib/graphql/schema/argument.rb +56 -5
  35. data/lib/graphql/schema/build_from_definition.rb +67 -38
  36. data/lib/graphql/schema/default_type_error.rb +2 -0
  37. data/lib/graphql/schema/directive/deprecated.rb +1 -1
  38. data/lib/graphql/schema/field.rb +32 -16
  39. data/lib/graphql/schema/field/connection_extension.rb +8 -7
  40. data/lib/graphql/schema/field/scope_extension.rb +1 -1
  41. data/lib/graphql/schema/input_object.rb +5 -3
  42. data/lib/graphql/schema/interface.rb +1 -1
  43. data/lib/graphql/schema/late_bound_type.rb +2 -2
  44. data/lib/graphql/schema/loader.rb +1 -0
  45. data/lib/graphql/schema/member/build_type.rb +14 -4
  46. data/lib/graphql/schema/member/has_arguments.rb +54 -53
  47. data/lib/graphql/schema/member/has_fields.rb +2 -2
  48. data/lib/graphql/schema/member/type_system_helpers.rb +2 -2
  49. data/lib/graphql/schema/relay_classic_mutation.rb +4 -2
  50. data/lib/graphql/schema/timeout.rb +29 -15
  51. data/lib/graphql/schema/unique_within_type.rb +1 -2
  52. data/lib/graphql/schema/validation.rb +8 -0
  53. data/lib/graphql/static_validation.rb +1 -0
  54. data/lib/graphql/static_validation/all_rules.rb +1 -0
  55. data/lib/graphql/static_validation/rules/fields_will_merge.rb +25 -17
  56. data/lib/graphql/static_validation/rules/input_object_names_are_unique.rb +30 -0
  57. data/lib/graphql/static_validation/rules/input_object_names_are_unique_error.rb +30 -0
  58. data/lib/graphql/static_validation/validation_timeout_error.rb +25 -0
  59. data/lib/graphql/static_validation/validator.rb +29 -7
  60. data/lib/graphql/subscriptions.rb +1 -3
  61. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +21 -7
  62. data/lib/graphql/tracing/platform_tracing.rb +1 -1
  63. data/lib/graphql/tracing/prometheus_tracing/graphql_collector.rb +4 -1
  64. data/lib/graphql/types/int.rb +9 -2
  65. data/lib/graphql/types/relay/base_connection.rb +2 -1
  66. data/lib/graphql/types/relay/base_edge.rb +2 -1
  67. data/lib/graphql/types/string.rb +7 -1
  68. data/lib/graphql/unauthorized_error.rb +1 -1
  69. data/lib/graphql/version.rb +1 -1
  70. data/readme.md +1 -1
  71. metadata +7 -3
@@ -4,7 +4,7 @@ module GraphQL
4
4
  class Schema
5
5
  class Field
6
6
  class ScopeExtension < GraphQL::Schema::FieldExtension
7
- def after_resolve(value:, context:, **rest)
7
+ def after_resolve(object:, arguments:, context:, value:, memo:)
8
8
  if value.nil?
9
9
  value
10
10
  else
@@ -126,9 +126,11 @@ module GraphQL
126
126
  argument_defn = super(*args, **kwargs, &block)
127
127
  # Add a method access
128
128
  method_name = argument_defn.keyword
129
- define_method(method_name) do
130
- self[method_name]
131
- end
129
+ class_eval <<-RUBY, __FILE__, __LINE__
130
+ def #{method_name}
131
+ self[#{method_name.inspect}]
132
+ end
133
+ RUBY
132
134
  end
133
135
 
134
136
  def to_graphql
@@ -30,7 +30,7 @@ module GraphQL
30
30
 
31
31
  # The interface is accessible if any of its possible types are accessible
32
32
  def accessible?(context)
33
- context.schema.possible_types(self).each do |type|
33
+ context.schema.possible_types(self, context).each do |type|
34
34
  if context.schema.accessible?(type, context)
35
35
  return true
36
36
  end
@@ -16,11 +16,11 @@ module GraphQL
16
16
  end
17
17
 
18
18
  def to_non_null_type
19
- GraphQL::NonNullType.new(of_type: self)
19
+ @to_non_null_type ||= GraphQL::NonNullType.new(of_type: self)
20
20
  end
21
21
 
22
22
  def to_list_type
23
- GraphQL::ListType.new(of_type: self)
23
+ @to_list_type ||= GraphQL::ListType.new(of_type: self)
24
24
  end
25
25
 
26
26
  def inspect
@@ -189,6 +189,7 @@ module GraphQL
189
189
  kwargs = {
190
190
  type: type_resolver.call(arg["type"]),
191
191
  description: arg["description"],
192
+ deprecation_reason: arg["deprecationReason"],
192
193
  required: false,
193
194
  method_access: false,
194
195
  camelize: false,
@@ -4,6 +4,10 @@ module GraphQL
4
4
  class Member
5
5
  # @api private
6
6
  module BuildType
7
+ if !String.method_defined?(:match?)
8
+ using GraphQL::StringMatchBackport
9
+ end
10
+
7
11
  LIST_TYPE_ERROR = "Use an array of [T] or [T, null: true] for list types; other arrays are not supported"
8
12
 
9
13
  module_function
@@ -162,10 +166,16 @@ module GraphQL
162
166
  end
163
167
 
164
168
  def underscore(string)
165
- string
166
- .gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2') # URLDecoder -> URL_Decoder
167
- .gsub(/([a-z\d])([A-Z])/,'\1_\2') # someThing -> some_Thing
168
- .downcase
169
+ if string.match?(/\A[a-z_]+\Z/)
170
+ return string
171
+ end
172
+ string2 = string.dup
173
+
174
+ string2.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2') # URLDecoder -> URL_Decoder
175
+ string2.gsub!(/([a-z\d])([A-Z])/,'\1_\2') # someThing -> some_Thing
176
+ string2.downcase!
177
+
178
+ string2
169
179
  end
170
180
  end
171
181
  end
@@ -43,6 +43,7 @@ module GraphQL
43
43
  # @param arg_defn [GraphQL::Schema::Argument]
44
44
  # @return [GraphQL::Schema::Argument]
45
45
  def add_argument(arg_defn)
46
+ @own_arguments ||= {}
46
47
  own_arguments[arg_defn.name] = arg_defn
47
48
  arg_defn
48
49
  end
@@ -84,70 +85,69 @@ module GraphQL
84
85
  # @param context [GraphQL::Query::Context]
85
86
  # @return [Hash<Symbol, Object>, Execution::Lazy<Hash>]
86
87
  def coerce_arguments(parent_object, values, context)
87
- argument_values = {}
88
- kwarg_arguments = {}
89
88
  # Cache this hash to avoid re-merging it
90
89
  arg_defns = self.arguments
91
90
 
92
- maybe_lazies = []
93
- arg_lazies = arg_defns.map do |arg_name, arg_defn|
94
- arg_key = arg_defn.keyword
95
- has_value = false
96
- default_used = false
97
- if values.key?(arg_name)
98
- has_value = true
99
- value = values[arg_name]
100
- elsif values.key?(arg_key)
101
- has_value = true
102
- value = values[arg_key]
103
- elsif arg_defn.default_value?
104
- has_value = true
105
- value = arg_defn.default_value
106
- default_used = true
107
- end
108
-
109
- if has_value
110
- loads = arg_defn.loads
111
- loaded_value = nil
112
- if loads && !arg_defn.from_resolver?
113
- loaded_value = if arg_defn.type.list?
114
- loaded_values = value.map { |val| load_application_object(arg_defn, loads, val, context) }
115
- context.schema.after_any_lazies(loaded_values) { |result| result }
116
- else
117
- load_application_object(arg_defn, loads, value, context)
118
- end
91
+ if arg_defns.empty?
92
+ GraphQL::Execution::Interpreter::Arguments.new(argument_values: nil)
93
+ else
94
+ argument_values = {}
95
+ arg_lazies = arg_defns.map do |arg_name, arg_defn|
96
+ arg_key = arg_defn.keyword
97
+ has_value = false
98
+ default_used = false
99
+ if values.key?(arg_name)
100
+ has_value = true
101
+ value = values[arg_name]
102
+ elsif values.key?(arg_key)
103
+ has_value = true
104
+ value = values[arg_key]
105
+ elsif arg_defn.default_value?
106
+ has_value = true
107
+ value = arg_defn.default_value
108
+ default_used = true
119
109
  end
120
110
 
121
- coerced_value = if loaded_value
122
- loaded_value
123
- else
124
- context.schema.error_handler.with_error_handling(context) do
125
- arg_defn.type.coerce_input(value, context)
111
+ if has_value
112
+ loads = arg_defn.loads
113
+ loaded_value = nil
114
+ if loads && !arg_defn.from_resolver?
115
+ loaded_value = if arg_defn.type.list?
116
+ loaded_values = value.map { |val| load_application_object(arg_defn, loads, val, context) }
117
+ context.schema.after_any_lazies(loaded_values) { |result| result }
118
+ else
119
+ load_application_object(arg_defn, loads, value, context)
120
+ end
126
121
  end
127
- end
128
122
 
129
- context.schema.after_lazy(coerced_value) do |coerced_value|
130
- prepared_value = context.schema.error_handler.with_error_handling(context) do
131
- arg_defn.prepare_value(parent_object, coerced_value, context: context)
123
+ coerced_value = if loaded_value
124
+ loaded_value
125
+ else
126
+ context.schema.error_handler.with_error_handling(context) do
127
+ arg_defn.type.coerce_input(value, context)
128
+ end
132
129
  end
133
130
 
134
- kwarg_arguments[arg_key] = prepared_value
135
- # TODO code smell to access such a deeply-nested constant in a distant module
136
- argument_values[arg_key] = GraphQL::Execution::Interpreter::ArgumentValue.new(
137
- value: prepared_value,
138
- definition: arg_defn,
139
- default_used: default_used,
140
- )
131
+ context.schema.after_lazy(coerced_value) do |coerced_value|
132
+ prepared_value = context.schema.error_handler.with_error_handling(context) do
133
+ arg_defn.prepare_value(parent_object, coerced_value, context: context)
134
+ end
135
+
136
+ # TODO code smell to access such a deeply-nested constant in a distant module
137
+ argument_values[arg_key] = GraphQL::Execution::Interpreter::ArgumentValue.new(
138
+ value: prepared_value,
139
+ definition: arg_defn,
140
+ default_used: default_used,
141
+ )
142
+ end
141
143
  end
142
144
  end
143
- end
144
145
 
145
- maybe_lazies.concat(arg_lazies)
146
- context.schema.after_any_lazies(maybe_lazies) do
147
- GraphQL::Execution::Interpreter::Arguments.new(
148
- keyword_arguments: kwarg_arguments,
149
- argument_values: argument_values,
150
- )
146
+ context.schema.after_any_lazies(arg_lazies) do
147
+ GraphQL::Execution::Interpreter::Arguments.new(
148
+ argument_values: argument_values,
149
+ )
150
+ end
151
151
  end
152
152
  end
153
153
 
@@ -229,8 +229,9 @@ module GraphQL
229
229
  end
230
230
  end
231
231
 
232
+ NO_ARGUMENTS = {}.freeze
232
233
  def own_arguments
233
- @own_arguments ||= {}
234
+ @own_arguments || NO_ARGUMENTS
234
235
  end
235
236
  end
236
237
  end
@@ -82,9 +82,9 @@ module GraphQL
82
82
  end
83
83
  end
84
84
 
85
- def global_id_field(field_name)
85
+ def global_id_field(field_name, **kwargs)
86
86
  id_resolver = GraphQL::Relay::GlobalIdResolve.new(type: self)
87
- field field_name, "ID", null: false
87
+ field field_name, "ID", **kwargs, null: false
88
88
  define_method(field_name) do
89
89
  id_resolver.call(object, {}, context)
90
90
  end
@@ -6,12 +6,12 @@ module GraphQL
6
6
  module TypeSystemHelpers
7
7
  # @return [Schema::NonNull] Make a non-null-type representation of this type
8
8
  def to_non_null_type
9
- GraphQL::Schema::NonNull.new(self)
9
+ @to_non_null_type ||= GraphQL::Schema::NonNull.new(self)
10
10
  end
11
11
 
12
12
  # @return [Schema::List] Make a list-type representation of this type
13
13
  def to_list_type
14
- GraphQL::Schema::List.new(self)
14
+ @to_list_type ||= GraphQL::Schema::List.new(self)
15
15
  end
16
16
 
17
17
  # @return [Boolean] true if this is a non-nullable type. A nullable list of non-nullables is considered nullable.
@@ -105,7 +105,7 @@ module GraphQL
105
105
  sig = super
106
106
  # Arguments were added at the root, but they should be nested
107
107
  sig[:arguments].clear
108
- sig[:arguments][:input] = { type: input_type, required: true }
108
+ sig[:arguments][:input] = { type: input_type, required: true, description: "Parameters for #{graphql_name}" }
109
109
  sig
110
110
  end
111
111
 
@@ -122,7 +122,9 @@ module GraphQL
122
122
  graphql_name("#{mutation_name}Input")
123
123
  description("Autogenerated input type of #{mutation_name}")
124
124
  mutation(mutation_class)
125
- own_arguments.merge!(mutation_args)
125
+ mutation_args.each do |_name, arg|
126
+ add_argument(arg)
127
+ end
126
128
  argument :client_mutation_id, String, "A unique identifier for the client performing the mutation.", required: false
127
129
  end
128
130
  end
@@ -7,7 +7,7 @@ module GraphQL
7
7
  # to the `errors` key. Any already-resolved fields will be in the `data` key, so
8
8
  # you'll get a partial response.
9
9
  #
10
- # You can subclass `GraphQL::Schema::Timeout` and override the `handle_timeout` method
10
+ # You can subclass `GraphQL::Schema::Timeout` and override `max_seconds` and/or `handle_timeout`
11
11
  # to provide custom logic when a timeout error occurs.
12
12
  #
13
13
  # Note that this will stop a query _in between_ field resolutions, but
@@ -33,8 +33,6 @@ module GraphQL
33
33
  # end
34
34
  #
35
35
  class Timeout
36
- attr_reader :max_seconds
37
-
38
36
  def self.use(schema, **options)
39
37
  tracer = new(**options)
40
38
  schema.tracer(tracer)
@@ -48,32 +46,39 @@ module GraphQL
48
46
  def trace(key, data)
49
47
  case key
50
48
  when 'execute_multiplex'
51
- timeout_state = {
52
- timeout_at: Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) + max_seconds * 1000,
53
- timed_out: false
54
- }
55
-
56
49
  data.fetch(:multiplex).queries.each do |query|
50
+ timeout_duration_s = max_seconds(query)
51
+ timeout_state = if timeout_duration_s == false
52
+ # if the method returns `false`, don't apply a timeout
53
+ false
54
+ else
55
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
56
+ timeout_at = now + (max_seconds(query) * 1000)
57
+ {
58
+ timeout_at: timeout_at,
59
+ timed_out: false
60
+ }
61
+ end
57
62
  query.context.namespace(self.class)[:state] = timeout_state
58
63
  end
59
64
 
60
65
  yield
61
66
  when 'execute_field', 'execute_field_lazy'
62
- query = data[:context] ? data.fetch(:context).query : data.fetch(:query)
63
- timeout_state = query.context.namespace(self.class).fetch(:state)
64
- if Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) > timeout_state.fetch(:timeout_at)
67
+ query_context = data[:context] || data[:query].context
68
+ timeout_state = query_context.namespace(self.class).fetch(:state)
69
+ # If the `:state` is `false`, then `max_seconds(query)` opted out of timeout for this query.
70
+ if timeout_state != false && Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) > timeout_state.fetch(:timeout_at)
65
71
  error = if data[:context]
66
- context = data.fetch(:context)
67
- GraphQL::Schema::Timeout::TimeoutError.new(context.parent_type, context.field)
72
+ GraphQL::Schema::Timeout::TimeoutError.new(query_context.parent_type, query_context.field)
68
73
  else
69
74
  field = data.fetch(:field)
70
75
  GraphQL::Schema::Timeout::TimeoutError.new(field.owner, field)
71
76
  end
72
77
 
73
78
  # Only invoke the timeout callback for the first timeout
74
- unless timeout_state[:timed_out]
79
+ if !timeout_state[:timed_out]
75
80
  timeout_state[:timed_out] = true
76
- handle_timeout(error, query)
81
+ handle_timeout(error, query_context.query)
77
82
  end
78
83
 
79
84
  error
@@ -85,6 +90,15 @@ module GraphQL
85
90
  end
86
91
  end
87
92
 
93
+ # Called at the start of each query.
94
+ # The default implementation returns the `max_seconds:` value from installing this plugin.
95
+ #
96
+ # @param query [GraphQL::Query] The query that's about to run
97
+ # @return [Integer, false] The number of seconds after which to interrupt query execution and call {#handle_error}, or `false` to bypass the timeout.
98
+ def max_seconds(query)
99
+ @max_seconds
100
+ end
101
+
88
102
  # Invoked when a query times out.
89
103
  # @param error [GraphQL::Schema::Timeout::TimeoutError]
90
104
  # @param query [GraphQL::Error]
@@ -27,8 +27,7 @@ module GraphQL
27
27
  # @param node_id [String] A unique ID generated by {.encode}
28
28
  # @return [Array<(String, String)>] The type name & value passed to {.encode}
29
29
  def decode(node_id, separator: self.default_id_separator)
30
- # urlsafe_decode64 is for forward compatibility
31
- Base64Bp.urlsafe_decode64(node_id).split(separator, 2)
30
+ GraphQL::Schema::Base64Encoder.decode(node_id).split(separator, 2)
32
31
  end
33
32
  end
34
33
  end
@@ -133,6 +133,12 @@ module GraphQL
133
133
  end
134
134
  }
135
135
 
136
+ DEPRECATED_ARGUMENTS_ARE_OPTIONAL = ->(argument) {
137
+ if argument.deprecation_reason && argument.type.non_null?
138
+ "must be optional because it's deprecated"
139
+ end
140
+ }
141
+
136
142
  TYPE_IS_VALID_INPUT_TYPE = ->(type) {
137
143
  outer_type = type.type
138
144
  inner_type = outer_type.respond_to?(:unwrap) ? outer_type.unwrap : nil
@@ -265,8 +271,10 @@ module GraphQL
265
271
  Rules::NAME_IS_STRING,
266
272
  Rules::RESERVED_NAME,
267
273
  Rules::DESCRIPTION_IS_STRING_OR_NIL,
274
+ Rules.assert_property(:deprecation_reason, String, NilClass),
268
275
  Rules::TYPE_IS_VALID_INPUT_TYPE,
269
276
  Rules::DEFAULT_VALUE_IS_VALID_FOR_TYPE,
277
+ Rules::DEPRECATED_ARGUMENTS_ARE_OPTIONAL,
270
278
  ],
271
279
  GraphQL::BaseType => [
272
280
  Rules::NAME_IS_STRING,
@@ -4,6 +4,7 @@ require "graphql/static_validation/definition_dependencies"
4
4
  require "graphql/static_validation/type_stack"
5
5
  require "graphql/static_validation/validator"
6
6
  require "graphql/static_validation/validation_context"
7
+ require "graphql/static_validation/validation_timeout_error"
7
8
  require "graphql/static_validation/literal_validator"
8
9
  require "graphql/static_validation/base_visitor"
9
10
  require "graphql/static_validation/no_validate_visitor"
@@ -34,6 +34,7 @@ module GraphQL
34
34
  GraphQL::StaticValidation::VariableUsagesAreAllowed,
35
35
  GraphQL::StaticValidation::MutationRootExists,
36
36
  GraphQL::StaticValidation::SubscriptionRootExists,
37
+ GraphQL::StaticValidation::InputObjectNamesAreUnique,
37
38
  ]
38
39
  end
39
40
  end
@@ -202,15 +202,16 @@ module GraphQL
202
202
  )
203
203
  end
204
204
 
205
- args = possible_arguments(node1, node2)
206
- if args.size > 1
207
- msg = "Field '#{response_key}' has an argument conflict: #{args.map { |arg| GraphQL::Language.serialize(arg) }.join(" or ")}?"
205
+ if !same_arguments?(node1, node2)
206
+ args = [serialize_field_args(node1), serialize_field_args(node2)]
207
+ conflicts = args.map { |arg| GraphQL::Language.serialize(arg) }.join(" or ")
208
+ msg = "Field '#{response_key}' has an argument conflict: #{conflicts}?"
208
209
  context.errors << GraphQL::StaticValidation::FieldsWillMergeError.new(
209
210
  msg,
210
211
  nodes: [node1, node2],
211
212
  path: [],
212
213
  field_name: response_key,
213
- conflicts: args.map { |arg| GraphQL::Language.serialize(arg) }.join(" or ")
214
+ conflicts: conflicts
214
215
  )
215
216
  end
216
217
  end
@@ -326,20 +327,19 @@ module GraphQL
326
327
  [fields, fragment_spreads]
327
328
  end
328
329
 
329
- def possible_arguments(field1, field2)
330
+ def same_arguments?(field1, field2)
330
331
  # Check for incompatible / non-identical arguments on this node:
331
- [field1, field2].map do |n|
332
- if n.arguments.any?
333
- serialized_args = {}
334
- n.arguments.each do |a|
335
- arg_value = a.value
336
- serialized_args[a.name] = serialize_arg(arg_value)
337
- end
338
- serialized_args
339
- else
340
- NO_ARGS
341
- end
342
- end.uniq
332
+ arguments1 = field1.arguments
333
+ arguments2 = field2.arguments
334
+
335
+ return false if arguments1.length != arguments2.length
336
+
337
+ arguments1.all? do |argument1|
338
+ argument2 = arguments2.find { |argument| argument.name == argument1.name }
339
+ return false if argument2.nil?
340
+
341
+ serialize_arg(argument1.value) == serialize_arg(argument2.value)
342
+ end
343
343
  end
344
344
 
345
345
  def serialize_arg(arg_value)
@@ -353,6 +353,14 @@ module GraphQL
353
353
  end
354
354
  end
355
355
 
356
+ def serialize_field_args(field)
357
+ serialized_args = {}
358
+ field.arguments.each do |argument|
359
+ serialized_args[argument.name] = serialize_arg(argument.value)
360
+ end
361
+ serialized_args
362
+ end
363
+
356
364
  def compared_fragments_key(frag1, frag2, exclusive)
357
365
  # Cache key to not compare two fragments more than once.
358
366
  # The key includes both fragment names sorted (this way we