graphql 1.11.2 → 1.11.7

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 (100) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/core.rb +8 -0
  3. data/lib/generators/graphql/object_generator.rb +2 -0
  4. data/lib/generators/graphql/templates/base_argument.erb +2 -0
  5. data/lib/generators/graphql/templates/base_enum.erb +2 -0
  6. data/lib/generators/graphql/templates/base_field.erb +2 -0
  7. data/lib/generators/graphql/templates/base_input_object.erb +2 -0
  8. data/lib/generators/graphql/templates/base_interface.erb +2 -0
  9. data/lib/generators/graphql/templates/base_mutation.erb +2 -0
  10. data/lib/generators/graphql/templates/base_object.erb +2 -0
  11. data/lib/generators/graphql/templates/base_scalar.erb +2 -0
  12. data/lib/generators/graphql/templates/base_union.erb +2 -0
  13. data/lib/generators/graphql/templates/enum.erb +2 -0
  14. data/lib/generators/graphql/templates/graphql_controller.erb +2 -0
  15. data/lib/generators/graphql/templates/interface.erb +2 -0
  16. data/lib/generators/graphql/templates/loader.erb +2 -0
  17. data/lib/generators/graphql/templates/mutation.erb +2 -0
  18. data/lib/generators/graphql/templates/mutation_type.erb +2 -0
  19. data/lib/generators/graphql/templates/object.erb +2 -0
  20. data/lib/generators/graphql/templates/query_type.erb +2 -0
  21. data/lib/generators/graphql/templates/scalar.erb +2 -0
  22. data/lib/generators/graphql/templates/schema.erb +2 -0
  23. data/lib/generators/graphql/templates/union.erb +3 -1
  24. data/lib/graphql.rb +17 -0
  25. data/lib/graphql/argument.rb +3 -3
  26. data/lib/graphql/backtrace/tracer.rb +2 -1
  27. data/lib/graphql/define/assign_global_id_field.rb +2 -2
  28. data/lib/graphql/execution/interpreter.rb +10 -0
  29. data/lib/graphql/execution/interpreter/arguments.rb +21 -6
  30. data/lib/graphql/execution/interpreter/arguments_cache.rb +8 -0
  31. data/lib/graphql/execution/interpreter/runtime.rb +94 -67
  32. data/lib/graphql/integer_decoding_error.rb +17 -0
  33. data/lib/graphql/introspection.rb +96 -0
  34. data/lib/graphql/introspection/field_type.rb +7 -3
  35. data/lib/graphql/introspection/input_value_type.rb +6 -0
  36. data/lib/graphql/introspection/introspection_query.rb +6 -92
  37. data/lib/graphql/introspection/type_type.rb +7 -3
  38. data/lib/graphql/invalid_null_error.rb +1 -1
  39. data/lib/graphql/language/block_string.rb +24 -5
  40. data/lib/graphql/language/lexer.rb +7 -3
  41. data/lib/graphql/language/lexer.rl +7 -3
  42. data/lib/graphql/language/nodes.rb +1 -1
  43. data/lib/graphql/language/parser.rb +107 -103
  44. data/lib/graphql/language/parser.y +4 -0
  45. data/lib/graphql/language/sanitized_printer.rb +59 -26
  46. data/lib/graphql/name_validator.rb +6 -7
  47. data/lib/graphql/pagination/connections.rb +11 -3
  48. data/lib/graphql/query.rb +6 -3
  49. data/lib/graphql/query/context.rb +34 -4
  50. data/lib/graphql/query/fingerprint.rb +2 -0
  51. data/lib/graphql/query/validation_pipeline.rb +4 -1
  52. data/lib/graphql/relay/array_connection.rb +2 -2
  53. data/lib/graphql/relay/range_add.rb +14 -5
  54. data/lib/graphql/schema.rb +49 -18
  55. data/lib/graphql/schema/argument.rb +56 -10
  56. data/lib/graphql/schema/build_from_definition.rb +67 -38
  57. data/lib/graphql/schema/build_from_definition/resolve_map.rb +3 -1
  58. data/lib/graphql/schema/default_type_error.rb +2 -0
  59. data/lib/graphql/schema/directive/deprecated.rb +1 -1
  60. data/lib/graphql/schema/field.rb +32 -16
  61. data/lib/graphql/schema/field/connection_extension.rb +44 -37
  62. data/lib/graphql/schema/field/scope_extension.rb +1 -1
  63. data/lib/graphql/schema/input_object.rb +5 -3
  64. data/lib/graphql/schema/interface.rb +1 -1
  65. data/lib/graphql/schema/late_bound_type.rb +2 -2
  66. data/lib/graphql/schema/loader.rb +1 -0
  67. data/lib/graphql/schema/member/build_type.rb +14 -4
  68. data/lib/graphql/schema/member/has_arguments.rb +54 -53
  69. data/lib/graphql/schema/member/has_fields.rb +17 -7
  70. data/lib/graphql/schema/member/type_system_helpers.rb +2 -2
  71. data/lib/graphql/schema/mutation.rb +4 -0
  72. data/lib/graphql/schema/relay_classic_mutation.rb +4 -2
  73. data/lib/graphql/schema/resolver.rb +6 -0
  74. data/lib/graphql/schema/resolver/has_payload_type.rb +2 -1
  75. data/lib/graphql/schema/subscription.rb +2 -12
  76. data/lib/graphql/schema/timeout.rb +29 -15
  77. data/lib/graphql/schema/unique_within_type.rb +1 -2
  78. data/lib/graphql/schema/validation.rb +8 -0
  79. data/lib/graphql/schema/warden.rb +2 -3
  80. data/lib/graphql/static_validation.rb +1 -0
  81. data/lib/graphql/static_validation/all_rules.rb +1 -0
  82. data/lib/graphql/static_validation/rules/fields_will_merge.rb +25 -17
  83. data/lib/graphql/static_validation/rules/input_object_names_are_unique.rb +30 -0
  84. data/lib/graphql/static_validation/rules/input_object_names_are_unique_error.rb +30 -0
  85. data/lib/graphql/static_validation/validation_timeout_error.rb +25 -0
  86. data/lib/graphql/static_validation/validator.rb +29 -7
  87. data/lib/graphql/subscriptions.rb +32 -22
  88. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +21 -7
  89. data/lib/graphql/tracing/appoptics_tracing.rb +10 -2
  90. data/lib/graphql/tracing/platform_tracing.rb +1 -1
  91. data/lib/graphql/tracing/prometheus_tracing/graphql_collector.rb +4 -1
  92. data/lib/graphql/types/int.rb +9 -2
  93. data/lib/graphql/types/iso_8601_date_time.rb +2 -1
  94. data/lib/graphql/types/relay/base_connection.rb +8 -6
  95. data/lib/graphql/types/relay/base_edge.rb +2 -1
  96. data/lib/graphql/types/string.rb +7 -1
  97. data/lib/graphql/unauthorized_error.rb +1 -1
  98. data/lib/graphql/version.rb +1 -1
  99. data/readme.md +1 -1
  100. metadata +10 -6
@@ -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
@@ -47,20 +47,22 @@ module GraphQL
47
47
  # A list of GraphQL-Ruby keywords.
48
48
  #
49
49
  # @api private
50
- GRAPHQL_RUBY_KEYWORDS = [:context, :object, :method, :raw_value]
50
+ GRAPHQL_RUBY_KEYWORDS = [:context, :object, :raw_value]
51
51
 
52
52
  # A list of field names that we should advise users to pick a different
53
53
  # resolve method name.
54
54
  #
55
55
  # @api private
56
- CONFLICT_FIELD_NAMES = Set.new(GRAPHQL_RUBY_KEYWORDS + RUBY_KEYWORDS)
56
+ CONFLICT_FIELD_NAMES = Set.new(GRAPHQL_RUBY_KEYWORDS + RUBY_KEYWORDS + Object.instance_methods)
57
57
 
58
58
  # Register this field with the class, overriding a previous one if needed.
59
59
  # @param field_defn [GraphQL::Schema::Field]
60
60
  # @return [void]
61
- def add_field(field_defn)
62
- if CONFLICT_FIELD_NAMES.include?(field_defn.resolver_method) && field_defn.original_name == field_defn.resolver_method && field_defn.method_conflict_warning?
63
- warn "#{self.graphql_name}'s `field :#{field_defn.name}` conflicts with a built-in method, use `resolver_method:` to pick a different resolver method for this field (for example, `resolver_method: :resolve_#{field_defn.resolver_method}` and `def resolve_#{field_defn.resolver_method}`). Or use `method_conflict_warning: false` to suppress this warning."
61
+ def add_field(field_defn, method_conflict_warning: field_defn.method_conflict_warning?)
62
+ # Check that `field_defn.original_name` equals `resolver_method` and `method_sym` --
63
+ # that shows that no override value was given manually.
64
+ if method_conflict_warning && CONFLICT_FIELD_NAMES.include?(field_defn.resolver_method) && field_defn.original_name == field_defn.resolver_method && field_defn.original_name == field_defn.method_sym
65
+ warn(conflict_field_name_warning(field_defn))
64
66
  end
65
67
  own_fields[field_defn.name] = field_defn
66
68
  nil
@@ -80,9 +82,9 @@ module GraphQL
80
82
  end
81
83
  end
82
84
 
83
- def global_id_field(field_name)
85
+ def global_id_field(field_name, **kwargs)
84
86
  id_resolver = GraphQL::Relay::GlobalIdResolve.new(type: self)
85
- field field_name, "ID", null: false
87
+ field field_name, "ID", **kwargs, null: false
86
88
  define_method(field_name) do
87
89
  id_resolver.call(object, {}, context)
88
90
  end
@@ -92,6 +94,14 @@ module GraphQL
92
94
  def own_fields
93
95
  @own_fields ||= {}
94
96
  end
97
+
98
+ private
99
+
100
+ # @param [GraphQL::Schema::Field]
101
+ # @return [String] A warning to give when this field definition might conflict with a built-in method
102
+ def conflict_field_name_warning(field_defn)
103
+ "#{self.graphql_name}'s `field :#{field_defn.original_name}` conflicts with a built-in method, use `resolver_method:` to pick a different resolver method for this field (for example, `resolver_method: :resolve_#{field_defn.resolver_method}` and `def resolve_#{field_defn.resolver_method}`). Or use `method_conflict_warning: false` to suppress this warning."
104
+ end
95
105
  end
96
106
  end
97
107
  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.
@@ -78,6 +78,10 @@ module GraphQL
78
78
 
79
79
  private
80
80
 
81
+ def conflict_field_name_warning(field_defn)
82
+ "#{self.graphql_name}'s `field :#{field_defn.name}` conflicts with a built-in method, use `hash_key:` or `method:` to pick a different resolve behavior for this field (for example, `hash_key: :#{field_defn.resolver_method}_value`, and modify the return hash). Or use `method_conflict_warning: false` to suppress this warning."
83
+ end
84
+
81
85
  # Override this to attach self as `mutation`
82
86
  def generate_payload_type
83
87
  payload_class = super
@@ -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
@@ -40,6 +40,7 @@ module GraphQL
40
40
  @arguments_by_keyword[arg.keyword] = arg
41
41
  end
42
42
  @arguments_loads_as_type = self.class.arguments_loads_as_type
43
+ @prepared_arguments = nil
43
44
  end
44
45
 
45
46
  # @return [Object] The application object this field is being resolved on
@@ -51,6 +52,10 @@ module GraphQL
51
52
  # @return [GraphQL::Schema::Field]
52
53
  attr_reader :field
53
54
 
55
+ def arguments
56
+ @prepared_arguments || raise("Arguments have not been prepared yet, still waiting for #load_arguments to resolve. (Call `.arguments` later in the code.)")
57
+ end
58
+
54
59
  # This method is _actually_ called by the runtime,
55
60
  # it does some preparation and then eventually calls
56
61
  # the user-defined `#resolve` method.
@@ -74,6 +79,7 @@ module GraphQL
74
79
  # for that argument, or may return a lazy object
75
80
  load_arguments_val = load_arguments(args)
76
81
  context.schema.after_lazy(load_arguments_val) do |loaded_args|
82
+ @prepared_arguments = loaded_args
77
83
  # Then call `authorized?`, which may raise or may return a lazy object
78
84
  authorized_val = if loaded_args.any?
79
85
  authorized?(**loaded_args)
@@ -58,7 +58,8 @@ module GraphQL
58
58
  resolver_fields.each do |name, f|
59
59
  # Reattach the already-defined field here
60
60
  # (The field's `.owner` will still point to the mutation, not the object type, I think)
61
- add_field(f)
61
+ # Don't re-warn about a method conflict. Since this type is generated, it should be fixed in the resolver instead.
62
+ add_field(f, method_conflict_warning: false)
62
63
  end
63
64
  end
64
65
  end
@@ -12,16 +12,6 @@ module GraphQL
12
12
  #
13
13
  # Also, `#unsubscribe` terminates the subscription.
14
14
  class Subscription < GraphQL::Schema::Resolver
15
- class EarlyTerminationError < StandardError
16
- end
17
-
18
- # Raised when `unsubscribe` is called; caught by `subscriptions.rb`
19
- class UnsubscribedError < EarlyTerminationError
20
- end
21
-
22
- # Raised when `no_update` is returned; caught by `subscriptions.rb`
23
- class NoUpdateError < EarlyTerminationError
24
- end
25
15
  extend GraphQL::Schema::Resolver::HasPayloadType
26
16
  extend GraphQL::Schema::Member::HasFields
27
17
 
@@ -65,7 +55,7 @@ module GraphQL
65
55
  def resolve_update(**args)
66
56
  ret_val = args.any? ? update(**args) : update
67
57
  if ret_val == :no_update
68
- raise NoUpdateError
58
+ throw :graphql_no_subscription_update
69
59
  else
70
60
  ret_val
71
61
  end
@@ -90,7 +80,7 @@ module GraphQL
90
80
 
91
81
  # Call this to halt execution and remove this subscription from the system
92
82
  def unsubscribe
93
- raise UnsubscribedError
83
+ throw :graphql_subscription_unsubscribed
94
84
  end
95
85
 
96
86
  READING_SCOPE = ::Object.new
@@ -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]