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.
- 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
|