graphql 0.15.3 → 0.16.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/graphql.rb +4 -1
- data/lib/graphql/analysis.rb +5 -0
- data/lib/graphql/analysis/analyze_query.rb +73 -0
- data/lib/graphql/analysis/max_query_complexity.rb +25 -0
- data/lib/graphql/analysis/max_query_depth.rb +25 -0
- data/lib/graphql/analysis/query_complexity.rb +122 -0
- data/lib/graphql/analysis/query_depth.rb +54 -0
- data/lib/graphql/analysis_error.rb +4 -0
- data/lib/graphql/base_type.rb +7 -0
- data/lib/graphql/define/assign_object_field.rb +2 -1
- data/lib/graphql/field.rb +25 -3
- data/lib/graphql/input_object_type.rb +1 -1
- data/lib/graphql/internal_representation.rb +2 -0
- data/lib/graphql/internal_representation/node.rb +81 -0
- data/lib/graphql/internal_representation/rewrite.rb +177 -0
- data/lib/graphql/language/visitor.rb +15 -9
- data/lib/graphql/object_type.rb +1 -1
- data/lib/graphql/query.rb +66 -7
- data/lib/graphql/query/context.rb +10 -3
- data/lib/graphql/query/directive_resolution.rb +5 -5
- data/lib/graphql/query/serial_execution.rb +5 -3
- data/lib/graphql/query/serial_execution/field_resolution.rb +22 -15
- data/lib/graphql/query/serial_execution/operation_resolution.rb +7 -5
- data/lib/graphql/query/serial_execution/selection_resolution.rb +20 -105
- data/lib/graphql/query/serial_execution/value_resolution.rb +15 -12
- data/lib/graphql/schema.rb +7 -2
- data/lib/graphql/schema/timeout_middleware.rb +67 -0
- data/lib/graphql/static_validation/all_rules.rb +0 -1
- data/lib/graphql/static_validation/type_stack.rb +7 -11
- data/lib/graphql/static_validation/validation_context.rb +11 -1
- data/lib/graphql/static_validation/validator.rb +14 -4
- data/lib/graphql/version.rb +1 -1
- data/readme.md +10 -9
- data/spec/graphql/analysis/analyze_query_spec.rb +50 -0
- data/spec/graphql/analysis/max_query_complexity_spec.rb +62 -0
- data/spec/graphql/{static_validation/rules/document_does_not_exceed_max_depth_spec.rb → analysis/max_query_depth_spec.rb} +20 -21
- data/spec/graphql/analysis/query_complexity_spec.rb +235 -0
- data/spec/graphql/analysis/query_depth_spec.rb +80 -0
- data/spec/graphql/directive_spec.rb +1 -0
- data/spec/graphql/internal_representation/rewrite_spec.rb +120 -0
- data/spec/graphql/introspection/schema_type_spec.rb +1 -0
- data/spec/graphql/language/visitor_spec.rb +14 -4
- data/spec/graphql/non_null_type_spec.rb +31 -0
- data/spec/graphql/query/context_spec.rb +24 -1
- data/spec/graphql/query_spec.rb +6 -2
- data/spec/graphql/schema/timeout_middleware_spec.rb +180 -0
- data/spec/graphql/static_validation/rules/argument_literals_are_compatible_spec.rb +1 -1
- data/spec/graphql/static_validation/rules/arguments_are_defined_spec.rb +1 -1
- data/spec/graphql/static_validation/rules/directives_are_defined_spec.rb +1 -1
- data/spec/graphql/static_validation/rules/directives_are_in_valid_locations_spec.rb +1 -1
- data/spec/graphql/static_validation/rules/fields_are_defined_on_type_spec.rb +1 -1
- data/spec/graphql/static_validation/rules/fields_have_appropriate_selections_spec.rb +1 -1
- data/spec/graphql/static_validation/rules/fields_will_merge_spec.rb +1 -1
- data/spec/graphql/static_validation/rules/fragment_spreads_are_possible_spec.rb +1 -1
- data/spec/graphql/static_validation/rules/fragment_types_exist_spec.rb +1 -1
- data/spec/graphql/static_validation/rules/fragments_are_finite_spec.rb +1 -1
- data/spec/graphql/static_validation/rules/fragments_are_on_composite_types_spec.rb +1 -1
- data/spec/graphql/static_validation/rules/fragments_are_used_spec.rb +1 -1
- data/spec/graphql/static_validation/rules/required_arguments_are_present_spec.rb +1 -1
- data/spec/graphql/static_validation/rules/variable_default_values_are_correctly_typed_spec.rb +1 -1
- data/spec/graphql/static_validation/rules/variable_usages_are_allowed_spec.rb +1 -1
- data/spec/graphql/static_validation/rules/variables_are_input_types_spec.rb +1 -1
- data/spec/graphql/static_validation/rules/variables_are_used_and_defined_spec.rb +1 -1
- data/spec/graphql/static_validation/validator_spec.rb +1 -1
- data/spec/support/dairy_app.rb +22 -1
- metadata +29 -5
- 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
|
-
#
|
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?(
|
5
|
-
|
6
|
-
|
7
|
-
args =
|
8
|
-
if !
|
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,
|
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
|
-
|
16
|
+
irep_root,
|
15
17
|
root_type,
|
16
|
-
ExecutionContext.new(
|
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 :
|
5
|
+
attr_reader :irep_node, :parent_type, :target, :execution_context, :field, :arguments
|
6
6
|
|
7
|
-
def initialize(
|
8
|
-
@
|
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 =
|
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 =
|
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 =
|
25
|
-
|
26
|
-
|
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,
|
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, :
|
5
|
+
attr_reader :target, :execution_context, :irep_node
|
6
6
|
|
7
|
-
def initialize(
|
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
|
-
|
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, :
|
5
|
+
attr_reader :target, :type, :irep_node, :execution_context
|
6
6
|
|
7
|
-
def initialize(target, type,
|
7
|
+
def initialize(target, type, irep_node, execution_context)
|
8
8
|
@target = target
|
9
9
|
@type = type
|
10
|
-
@
|
10
|
+
@irep_node = irep_node
|
11
11
|
@execution_context = execution_context
|
12
12
|
end
|
13
13
|
|
14
14
|
def result
|
15
|
-
|
16
|
-
.
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
-
|
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
|
-
|
109
|
-
|
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
|
116
|
-
|
117
|
-
|
118
|
-
|
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
|
-
:
|
12
|
-
def initialize(value, field_type, target, parent_type,
|
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
|
-
@
|
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,
|
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(
|
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,
|
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
|
-
|
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
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
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
|
|
data/lib/graphql/schema.rb
CHANGED
@@ -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
|