graphql 0.15.3 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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