graphql 1.11.1 → 1.11.6

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 (94) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/core.rb +8 -0
  3. data/lib/generators/graphql/templates/base_argument.erb +2 -0
  4. data/lib/generators/graphql/templates/base_enum.erb +2 -0
  5. data/lib/generators/graphql/templates/base_field.erb +2 -0
  6. data/lib/generators/graphql/templates/base_input_object.erb +2 -0
  7. data/lib/generators/graphql/templates/base_interface.erb +2 -0
  8. data/lib/generators/graphql/templates/base_mutation.erb +2 -0
  9. data/lib/generators/graphql/templates/base_object.erb +2 -0
  10. data/lib/generators/graphql/templates/base_scalar.erb +2 -0
  11. data/lib/generators/graphql/templates/base_union.erb +2 -0
  12. data/lib/generators/graphql/templates/enum.erb +2 -0
  13. data/lib/generators/graphql/templates/graphql_controller.erb +13 -9
  14. data/lib/generators/graphql/templates/interface.erb +2 -0
  15. data/lib/generators/graphql/templates/loader.erb +2 -0
  16. data/lib/generators/graphql/templates/mutation.erb +2 -0
  17. data/lib/generators/graphql/templates/mutation_type.erb +2 -0
  18. data/lib/generators/graphql/templates/object.erb +2 -0
  19. data/lib/generators/graphql/templates/query_type.erb +2 -0
  20. data/lib/generators/graphql/templates/scalar.erb +2 -0
  21. data/lib/generators/graphql/templates/schema.erb +2 -0
  22. data/lib/generators/graphql/templates/union.erb +3 -1
  23. data/lib/graphql.rb +16 -0
  24. data/lib/graphql/argument.rb +3 -3
  25. data/lib/graphql/backtrace/tracer.rb +2 -1
  26. data/lib/graphql/define/assign_global_id_field.rb +2 -2
  27. data/lib/graphql/directive.rb +4 -0
  28. data/lib/graphql/execution/interpreter.rb +10 -0
  29. data/lib/graphql/execution/interpreter/arguments.rb +1 -1
  30. data/lib/graphql/execution/interpreter/runtime.rb +59 -45
  31. data/lib/graphql/field.rb +4 -0
  32. data/lib/graphql/input_object_type.rb +4 -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/language/block_string.rb +24 -5
  39. data/lib/graphql/language/lexer.rb +7 -3
  40. data/lib/graphql/language/lexer.rl +7 -3
  41. data/lib/graphql/language/nodes.rb +2 -1
  42. data/lib/graphql/language/parser.rb +107 -103
  43. data/lib/graphql/language/parser.y +4 -0
  44. data/lib/graphql/language/sanitized_printer.rb +59 -26
  45. data/lib/graphql/language/visitor.rb +2 -2
  46. data/lib/graphql/name_validator.rb +6 -7
  47. data/lib/graphql/pagination/connection.rb +6 -8
  48. data/lib/graphql/pagination/connections.rb +23 -3
  49. data/lib/graphql/query.rb +2 -2
  50. data/lib/graphql/query/context.rb +30 -3
  51. data/lib/graphql/query/fingerprint.rb +2 -0
  52. data/lib/graphql/query/validation_pipeline.rb +3 -0
  53. data/lib/graphql/relay/range_add.rb +14 -5
  54. data/lib/graphql/schema.rb +40 -31
  55. data/lib/graphql/schema/argument.rb +56 -5
  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/directive/deprecated.rb +1 -1
  59. data/lib/graphql/schema/enum_value.rb +1 -0
  60. data/lib/graphql/schema/field.rb +17 -10
  61. data/lib/graphql/schema/field/connection_extension.rb +44 -34
  62. data/lib/graphql/schema/input_object.rb +21 -18
  63. data/lib/graphql/schema/interface.rb +1 -1
  64. data/lib/graphql/schema/late_bound_type.rb +2 -2
  65. data/lib/graphql/schema/loader.rb +20 -1
  66. data/lib/graphql/schema/member/build_type.rb +14 -4
  67. data/lib/graphql/schema/member/has_arguments.rb +19 -1
  68. data/lib/graphql/schema/member/has_fields.rb +17 -7
  69. data/lib/graphql/schema/member/type_system_helpers.rb +2 -2
  70. data/lib/graphql/schema/mutation.rb +4 -0
  71. data/lib/graphql/schema/relay_classic_mutation.rb +3 -1
  72. data/lib/graphql/schema/resolver.rb +6 -0
  73. data/lib/graphql/schema/resolver/has_payload_type.rb +2 -1
  74. data/lib/graphql/schema/subscription.rb +2 -12
  75. data/lib/graphql/schema/timeout.rb +29 -15
  76. data/lib/graphql/schema/union.rb +29 -0
  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 +8 -3
  80. data/lib/graphql/static_validation/literal_validator.rb +7 -7
  81. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +1 -1
  82. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +2 -2
  83. data/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb +1 -2
  84. data/lib/graphql/static_validation/rules/variables_are_used_and_defined.rb +4 -2
  85. data/lib/graphql/static_validation/validator.rb +7 -4
  86. data/lib/graphql/subscriptions.rb +32 -22
  87. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +45 -20
  88. data/lib/graphql/subscriptions/serialize.rb +22 -4
  89. data/lib/graphql/tracing/appoptics_tracing.rb +10 -2
  90. data/lib/graphql/types/iso_8601_date_time.rb +2 -1
  91. data/lib/graphql/types/relay/base_connection.rb +6 -5
  92. data/lib/graphql/unauthorized_error.rb +1 -1
  93. data/lib/graphql/version.rb +1 -1
  94. metadata +3 -3
@@ -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
@@ -166,10 +168,7 @@ module GraphQL
166
168
  return result
167
169
  end
168
170
 
169
- # We're not actually _using_ the coerced result, we're just
170
- # using these methods to make sure that the object will
171
- # behave like a hash below, when we call `each` on it.
172
- begin
171
+ input = begin
173
172
  input.to_h
174
173
  rescue
175
174
  begin
@@ -182,21 +181,25 @@ module GraphQL
182
181
  end
183
182
  end
184
183
 
185
- visible_arguments_map = warden.arguments(self).reduce({}) { |m, f| m[f.name] = f; m}
186
-
187
- # Items in the input that are unexpected
188
- input.each do |name, value|
189
- if visible_arguments_map[name].nil?
190
- result.add_problem("Field is not defined on #{self.graphql_name}", [name])
184
+ # Inject missing required arguments
185
+ missing_required_inputs = self.arguments.reduce({}) do |m, (argument_name, argument)|
186
+ if !input.key?(argument_name) && argument.type.non_null? && warden.get_argument(self, argument_name)
187
+ m[argument_name] = nil
191
188
  end
189
+
190
+ m
192
191
  end
193
192
 
194
- # Items in the input that are expected, but have invalid values
195
- visible_arguments_map.map do |name, argument|
196
- argument_result = argument.type.validate_input(input[name], ctx)
197
- if !argument_result.valid?
198
- result.merge_result!(name, argument_result)
193
+ input.merge(missing_required_inputs).each do |argument_name, value|
194
+ argument = warden.get_argument(self, argument_name)
195
+ # Items in the input that are unexpected
196
+ unless argument
197
+ result.add_problem("Field is not defined on #{self.graphql_name}", [argument_name])
198
+ next
199
199
  end
200
+ # Items in the input that are expected, but have invalid values
201
+ argument_result = argument.type.validate_input(value, ctx)
202
+ result.merge_result!(argument_name, argument_result) unless argument_result.valid?
200
203
  end
201
204
 
202
205
  result
@@ -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
@@ -25,8 +25,15 @@ module GraphQL
25
25
  types[type["name"]] = type_object
26
26
  end
27
27
 
28
+ directives = []
29
+ schema.fetch("directives", []).each do |directive|
30
+ next if GraphQL::Schema.default_directives.include?(directive.fetch("name"))
31
+ directives << define_directive(directive, type_resolver)
32
+ end
33
+
28
34
  Class.new(GraphQL::Schema) do
29
35
  orphan_types(types.values)
36
+ directives(directives)
30
37
 
31
38
  def self.resolve_type(*)
32
39
  raise(GraphQL::RequiredImplementationMissingError, "This schema was loaded from string, so it can't resolve types for objects")
@@ -98,7 +105,7 @@ module GraphQL
98
105
  value(
99
106
  enum_value["name"],
100
107
  description: enum_value["description"],
101
- deprecation_reason: enum_value["deprecation_reason"],
108
+ deprecation_reason: enum_value["deprecationReason"],
102
109
  )
103
110
  end
104
111
  end
@@ -147,6 +154,16 @@ module GraphQL
147
154
  end
148
155
  end
149
156
 
157
+ def define_directive(directive, type_resolver)
158
+ loader = self
159
+ Class.new(GraphQL::Schema::Directive) do
160
+ graphql_name(directive["name"])
161
+ description(directive["description"])
162
+ locations(*directive["locations"].map(&:to_sym))
163
+ loader.build_arguments(self, directive["args"], type_resolver)
164
+ end
165
+ end
166
+
150
167
  public
151
168
 
152
169
  def build_fields(type_defn, fields, type_resolver)
@@ -156,6 +173,7 @@ module GraphQL
156
173
  field_hash["name"],
157
174
  type: type_resolver.call(field_hash["type"]),
158
175
  description: field_hash["description"],
176
+ deprecation_reason: field_hash["deprecationReason"],
159
177
  null: true,
160
178
  camelize: false,
161
179
  ) do
@@ -171,6 +189,7 @@ module GraphQL
171
189
  kwargs = {
172
190
  type: type_resolver.call(arg["type"]),
173
191
  description: arg["description"],
192
+ deprecation_reason: arg["deprecationReason"],
174
193
  required: false,
175
194
  method_access: false,
176
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
@@ -58,6 +59,22 @@ module GraphQL
58
59
  end
59
60
  end
60
61
 
62
+ # @return [GraphQL::Schema::Argument, nil] Argument defined on this thing, fetched by name.
63
+ def get_argument(argument_name)
64
+ a = own_arguments[argument_name]
65
+
66
+ if a || !self.is_a?(Class)
67
+ a
68
+ else
69
+ for ancestor in ancestors
70
+ if ancestor.respond_to?(:own_arguments) && a = ancestor.own_arguments[argument_name]
71
+ return a
72
+ end
73
+ end
74
+ nil
75
+ end
76
+ end
77
+
61
78
  # @param new_arg_class [Class] A class to use for building argument definitions
62
79
  def argument_class(new_arg_class = nil)
63
80
  self.class.argument_class(new_arg_class)
@@ -213,8 +230,9 @@ module GraphQL
213
230
  end
214
231
  end
215
232
 
233
+ NO_ARGUMENTS = {}.freeze
216
234
  def own_arguments
217
- @own_arguments ||= {}
235
+ @own_arguments || NO_ARGUMENTS
218
236
  end
219
237
  end
220
238
  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
@@ -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]