graphql 0.15.3 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql.rb +4 -1
  3. data/lib/graphql/analysis.rb +5 -0
  4. data/lib/graphql/analysis/analyze_query.rb +73 -0
  5. data/lib/graphql/analysis/max_query_complexity.rb +25 -0
  6. data/lib/graphql/analysis/max_query_depth.rb +25 -0
  7. data/lib/graphql/analysis/query_complexity.rb +122 -0
  8. data/lib/graphql/analysis/query_depth.rb +54 -0
  9. data/lib/graphql/analysis_error.rb +4 -0
  10. data/lib/graphql/base_type.rb +7 -0
  11. data/lib/graphql/define/assign_object_field.rb +2 -1
  12. data/lib/graphql/field.rb +25 -3
  13. data/lib/graphql/input_object_type.rb +1 -1
  14. data/lib/graphql/internal_representation.rb +2 -0
  15. data/lib/graphql/internal_representation/node.rb +81 -0
  16. data/lib/graphql/internal_representation/rewrite.rb +177 -0
  17. data/lib/graphql/language/visitor.rb +15 -9
  18. data/lib/graphql/object_type.rb +1 -1
  19. data/lib/graphql/query.rb +66 -7
  20. data/lib/graphql/query/context.rb +10 -3
  21. data/lib/graphql/query/directive_resolution.rb +5 -5
  22. data/lib/graphql/query/serial_execution.rb +5 -3
  23. data/lib/graphql/query/serial_execution/field_resolution.rb +22 -15
  24. data/lib/graphql/query/serial_execution/operation_resolution.rb +7 -5
  25. data/lib/graphql/query/serial_execution/selection_resolution.rb +20 -105
  26. data/lib/graphql/query/serial_execution/value_resolution.rb +15 -12
  27. data/lib/graphql/schema.rb +7 -2
  28. data/lib/graphql/schema/timeout_middleware.rb +67 -0
  29. data/lib/graphql/static_validation/all_rules.rb +0 -1
  30. data/lib/graphql/static_validation/type_stack.rb +7 -11
  31. data/lib/graphql/static_validation/validation_context.rb +11 -1
  32. data/lib/graphql/static_validation/validator.rb +14 -4
  33. data/lib/graphql/version.rb +1 -1
  34. data/readme.md +10 -9
  35. data/spec/graphql/analysis/analyze_query_spec.rb +50 -0
  36. data/spec/graphql/analysis/max_query_complexity_spec.rb +62 -0
  37. data/spec/graphql/{static_validation/rules/document_does_not_exceed_max_depth_spec.rb → analysis/max_query_depth_spec.rb} +20 -21
  38. data/spec/graphql/analysis/query_complexity_spec.rb +235 -0
  39. data/spec/graphql/analysis/query_depth_spec.rb +80 -0
  40. data/spec/graphql/directive_spec.rb +1 -0
  41. data/spec/graphql/internal_representation/rewrite_spec.rb +120 -0
  42. data/spec/graphql/introspection/schema_type_spec.rb +1 -0
  43. data/spec/graphql/language/visitor_spec.rb +14 -4
  44. data/spec/graphql/non_null_type_spec.rb +31 -0
  45. data/spec/graphql/query/context_spec.rb +24 -1
  46. data/spec/graphql/query_spec.rb +6 -2
  47. data/spec/graphql/schema/timeout_middleware_spec.rb +180 -0
  48. data/spec/graphql/static_validation/rules/argument_literals_are_compatible_spec.rb +1 -1
  49. data/spec/graphql/static_validation/rules/arguments_are_defined_spec.rb +1 -1
  50. data/spec/graphql/static_validation/rules/directives_are_defined_spec.rb +1 -1
  51. data/spec/graphql/static_validation/rules/directives_are_in_valid_locations_spec.rb +1 -1
  52. data/spec/graphql/static_validation/rules/fields_are_defined_on_type_spec.rb +1 -1
  53. data/spec/graphql/static_validation/rules/fields_have_appropriate_selections_spec.rb +1 -1
  54. data/spec/graphql/static_validation/rules/fields_will_merge_spec.rb +1 -1
  55. data/spec/graphql/static_validation/rules/fragment_spreads_are_possible_spec.rb +1 -1
  56. data/spec/graphql/static_validation/rules/fragment_types_exist_spec.rb +1 -1
  57. data/spec/graphql/static_validation/rules/fragments_are_finite_spec.rb +1 -1
  58. data/spec/graphql/static_validation/rules/fragments_are_on_composite_types_spec.rb +1 -1
  59. data/spec/graphql/static_validation/rules/fragments_are_used_spec.rb +1 -1
  60. data/spec/graphql/static_validation/rules/required_arguments_are_present_spec.rb +1 -1
  61. data/spec/graphql/static_validation/rules/variable_default_values_are_correctly_typed_spec.rb +1 -1
  62. data/spec/graphql/static_validation/rules/variable_usages_are_allowed_spec.rb +1 -1
  63. data/spec/graphql/static_validation/rules/variables_are_input_types_spec.rb +1 -1
  64. data/spec/graphql/static_validation/rules/variables_are_used_and_defined_spec.rb +1 -1
  65. data/spec/graphql/static_validation/validator_spec.rb +1 -1
  66. data/spec/support/dairy_app.rb +22 -1
  67. metadata +29 -5
  68. data/lib/graphql/static_validation/rules/document_does_not_exceed_max_depth.rb +0 -79
@@ -5,10 +5,12 @@ module GraphQL
5
5
  class Context
6
6
  attr_accessor :execution_strategy
7
7
 
8
- # The {GraphQL::Language::Nodes::Field} for the currently-executing field.
9
- # @return [GraphQL::Language::Nodes::Field]
8
+ # @return [GraphQL::Language::Nodes::Field] The AST node for the currently-executing field
10
9
  attr_accessor :ast_node
11
10
 
11
+ # @return [GraphQL::InternalRepresentation::Node] The internal representation for this query node
12
+ attr_accessor :irep_node
13
+
12
14
  # @return [Array<GraphQL::ExecutionError>] errors returned during execution
13
15
  attr_reader :errors
14
16
 
@@ -28,10 +30,15 @@ module GraphQL
28
30
  @errors = []
29
31
  end
30
32
 
31
- # Lookup `key` from the hash passed to {Schema#execute} as `context`
33
+ # Lookup `key` from the hash passed to {Schema#execute} as `context:`
32
34
  def [](key)
33
35
  @values[key]
34
36
  end
37
+
38
+ # Reassign `key` to the hash passed to {Schema#execute} as `context:`
39
+ def []=(key, value)
40
+ @values[key] = value
41
+ end
35
42
  end
36
43
  end
37
44
  end
@@ -1,11 +1,11 @@
1
1
  module GraphQL
2
2
  class Query
3
3
  module DirectiveResolution
4
- def self.include_node?(ast_node, query)
5
- ast_node.directives.each do |ast_directive|
6
- directive = query.schema.directives[ast_directive.name]
7
- args = GraphQL::Query::LiteralInput.from_arguments(ast_directive.arguments, directive.arguments, query.variables)
8
- if !directive.include?(args)
4
+ def self.include_node?(irep_node, query)
5
+ irep_node.directives.each do |directive_node|
6
+ directive_defn = directive_node.definition
7
+ args = query.arguments_for(directive_node)
8
+ if !directive_defn.include?(args)
9
9
  return false
10
10
  end
11
11
  end
@@ -9,11 +9,13 @@ module GraphQL
9
9
  # @param root_type [GraphQL::ObjectType] either the query type or the mutation type
10
10
  # @param query_obj [GraphQL::Query] the query object for this execution
11
11
  # @return [Hash] a spec-compliant GraphQL result, as a hash
12
- def execute(ast_operation, root_type, query_obj)
12
+ def execute(ast_operation, root_type, query_object)
13
+ irep_root = query_object.internal_representation[ast_operation.name]
14
+
13
15
  operation_resolution.new(
14
- ast_operation,
16
+ irep_root,
15
17
  root_type,
16
- ExecutionContext.new(query_obj, self)
18
+ ExecutionContext.new(query_object, self)
17
19
  ).result
18
20
  end
19
21
 
@@ -2,28 +2,33 @@ module GraphQL
2
2
  class Query
3
3
  class SerialExecution
4
4
  class FieldResolution
5
- attr_reader :ast_node, :parent_type, :target, :execution_context, :field, :arguments
5
+ attr_reader :irep_node, :parent_type, :target, :execution_context, :field, :arguments
6
6
 
7
- def initialize(ast_node, parent_type, target, execution_context)
8
- @ast_node = ast_node
7
+ def initialize(irep_node, parent_type, target, execution_context)
8
+ @irep_node = irep_node
9
9
  @parent_type = parent_type
10
10
  @target = target
11
11
  @execution_context = execution_context
12
- @field = execution_context.get_field(parent_type, ast_node.name)
12
+ @field = execution_context.get_field(parent_type, irep_node.definition.name)
13
13
  if @field.nil?
14
14
  raise("No field found on #{parent_type.name} '#{parent_type}' for '#{ast_node.name}'")
15
15
  end
16
- @arguments = GraphQL::Query::LiteralInput.from_arguments(
17
- ast_node.arguments,
18
- field.arguments,
19
- execution_context.query.variables
20
- )
16
+ @arguments = execution_context.query.arguments_for(irep_node)
21
17
  end
22
18
 
23
19
  def result
24
- result_name = ast_node.alias || ast_node.name
25
- raw_value = get_raw_value
26
- { result_name => get_finished_value(raw_value) }
20
+ result_name = irep_node.name
21
+ begin
22
+ raw_value = get_raw_value
23
+ { result_name => get_finished_value(raw_value) }
24
+ rescue GraphQL::InvalidNullError => err
25
+ if field.type.kind.non_null?
26
+ raise(err)
27
+ else
28
+ err.parent_error? || execution_context.add_error(err)
29
+ {result_name => nil}
30
+ end
31
+ end
27
32
  end
28
33
 
29
34
  private
@@ -32,12 +37,12 @@ module GraphQL
32
37
  # continue by "finishing" the value, eg. executing sub-fields or coercing values
33
38
  def get_finished_value(raw_value)
34
39
  if raw_value.is_a?(GraphQL::ExecutionError)
35
- raw_value.ast_node = ast_node
40
+ raw_value.ast_node = irep_node.ast_node
36
41
  execution_context.add_error(raw_value)
37
42
  end
38
43
 
39
44
  strategy_class = GraphQL::Query::SerialExecution::ValueResolution.get_strategy_for_kind(field.type.kind)
40
- result_strategy = strategy_class.new(raw_value, field.type, target, parent_type, ast_node, execution_context)
45
+ result_strategy = strategy_class.new(raw_value, field.type, target, parent_type, irep_node, execution_context)
41
46
  result_strategy.result
42
47
  end
43
48
 
@@ -63,9 +68,11 @@ module GraphQL
63
68
  # @return [Proc] suitable to be the last step in a middleware chain
64
69
  def get_middleware_proc_from_field_resolve
65
70
  -> (_parent_type, parent_object, field_definition, field_args, context, _next) {
66
- context.ast_node = ast_node
71
+ context.ast_node = irep_node.ast_node
72
+ context.irep_node = irep_node
67
73
  value = field_definition.resolve(parent_object, field_args, context)
68
74
  context.ast_node = nil
75
+ context.irep_node = nil
69
76
  value
70
77
  }
71
78
  end
@@ -2,22 +2,24 @@ module GraphQL
2
2
  class Query
3
3
  class SerialExecution
4
4
  class OperationResolution
5
- attr_reader :target, :ast_operation_definition, :execution_context
5
+ attr_reader :target, :execution_context, :irep_node
6
6
 
7
- def initialize(ast_operation_definition, target, execution_context)
8
- @ast_operation_definition = ast_operation_definition
7
+ def initialize(irep_node, target, execution_context)
9
8
  @target = target
9
+ @irep_node = irep_node
10
10
  @execution_context = execution_context
11
11
  end
12
12
 
13
13
  def result
14
- selections = ast_operation_definition.selections
15
14
  execution_context.strategy.selection_resolution.new(
16
15
  execution_context.query.root_value,
17
16
  target,
18
- selections,
17
+ irep_node,
19
18
  execution_context
20
19
  ).result
20
+ rescue GraphQL::InvalidNullError => err
21
+ err.parent_error? || execution_context.add_error(err)
22
+ nil
21
23
  end
22
24
  end
23
25
  end
@@ -2,124 +2,39 @@ module GraphQL
2
2
  class Query
3
3
  class SerialExecution
4
4
  class SelectionResolution
5
- attr_reader :target, :type, :selections, :execution_context
5
+ attr_reader :target, :type, :irep_node, :execution_context
6
6
 
7
- def initialize(target, type, selections, execution_context)
7
+ def initialize(target, type, irep_node, execution_context)
8
8
  @target = target
9
9
  @type = type
10
- @selections = selections
10
+ @irep_node = irep_node
11
11
  @execution_context = execution_context
12
12
  end
13
13
 
14
14
  def result
15
- flatten_and_merge_selections(selections)
16
- .values
17
- .reduce({}) { |result, ast_node|
18
- result.merge(resolve_field(ast_node))
19
- }
20
- rescue GraphQL::InvalidNullError => err
21
- err.parent_error? || execution_context.add_error(err)
22
- nil
23
- end
24
-
25
- private
26
-
27
- def flatten_selection(ast_node)
28
- strategy_method = STRATEGIES[ast_node.class]
29
- send(strategy_method, ast_node)
30
- end
31
-
32
- STRATEGIES = {
33
- GraphQL::Language::Nodes::Field => :flatten_field,
34
- GraphQL::Language::Nodes::InlineFragment => :flatten_inline_fragment,
35
- GraphQL::Language::Nodes::FragmentSpread => :flatten_fragment_spread,
36
- }
37
-
38
- def flatten_field(ast_node)
39
- result_name = ast_node.alias || ast_node.name
40
- { result_name => ast_node }
41
- end
42
-
43
- def flatten_inline_fragment(ast_node)
44
- return {} unless GraphQL::Query::DirectiveResolution.include_node?(ast_node, execution_context.query)
45
- flatten_fragment(ast_node)
46
- end
47
-
48
- def flatten_fragment_spread(ast_node)
49
- return {} unless GraphQL::Query::DirectiveResolution.include_node?(ast_node, execution_context.query)
50
- ast_fragment_defn = execution_context.get_fragment(ast_node.name)
51
- flatten_fragment(ast_fragment_defn)
52
- end
53
-
54
- def flatten_fragment(ast_fragment)
55
- if fragment_type_can_apply?(ast_fragment)
56
- flatten_and_merge_selections(ast_fragment.selections)
57
- else
58
- {}
59
- end
60
- end
61
-
62
- def fragment_type_can_apply?(ast_fragment)
63
- if ast_fragment.type.nil?
64
- true
65
- else
66
- child_type = execution_context.get_type(ast_fragment.type)
67
- resolved_type = GraphQL::Query::TypeResolver.new(target, child_type, type, execution_context.query.context).type
68
- !resolved_type.nil?
69
- end
70
- end
71
-
72
- def merge_fields(field1, field2)
73
- field_type = execution_context.get_field(type, field2.name).type.unwrap
74
-
75
- if field_type.kind.fields?
76
- # create a new ast field node merging selections from each field.
77
- # Because of static validation, we can assume that name, alias,
78
- # arguments, and directives are exactly the same for fields 1 and 2.
79
- GraphQL::Language::Nodes::Field.new(
80
- name: field2.name,
81
- alias: field2.alias,
82
- arguments: field2.arguments,
83
- directives: field2.directives,
84
- selections: field1.selections + field2.selections
85
- )
86
- else
87
- field2
15
+ irep_node.children.each_with_object({}) do |(name, irep_node), memo|
16
+ if included_by_directives?(irep_node, execution_context.query) && applies_to_type?(irep_node, type, target)
17
+ field_result = execution_context.strategy.field_resolution.new(
18
+ irep_node,
19
+ type,
20
+ target,
21
+ execution_context
22
+ ).result
23
+ memo.merge!(field_result)
24
+ end
88
25
  end
89
26
  end
90
27
 
91
- def resolve_field(ast_node)
92
- return {} unless GraphQL::Query::DirectiveResolution.include_node?(ast_node, execution_context.query)
93
- execution_context.strategy.field_resolution.new(
94
- ast_node,
95
- type,
96
- target,
97
- execution_context
98
- ).result
99
- end
100
-
101
- def merge_into_result(memo, selection)
102
- name = if selection.respond_to?(:alias)
103
- selection.alias || selection.name
104
- else
105
- selection.name
106
- end
28
+ private
107
29
 
108
- memo[name] = if memo.key?(name)
109
- merge_fields(memo[name], selection)
110
- else
111
- selection
112
- end
30
+ def included_by_directives?(irep_node, query)
31
+ GraphQL::Query::DirectiveResolution.include_node?(irep_node, query)
113
32
  end
114
33
 
115
- def flatten_and_merge_selections(selections)
116
- selections.reduce({}) do |result, ast_node|
117
- flattened_selections = flatten_selection(ast_node)
118
- flattened_selections.each do |name, selection|
119
- merge_into_result(result, selection)
120
- end
121
- result
122
- end
34
+ def applies_to_type?(irep_node, type, target)
35
+ irep_node.on_types.any? { |child_type|
36
+ GraphQL::Query::TypeResolver.new(target, child_type, type, execution_context.query.context).type
37
+ }
123
38
  end
124
39
  end
125
40
  end
@@ -8,13 +8,13 @@ module GraphQL
8
8
 
9
9
  class BaseResolution
10
10
  attr_reader :value, :field_type, :target, :parent_type,
11
- :ast_field, :execution_context
12
- def initialize(value, field_type, target, parent_type, ast_field, execution_context)
11
+ :irep_node, :execution_context
12
+ def initialize(value, field_type, target, parent_type, irep_node, execution_context)
13
13
  @value = value
14
14
  @field_type = field_type
15
15
  @target = target
16
16
  @parent_type = parent_type
17
- @ast_field = ast_field
17
+ @irep_node = irep_node
18
18
  @execution_context = execution_context
19
19
  end
20
20
 
@@ -46,7 +46,7 @@ module GraphQL
46
46
  wrapped_type = field_type.of_type
47
47
  strategy_class = get_strategy_for_kind(wrapped_type.kind)
48
48
  value.map do |item|
49
- inner_strategy = strategy_class.new(item, wrapped_type, target, parent_type, ast_field, execution_context)
49
+ inner_strategy = strategy_class.new(item, wrapped_type, target, parent_type, irep_node, execution_context)
50
50
  inner_strategy.result
51
51
  end
52
52
  end
@@ -57,11 +57,11 @@ module GraphQL
57
57
  resolved_type = field_type.resolve_type(value, execution_context)
58
58
 
59
59
  unless resolved_type.is_a?(GraphQL::ObjectType)
60
- raise GraphQL::ObjectType::UnresolvedTypeError.new(ast_field.name, field_type, parent_type)
60
+ raise GraphQL::ObjectType::UnresolvedTypeError.new(irep_node.definition.name, field_type, parent_type)
61
61
  end
62
62
 
63
63
  strategy_class = get_strategy_for_kind(resolved_type.kind)
64
- inner_strategy = strategy_class.new(value, resolved_type, target, parent_type, ast_field, execution_context)
64
+ inner_strategy = strategy_class.new(value, resolved_type, target, parent_type, irep_node, execution_context)
65
65
  inner_strategy.result
66
66
  end
67
67
  end
@@ -72,7 +72,7 @@ module GraphQL
72
72
  execution_context.strategy.selection_resolution.new(
73
73
  value,
74
74
  field_type,
75
- ast_field.selections,
75
+ irep_node,
76
76
  execution_context
77
77
  ).result
78
78
  end
@@ -81,11 +81,14 @@ module GraphQL
81
81
  class NonNullResolution < BaseResolution
82
82
  # Get the "wrapped" type and resolve the value according to that type
83
83
  def result
84
- raise GraphQL::InvalidNullError.new(ast_field.name, value) if value.nil? || value.is_a?(GraphQL::ExecutionError)
85
- wrapped_type = field_type.of_type
86
- strategy_class = get_strategy_for_kind(wrapped_type.kind)
87
- inner_strategy = strategy_class.new(value, wrapped_type, target, parent_type, ast_field, execution_context)
88
- inner_strategy.result
84
+ if value.nil? || value.is_a?(GraphQL::ExecutionError)
85
+ raise GraphQL::InvalidNullError.new(irep_node.definition.name, value)
86
+ else
87
+ wrapped_type = field_type.of_type
88
+ strategy_class = get_strategy_for_kind(wrapped_type.kind)
89
+ inner_strategy = strategy_class.new(value, wrapped_type, target, parent_type, irep_node, execution_context)
90
+ inner_strategy.result
91
+ end
89
92
  end
90
93
  end
91
94
 
@@ -4,6 +4,7 @@ require "graphql/schema/middleware_chain"
4
4
  require "graphql/schema/rescue_middleware"
5
5
  require "graphql/schema/possible_types"
6
6
  require "graphql/schema/reduce_types"
7
+ require "graphql/schema/timeout_middleware"
7
8
  require "graphql/schema/type_expression"
8
9
  require "graphql/schema/type_map"
9
10
  require "graphql/schema/validation"
@@ -16,8 +17,10 @@ module GraphQL
16
17
  DIRECTIVES = [GraphQL::Directive::SkipDirective, GraphQL::Directive::IncludeDirective]
17
18
  DYNAMIC_FIELDS = ["__type", "__typename", "__schema"]
18
19
 
19
- attr_reader :query, :mutation, :subscription, :directives, :static_validator
20
+ attr_reader :query, :mutation, :subscription, :directives, :static_validator, :query_analyzers
20
21
  attr_accessor :max_depth
22
+ attr_accessor :max_complexity
23
+
21
24
  # Override these if you don't want the default executor:
22
25
  attr_accessor :query_execution_strategy,
23
26
  :mutation_execution_strategy,
@@ -31,16 +34,18 @@ module GraphQL
31
34
  # @param subscription [GraphQL::ObjectType] the subscription root for the schema
32
35
  # @param max_depth [Integer] maximum query nesting (if it's greater, raise an error)
33
36
  # @param types [Array<GraphQL::BaseType>] additional types to include in this schema
34
- def initialize(query:, mutation: nil, subscription: nil, max_depth: nil, types: [])
37
+ def initialize(query:, mutation: nil, subscription: nil, max_depth: nil, max_complexity: nil, types: [])
35
38
  @query = query
36
39
  @mutation = mutation
37
40
  @subscription = subscription
38
41
  @max_depth = max_depth
42
+ @max_complexity = max_complexity
39
43
  @orphan_types = types
40
44
  @directives = DIRECTIVES.reduce({}) { |m, d| m[d.name] = d; m }
41
45
  @static_validator = GraphQL::StaticValidation::Validator.new(schema: self)
42
46
  @rescue_middleware = GraphQL::Schema::RescueMiddleware.new
43
47
  @middleware = [@rescue_middleware]
48
+ @query_analyzers = []
44
49
  # Default to the built-in execution strategy:
45
50
  self.query_execution_strategy = GraphQL::Query::SerialExecution
46
51
  self.mutation_execution_strategy = GraphQL::Query::SerialExecution
@@ -0,0 +1,67 @@
1
+ module GraphQL
2
+ class Schema
3
+ # This middleware will stop resolving new fields after `max_seconds` have elapsed.
4
+ # After the time has passed, any remaining fields will be `nil`, with errors added
5
+ # to the `errors` key. Any already-resolved fields will be in the `data` key, so
6
+ # you'll get a partial response.
7
+ #
8
+ # You can provide a block which will be called with any timeout errors that occur.
9
+ #
10
+ # Note that this will stop a query _in between_ field resolutions, but
11
+ # it doesn't interrupt long-running `resolve` functions. Be sure to use
12
+ # timeout options for external connections. For more info, see
13
+ # www.mikeperham.com/2015/05/08/timeout-rubys-most-dangerous-api/
14
+ #
15
+ # @example Stop resolving fields after 2 seconds
16
+ # MySchema.middleware << GraphQL::Schema::TimeoutMiddleware.new(max_seconds: 2)
17
+ #
18
+ # @example Notifying Bugsnag on a timeout
19
+ # MySchema.middleware << GraphQL::Schema::TimeoutMiddleware(max_seconds: 1.5) do |timeout_error, query|
20
+ # Bugsnag.notify(timeout_error, {query_string: query_ctx.query.query_string})
21
+ # end
22
+ #
23
+ class TimeoutMiddleware
24
+ # This key is used for storing timeout data in the {Query::Context} instance
25
+ DEFAULT_CONTEXT_KEY = :__timeout_at__
26
+ # @param max_seconds [Numeric] how many seconds the query should be allowed to resolve new fields
27
+ # @param context_key [Symbol] what key should be used to read and write to the query context
28
+ def initialize(max_seconds:, context_key: DEFAULT_CONTEXT_KEY, &block)
29
+ @max_seconds = max_seconds
30
+ @context_key = context_key
31
+ @error_handler = block
32
+ end
33
+
34
+ def call(parent_type, parent_object, field_definition, field_args, query_context, next_middleware)
35
+ timeout_at = query_context[@context_key] ||= Time.now + @max_seconds
36
+ if timeout_at < Time.now
37
+ on_timeout(parent_type, parent_object, field_definition, field_args, query_context)
38
+ else
39
+ next_middleware.call
40
+ end
41
+ end
42
+
43
+ # This is called when a field _would_ be resolved, except that we're over the time limit.
44
+ # @return [GraphQL::Schema::TimeoutMiddleware::TimeoutError] An error whose message will be added to the `errors` key
45
+ def on_timeout(parent_type, parent_object, field_definition, field_args, query_context)
46
+ err = GraphQL::Schema::TimeoutMiddleware::TimeoutError.new(parent_type, field_definition)
47
+ if @error_handler
48
+ @error_handler.call(err, query_context.query)
49
+ end
50
+ err
51
+ end
52
+ end
53
+
54
+ # This error is raised when a query exceeds `max_seconds`.
55
+ # Since it's a child of {GraphQL::ExecutionError},
56
+ # its message will be added to the response's `errors` key.
57
+ #
58
+ # To raise an error that will stop query resolution, use a custom block
59
+ # to take this error and raise a new one which _doesn't_ descend from {GraphQL::ExecutionError},
60
+ # such as `RuntimeError`.
61
+ class GraphQL::Schema::TimeoutMiddleware::TimeoutError < GraphQL::ExecutionError
62
+ def initialize(parent_type, field_defn)
63
+ super("Timeout on #{parent_type.name}.#{field_defn.name}")
64
+ end
65
+ end
66
+ end
67
+ end