graphql 1.10.4 → 1.10.5
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 +0 -1
- data/lib/graphql/analysis/ast/query_complexity.rb +117 -105
- data/lib/graphql/execution/lookahead.rb +21 -108
- data/lib/graphql/pagination/active_record_relation_connection.rb +7 -1
- data/lib/graphql/pagination/array_connection.rb +2 -2
- data/lib/graphql/pagination/connection.rb +17 -5
- data/lib/graphql/pagination/relation_connection.rb +3 -2
- data/lib/graphql/query/input_validation_result.rb +23 -6
- data/lib/graphql/query/variables.rb +1 -1
- data/lib/graphql/relay/array_connection.rb +8 -10
- data/lib/graphql/relay/base_connection.rb +19 -11
- data/lib/graphql/relay/relation_connection.rb +8 -10
- data/lib/graphql/scalar_type.rb +14 -1
- data/lib/graphql/schema.rb +6 -0
- data/lib/graphql/schema/build_from_definition.rb +3 -0
- data/lib/graphql/schema/scalar.rb +9 -1
- data/lib/graphql/static_validation/literal_validator.rb +47 -22
- data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +42 -87
- data/lib/graphql/static_validation/rules/argument_literals_are_compatible_error.rb +17 -5
- data/lib/graphql/static_validation/rules/arguments_are_defined.rb +25 -20
- data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +9 -9
- data/lib/graphql/static_validation/validation_context.rb +1 -1
- data/lib/graphql/version.rb +1 -1
- metadata +2 -3
- data/lib/graphql/literal_validation_error.rb +0 -6
@@ -6,12 +6,18 @@ module GraphQL
|
|
6
6
|
# Customizes `RelationConnection` to work with `ActiveRecord::Relation`s.
|
7
7
|
class ActiveRecordRelationConnection < Pagination::RelationConnection
|
8
8
|
def relation_count(relation)
|
9
|
-
if relation.respond_to?(:unscope)
|
9
|
+
int_or_hash = if relation.respond_to?(:unscope)
|
10
10
|
relation.unscope(:order).count(:all)
|
11
11
|
else
|
12
12
|
# Rails 3
|
13
13
|
relation.count
|
14
14
|
end
|
15
|
+
if int_or_hash.is_a?(Integer)
|
16
|
+
int_or_hash
|
17
|
+
else
|
18
|
+
# Grouped relations return count-by-group hashes
|
19
|
+
int_or_hash.length
|
20
|
+
end
|
15
21
|
end
|
16
22
|
|
17
23
|
def relation_limit(relation)
|
@@ -21,7 +21,7 @@ module GraphQL
|
|
21
21
|
|
22
22
|
def cursor_for(item)
|
23
23
|
idx = items.find_index(item) + 1
|
24
|
-
|
24
|
+
encode(idx.to_s)
|
25
25
|
end
|
26
26
|
|
27
27
|
private
|
@@ -59,7 +59,7 @@ module GraphQL
|
|
59
59
|
sliced_nodes.count > first
|
60
60
|
elsif before
|
61
61
|
# The original array is longer than the `before` index
|
62
|
-
index_from_cursor(before) < items.length
|
62
|
+
index_from_cursor(before) < items.length + 1
|
63
63
|
else
|
64
64
|
false
|
65
65
|
end
|
@@ -29,14 +29,22 @@ module GraphQL
|
|
29
29
|
# Raw access to client-provided values. (`max_page_size` not applied to first or last.)
|
30
30
|
attr_accessor :before_value, :after_value, :first_value, :last_value
|
31
31
|
|
32
|
-
# @return [String, nil] the client-provided cursor
|
32
|
+
# @return [String, nil] the client-provided cursor. `""` is treated as `nil`.
|
33
33
|
def before
|
34
|
-
@
|
34
|
+
if defined?(@before)
|
35
|
+
@before
|
36
|
+
else
|
37
|
+
@before = @before_value == "" ? nil : @before_value
|
38
|
+
end
|
35
39
|
end
|
36
40
|
|
37
|
-
# @return [String, nil] the client-provided cursor
|
41
|
+
# @return [String, nil] the client-provided cursor. `""` is treated as `nil`.
|
38
42
|
def after
|
39
|
-
@
|
43
|
+
if defined?(@after)
|
44
|
+
@after
|
45
|
+
else
|
46
|
+
@after = @after_value == "" ? nil : @after_value
|
47
|
+
end
|
40
48
|
end
|
41
49
|
|
42
50
|
# @param items [Object] some unpaginated collection item, like an `Array` or `ActiveRecord::Relation`
|
@@ -147,7 +155,11 @@ module GraphQL
|
|
147
155
|
end
|
148
156
|
|
149
157
|
def decode(cursor)
|
150
|
-
context.schema.cursor_encoder.decode(cursor)
|
158
|
+
context.schema.cursor_encoder.decode(cursor, nonce: true)
|
159
|
+
end
|
160
|
+
|
161
|
+
def encode(cursor)
|
162
|
+
context.schema.cursor_encoder.encode(cursor, nonce: true)
|
151
163
|
end
|
152
164
|
|
153
165
|
# A wrapper around paginated items. It includes a {cursor} for pagination
|
@@ -44,7 +44,7 @@ module GraphQL
|
|
44
44
|
load_nodes
|
45
45
|
# index in nodes + existing offset + 1 (because it's offset, not index)
|
46
46
|
offset = nodes.index(item) + 1 + (@paged_nodes_offset || 0) + (relation_offset(items) || 0)
|
47
|
-
|
47
|
+
encode(offset.to_s)
|
48
48
|
end
|
49
49
|
|
50
50
|
private
|
@@ -137,8 +137,9 @@ module GraphQL
|
|
137
137
|
def limited_nodes
|
138
138
|
@limited_nodes ||= begin
|
139
139
|
paginated_nodes = sliced_nodes
|
140
|
+
previous_limit = relation_limit(paginated_nodes)
|
140
141
|
|
141
|
-
if first && (
|
142
|
+
if first && (previous_limit.nil? || previous_limit > first)
|
142
143
|
# `first` would create a stricter limit that the one already applied, so add it
|
143
144
|
paginated_nodes = set_limit(paginated_nodes, first)
|
144
145
|
end
|
@@ -4,22 +4,39 @@ module GraphQL
|
|
4
4
|
class InputValidationResult
|
5
5
|
attr_accessor :problems
|
6
6
|
|
7
|
+
def initialize(valid: true, problems: nil)
|
8
|
+
@valid = valid
|
9
|
+
@problems = problems
|
10
|
+
end
|
11
|
+
|
7
12
|
def valid?
|
8
|
-
@
|
13
|
+
@valid
|
9
14
|
end
|
10
15
|
|
11
|
-
def add_problem(explanation, path = nil)
|
16
|
+
def add_problem(explanation, path = nil, extensions: nil, message: nil)
|
12
17
|
@problems ||= []
|
13
|
-
@
|
18
|
+
@valid = false
|
19
|
+
problem = { "path" => path || [], "explanation" => explanation }
|
20
|
+
if extensions
|
21
|
+
problem["extensions"] = extensions
|
22
|
+
end
|
23
|
+
if message
|
24
|
+
problem["message"] = message
|
25
|
+
end
|
26
|
+
@problems.push(problem)
|
14
27
|
end
|
15
28
|
|
16
29
|
def merge_result!(path, inner_result)
|
17
30
|
return if inner_result.valid?
|
18
31
|
|
19
|
-
inner_result.problems
|
20
|
-
|
21
|
-
|
32
|
+
if inner_result.problems
|
33
|
+
inner_result.problems.each do |p|
|
34
|
+
item_path = [path, *p["path"]]
|
35
|
+
add_problem(p["explanation"], item_path, message: p["message"], extensions: p["extensions"])
|
36
|
+
end
|
22
37
|
end
|
38
|
+
# It could have been explicitly set on inner_result (if it had no problems)
|
39
|
+
@valid = false
|
23
40
|
end
|
24
41
|
end
|
25
42
|
end
|
@@ -52,7 +52,7 @@ module GraphQL
|
|
52
52
|
end
|
53
53
|
end
|
54
54
|
end
|
55
|
-
rescue GraphQL::
|
55
|
+
rescue GraphQL::ExecutionError => ex
|
56
56
|
# TODO: This should really include the path to the problematic node in the variable value
|
57
57
|
# like InputValidationResults generated by validate_non_null_input but unfortunately we don't
|
58
58
|
# have this information available in the coerce_input call chain. Note this path is the path
|
@@ -34,19 +34,17 @@ module GraphQL
|
|
34
34
|
private
|
35
35
|
|
36
36
|
def first
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
37
|
+
@first ||= begin
|
38
|
+
capped = limit_pagination_argument(arguments[:first], max_page_size)
|
39
|
+
if capped.nil? && last.nil?
|
40
|
+
capped = max_page_size
|
41
|
+
end
|
42
|
+
capped
|
43
|
+
end
|
42
44
|
end
|
43
45
|
|
44
46
|
def last
|
45
|
-
|
46
|
-
|
47
|
-
@last = get_limited_arg(:last)
|
48
|
-
@last = max_page_size if @last && max_page_size && @last > max_page_size
|
49
|
-
@last
|
47
|
+
@last ||= limit_pagination_argument(arguments[:last], max_page_size)
|
50
48
|
end
|
51
49
|
|
52
50
|
# apply first / last limit results
|
@@ -81,7 +81,13 @@ module GraphQL
|
|
81
81
|
# The value passed as `first:`, if there was one. Negative numbers become `0`.
|
82
82
|
# @return [Integer, nil]
|
83
83
|
def first
|
84
|
-
@first ||=
|
84
|
+
@first ||= begin
|
85
|
+
capped = limit_pagination_argument(arguments[:first], max_page_size)
|
86
|
+
if capped.nil? && last.nil?
|
87
|
+
capped = max_page_size
|
88
|
+
end
|
89
|
+
capped
|
90
|
+
end
|
85
91
|
end
|
86
92
|
|
87
93
|
# The value passed as `after:`, if there was one
|
@@ -93,7 +99,7 @@ module GraphQL
|
|
93
99
|
# The value passed as `last:`, if there was one. Negative numbers become `0`.
|
94
100
|
# @return [Integer, nil]
|
95
101
|
def last
|
96
|
-
@last ||=
|
102
|
+
@last ||= limit_pagination_argument(arguments[:last], max_page_size)
|
97
103
|
end
|
98
104
|
|
99
105
|
# The value passed as `before:`, if there was one
|
@@ -152,16 +158,18 @@ module GraphQL
|
|
152
158
|
|
153
159
|
private
|
154
160
|
|
155
|
-
#
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
161
|
+
# @param argument [nil, Integer] `first` or `last`, as provided by the client
|
162
|
+
# @param max_page_size [nil, Integer]
|
163
|
+
# @return [nil, Integer] `nil` if the input was `nil`, otherwise a value between `0` and `max_page_size`
|
164
|
+
def limit_pagination_argument(argument, max_page_size)
|
165
|
+
if argument
|
166
|
+
if argument < 0
|
167
|
+
argument = 0
|
168
|
+
elsif max_page_size && argument > max_page_size
|
169
|
+
argument = max_page_size
|
170
|
+
end
|
164
171
|
end
|
172
|
+
argument
|
165
173
|
end
|
166
174
|
|
167
175
|
def paged_nodes
|
@@ -50,19 +50,17 @@ module GraphQL
|
|
50
50
|
end
|
51
51
|
|
52
52
|
def first
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
53
|
+
@first ||= begin
|
54
|
+
capped = limit_pagination_argument(arguments[:first], max_page_size)
|
55
|
+
if capped.nil? && last.nil?
|
56
|
+
capped = max_page_size
|
57
|
+
end
|
58
|
+
capped
|
59
|
+
end
|
58
60
|
end
|
59
61
|
|
60
62
|
def last
|
61
|
-
|
62
|
-
|
63
|
-
@last = get_limited_arg(:last)
|
64
|
-
@last = max_page_size if @last && max_page_size && @last > max_page_size
|
65
|
-
@last
|
63
|
+
@last ||= limit_pagination_argument(arguments[:last], max_page_size)
|
66
64
|
end
|
67
65
|
|
68
66
|
private
|
data/lib/graphql/scalar_type.rb
CHANGED
@@ -67,8 +67,21 @@ module GraphQL
|
|
67
67
|
|
68
68
|
def validate_non_null_input(value, ctx)
|
69
69
|
result = Query::InputValidationResult.new
|
70
|
-
|
70
|
+
|
71
|
+
coerced_result = begin
|
72
|
+
coerce_non_null_input(value, ctx)
|
73
|
+
rescue GraphQL::CoercionError => err
|
74
|
+
err
|
75
|
+
end
|
76
|
+
|
77
|
+
if value.is_a?(GraphQL::Language::Nodes::Enum) || coerced_result.nil?
|
71
78
|
result.add_problem("Could not coerce value #{GraphQL::Language.serialize(value)} to #{name}")
|
79
|
+
elsif coerced_result.is_a?(GraphQL::CoercionError)
|
80
|
+
result.add_problem(
|
81
|
+
coerced_result.message,
|
82
|
+
message: coerced_result.message,
|
83
|
+
extensions: coerced_result.extensions
|
84
|
+
)
|
72
85
|
end
|
73
86
|
result
|
74
87
|
end
|
data/lib/graphql/schema.rb
CHANGED
@@ -908,6 +908,7 @@ module GraphQL
|
|
908
908
|
schema_defn.cursor_encoder = cursor_encoder
|
909
909
|
schema_defn.tracers.concat(tracers)
|
910
910
|
schema_defn.query_analyzers.concat(query_analyzers)
|
911
|
+
schema_defn.analysis_engine = analysis_engine
|
911
912
|
|
912
913
|
schema_defn.middleware.concat(all_middleware)
|
913
914
|
schema_defn.multiplex_analyzers.concat(multiplex_analyzers)
|
@@ -934,6 +935,11 @@ module GraphQL
|
|
934
935
|
if !schema_defn.interpreter?
|
935
936
|
schema_defn.instrumenters[:query] << GraphQL::Schema::Member::Instrumentation
|
936
937
|
end
|
938
|
+
|
939
|
+
if new_connections?
|
940
|
+
schema_defn.connections = self.connections
|
941
|
+
end
|
942
|
+
|
937
943
|
schema_defn.send(:rebuild_artifacts)
|
938
944
|
|
939
945
|
schema_defn
|
@@ -43,13 +43,21 @@ module GraphQL
|
|
43
43
|
|
44
44
|
def validate_non_null_input(value, ctx)
|
45
45
|
result = Query::InputValidationResult.new
|
46
|
-
|
46
|
+
coerced_result = begin
|
47
|
+
coerce_input(value, ctx)
|
48
|
+
rescue GraphQL::CoercionError => err
|
49
|
+
err
|
50
|
+
end
|
51
|
+
|
52
|
+
if coerced_result.nil?
|
47
53
|
str_value = if value == Float::INFINITY
|
48
54
|
""
|
49
55
|
else
|
50
56
|
" #{GraphQL::Language.serialize(value)}"
|
51
57
|
end
|
52
58
|
result.add_problem("Could not coerce value#{str_value} to #{graphql_name}")
|
59
|
+
elsif coerced_result.is_a?(GraphQL::CoercionError)
|
60
|
+
result.add_problem(coerced_result.message, message: coerced_result.message, extensions: coerced_result.extensions)
|
53
61
|
end
|
54
62
|
result
|
55
63
|
end
|
@@ -6,59 +6,73 @@ module GraphQL
|
|
6
6
|
def initialize(context:)
|
7
7
|
@context = context
|
8
8
|
@warden = context.warden
|
9
|
+
@invalid_response = GraphQL::Query::InputValidationResult.new(valid: false, problems: [])
|
10
|
+
@valid_response = GraphQL::Query::InputValidationResult.new(valid: true, problems: [])
|
9
11
|
end
|
10
12
|
|
11
13
|
def validate(ast_value, type)
|
14
|
+
catch(:invalid) do
|
15
|
+
recursively_validate(ast_value, type)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def recursively_validate(ast_value, type)
|
12
22
|
if type.nil?
|
13
23
|
# this means we're an undefined argument, see #present_input_field_values_are_valid
|
14
24
|
maybe_raise_if_invalid(ast_value) do
|
15
|
-
|
25
|
+
@invalid_response
|
16
26
|
end
|
17
27
|
elsif ast_value.is_a?(GraphQL::Language::Nodes::NullValue)
|
18
28
|
maybe_raise_if_invalid(ast_value) do
|
19
|
-
|
29
|
+
type.kind.non_null? ? @invalid_response : @valid_response
|
20
30
|
end
|
21
31
|
elsif type.kind.non_null?
|
22
32
|
maybe_raise_if_invalid(ast_value) do
|
23
|
-
|
24
|
-
|
33
|
+
ast_value.nil? ?
|
34
|
+
@invalid_response :
|
35
|
+
recursively_validate(ast_value, type.of_type)
|
36
|
+
end
|
25
37
|
elsif type.kind.list?
|
26
38
|
item_type = type.of_type
|
27
|
-
ensure_array(ast_value).
|
39
|
+
results = ensure_array(ast_value).map { |val| recursively_validate(val, item_type) }
|
40
|
+
merge_results(results)
|
28
41
|
elsif ast_value.is_a?(GraphQL::Language::Nodes::VariableIdentifier)
|
29
|
-
|
42
|
+
@valid_response
|
30
43
|
elsif type.kind.scalar? && constant_scalar?(ast_value)
|
31
44
|
maybe_raise_if_invalid(ast_value) do
|
32
|
-
type.
|
45
|
+
type.validate_input(ast_value, @context)
|
33
46
|
end
|
34
47
|
elsif type.kind.enum?
|
35
48
|
maybe_raise_if_invalid(ast_value) do
|
36
49
|
if ast_value.is_a?(GraphQL::Language::Nodes::Enum)
|
37
|
-
type.
|
50
|
+
type.validate_input(ast_value.name, @context)
|
38
51
|
else
|
39
52
|
# if our ast_value isn't an Enum it's going to be invalid so return false
|
40
|
-
|
53
|
+
@invalid_response
|
41
54
|
end
|
42
55
|
end
|
43
56
|
elsif type.kind.input_object? && ast_value.is_a?(GraphQL::Language::Nodes::InputObject)
|
44
57
|
maybe_raise_if_invalid(ast_value) do
|
45
|
-
|
58
|
+
merge_results([
|
59
|
+
required_input_fields_are_present(type, ast_value),
|
60
|
+
present_input_field_values_are_valid(type, ast_value)
|
61
|
+
])
|
46
62
|
end
|
47
63
|
else
|
48
64
|
maybe_raise_if_invalid(ast_value) do
|
49
|
-
|
65
|
+
@invalid_response
|
50
66
|
end
|
51
67
|
end
|
52
68
|
end
|
53
69
|
|
54
|
-
|
55
|
-
|
70
|
+
# When `error_bubbling` is false, we want to bail on the first failure that we find.
|
71
|
+
# Use `throw` to escape the current call stack, returning the invalid response.
|
56
72
|
def maybe_raise_if_invalid(ast_value)
|
57
73
|
ret = yield
|
58
|
-
if !@context.schema.error_bubbling && !ret
|
59
|
-
|
60
|
-
e.ast_value = ast_value
|
61
|
-
raise e
|
74
|
+
if !@context.schema.error_bubbling && !ret.valid?
|
75
|
+
throw(:invalid, ret)
|
62
76
|
else
|
63
77
|
ret
|
64
78
|
end
|
@@ -87,28 +101,39 @@ module GraphQL
|
|
87
101
|
present_field_names = ast_node.arguments.map(&:name)
|
88
102
|
missing_required_field_names = required_field_names - present_field_names
|
89
103
|
if @context.schema.error_bubbling
|
90
|
-
missing_required_field_names.empty?
|
104
|
+
missing_required_field_names.empty? ? @valid_response : @invalid_response
|
91
105
|
else
|
92
|
-
missing_required_field_names.
|
93
|
-
|
106
|
+
results = missing_required_field_names.map do |name|
|
107
|
+
arg_type = @warden.arguments(type).find { |f| f.name == name }.type
|
108
|
+
recursively_validate(GraphQL::Language::Nodes::NullValue.new(name: name), arg_type)
|
94
109
|
end
|
110
|
+
merge_results(results)
|
95
111
|
end
|
96
112
|
end
|
97
113
|
|
98
114
|
def present_input_field_values_are_valid(type, ast_node)
|
99
115
|
field_map = @warden.arguments(type).reduce({}) { |m, f| m[f.name] = f; m}
|
100
|
-
ast_node.arguments.
|
116
|
+
results = ast_node.arguments.map do |value|
|
101
117
|
field = field_map[value.name]
|
102
118
|
# we want to call validate on an argument even if it's an invalid one
|
103
119
|
# so that our raise exception is on it instead of the entire InputObject
|
104
120
|
type = field && field.type
|
105
|
-
|
121
|
+
recursively_validate(value.value, type)
|
106
122
|
end
|
123
|
+
merge_results(results)
|
107
124
|
end
|
108
125
|
|
109
126
|
def ensure_array(value)
|
110
127
|
value.is_a?(Array) ? value : [value]
|
111
128
|
end
|
129
|
+
|
130
|
+
def merge_results(results_list)
|
131
|
+
merged_result = Query::InputValidationResult.new
|
132
|
+
results_list.each do |inner_result|
|
133
|
+
merged_result.merge_result!([], inner_result)
|
134
|
+
end
|
135
|
+
merged_result
|
136
|
+
end
|
112
137
|
end
|
113
138
|
end
|
114
139
|
end
|