graphql 0.18.13 → 0.18.14
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/graphql/analysis.rb +1 -0
- data/lib/graphql/analysis/analyze_query.rb +11 -34
- data/lib/graphql/analysis/reducer_state.rb +47 -0
- data/lib/graphql/execution_error.rb +5 -0
- data/lib/graphql/query.rb +4 -1
- data/lib/graphql/static_validation/rules/fields_will_merge.rb +10 -17
- data/lib/graphql/version.rb +1 -1
- data/spec/graphql/analysis/analyze_query_spec.rb +100 -4
- data/spec/graphql/static_validation/rules/fields_will_merge_spec.rb +0 -2
- data/spec/graphql/static_validation/rules/fragments_are_finite_spec.rb +3 -1
- data/spec/graphql/static_validation/validator_spec.rb +31 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d8692b90c4aa941fe5a1139719790eb1ba55c92e
|
4
|
+
data.tar.gz: 84c924ce93a593f26a7419f9ed69e5220380201c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 26a2968e15530d07b78b293f41bc103fdc3026036de39fd84e0f48434659d23558d806e67b7d07288cd5eb9b6f08cfbb06e76d809b865d35c70355cd7ad51ae7
|
7
|
+
data.tar.gz: 9c3401de8f427e468db33e2403b729ec5ccc291b53e95d8d853ef86e28744cfec291c5df778f34c8497149d82f4aa3054ddb0ff8059516e973f0fbc2441e999d
|
data/lib/graphql/analysis.rb
CHANGED
@@ -2,5 +2,6 @@ require "graphql/analysis/max_query_complexity"
|
|
2
2
|
require "graphql/analysis/max_query_depth"
|
3
3
|
require "graphql/analysis/query_complexity"
|
4
4
|
require "graphql/analysis/query_depth"
|
5
|
+
require "graphql/analysis/reducer_state"
|
5
6
|
require "graphql/analysis/analyze_query"
|
6
7
|
require "graphql/analysis/field_usage"
|
@@ -13,15 +13,15 @@ module GraphQL
|
|
13
13
|
# @param analyzers [Array<#call>] Objects that respond to `#call(memo, visit_type, irep_node)`
|
14
14
|
# @return [Array<Any>] Results from those analyzers
|
15
15
|
def analyze_query(query, analyzers)
|
16
|
-
|
16
|
+
reducer_states = analyzers.map { |r| ReducerState.new(r, query) }
|
17
17
|
|
18
18
|
irep = query.internal_representation
|
19
19
|
|
20
20
|
irep.each do |name, op_node|
|
21
|
-
reduce_node(op_node,
|
21
|
+
reduce_node(op_node, reducer_states)
|
22
22
|
end
|
23
23
|
|
24
|
-
|
24
|
+
reducer_states.map { |r| r.finalize_reducer }
|
25
25
|
end
|
26
26
|
|
27
27
|
private
|
@@ -29,44 +29,21 @@ module GraphQL
|
|
29
29
|
module_function
|
30
30
|
|
31
31
|
# Enter the node, visit its children, then leave the node.
|
32
|
-
def reduce_node(irep_node,
|
33
|
-
visit_analyzers(:enter, irep_node,
|
32
|
+
def reduce_node(irep_node, reducer_states)
|
33
|
+
visit_analyzers(:enter, irep_node, reducer_states)
|
34
34
|
|
35
35
|
irep_node.children.each do |name, child_irep_node|
|
36
|
-
reduce_node(child_irep_node,
|
36
|
+
reduce_node(child_irep_node, reducer_states)
|
37
37
|
end
|
38
38
|
|
39
|
-
visit_analyzers(:leave, irep_node,
|
39
|
+
visit_analyzers(:leave, irep_node, reducer_states)
|
40
40
|
end
|
41
41
|
|
42
|
-
def visit_analyzers(visit_type, irep_node,
|
43
|
-
|
44
|
-
|
45
|
-
memo = reducer_and_value[1]
|
46
|
-
next_memo = reducer.call(memo, visit_type, irep_node)
|
47
|
-
reducer_and_value[1] = next_memo
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
# If the reducer has an `initial_value` method, call it and store
|
52
|
-
# the result as `memo`. Otherwise, use `nil` as memo.
|
53
|
-
# @return [Array<(#call, Any)>] reducer-memo pairs
|
54
|
-
def initialize_reducer(reducer, query)
|
55
|
-
if reducer.respond_to?(:initial_value)
|
56
|
-
[reducer, reducer.initial_value(query)]
|
57
|
-
else
|
58
|
-
[reducer, nil]
|
59
|
-
end
|
60
|
-
end
|
42
|
+
def visit_analyzers(visit_type, irep_node, reducer_states)
|
43
|
+
reducer_states.each do |reducer_state|
|
44
|
+
next_memo = reducer_state.call(visit_type, irep_node)
|
61
45
|
|
62
|
-
|
63
|
-
# Otherwise, use the last value from the traversal.
|
64
|
-
# @return [Any] final memo value
|
65
|
-
def finalize_reducer(reducer, reduced_value)
|
66
|
-
if reducer.respond_to?(:final_value)
|
67
|
-
reducer.final_value(reduced_value)
|
68
|
-
else
|
69
|
-
reduced_value
|
46
|
+
reducer_state.memo = next_memo
|
70
47
|
end
|
71
48
|
end
|
72
49
|
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module GraphQL
|
2
|
+
module Analysis
|
3
|
+
class ReducerState
|
4
|
+
attr_reader :reducer
|
5
|
+
attr_accessor :memo, :errors
|
6
|
+
|
7
|
+
def initialize(reducer, query)
|
8
|
+
@reducer = reducer
|
9
|
+
@memo = initialize_reducer(reducer, query)
|
10
|
+
@errors = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(visit_type, irep_node)
|
14
|
+
@memo = @reducer.call(@memo, visit_type, irep_node)
|
15
|
+
rescue AnalysisError => err
|
16
|
+
@errors << err
|
17
|
+
end
|
18
|
+
|
19
|
+
# Respond with any errors, if found. Otherwise, if the reducer accepts
|
20
|
+
# `final_value`, send it the last memo value.
|
21
|
+
# Otherwise, use the last value from the traversal.
|
22
|
+
# @return [Any] final memo value
|
23
|
+
def finalize_reducer
|
24
|
+
if @errors.any?
|
25
|
+
@errors
|
26
|
+
elsif reducer.respond_to?(:final_value)
|
27
|
+
reducer.final_value(@memo)
|
28
|
+
else
|
29
|
+
@memo
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
# If the reducer has an `initial_value` method, call it and store
|
36
|
+
# the result as `memo`. Otherwise, use `nil` as memo.
|
37
|
+
# @return [Any] initial memo value
|
38
|
+
def initialize_reducer(reducer, query)
|
39
|
+
if reducer.respond_to?(:initial_value)
|
40
|
+
reducer.initial_value(query)
|
41
|
+
else
|
42
|
+
nil
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -6,6 +6,11 @@ module GraphQL
|
|
6
6
|
# @return [GraphQL::Language::Nodes::Field] the field where the error occured
|
7
7
|
attr_accessor :ast_node
|
8
8
|
|
9
|
+
def initialize(message, ast_node: nil)
|
10
|
+
@ast_node = ast_node
|
11
|
+
super(message)
|
12
|
+
end
|
13
|
+
|
9
14
|
# @return [Hash] An entry for the response's "errors" key
|
10
15
|
def to_h
|
11
16
|
hash = {
|
data/lib/graphql/query.rb
CHANGED
@@ -182,7 +182,10 @@ module GraphQL
|
|
182
182
|
@analysis_errors = begin
|
183
183
|
if @query_analyzers.any?
|
184
184
|
reduce_results = GraphQL::Analysis.analyze_query(self, @query_analyzers)
|
185
|
-
reduce_results
|
185
|
+
reduce_results
|
186
|
+
.flatten # accept n-dimensional array
|
187
|
+
.select { |r| r.is_a?(GraphQL::AnalysisError) }
|
188
|
+
.map(&:to_h)
|
186
189
|
else
|
187
190
|
[]
|
188
191
|
end
|
@@ -13,33 +13,34 @@ module GraphQL
|
|
13
13
|
visitor[GraphQL::Language::Nodes::Document].leave << -> (node, parent) {
|
14
14
|
has_selections.each { |node|
|
15
15
|
field_map = gather_fields_by_name(node.selections, {}, [], context)
|
16
|
-
find_conflicts(field_map, context)
|
16
|
+
find_conflicts(field_map, [], context)
|
17
17
|
}
|
18
18
|
}
|
19
19
|
end
|
20
20
|
|
21
21
|
private
|
22
22
|
|
23
|
-
def find_conflicts(field_map, context)
|
23
|
+
def find_conflicts(field_map, visited_fragments, context)
|
24
24
|
field_map.each do |name, ast_fields|
|
25
25
|
comparison = FieldDefinitionComparison.new(name, ast_fields, context)
|
26
26
|
context.errors.push(*comparison.errors)
|
27
27
|
|
28
28
|
|
29
29
|
subfield_map = {}
|
30
|
-
visited_fragments = []
|
31
30
|
ast_fields.each do |defn|
|
32
31
|
gather_fields_by_name(defn.selections, subfield_map, visited_fragments, context)
|
33
32
|
end
|
34
|
-
|
33
|
+
|
34
|
+
find_conflicts(subfield_map, visited_fragments, context)
|
35
35
|
end
|
36
36
|
end
|
37
37
|
|
38
38
|
def gather_fields_by_name(fields, field_map, visited_fragments, context)
|
39
39
|
fields.each do |field|
|
40
|
-
|
40
|
+
case field
|
41
|
+
when GraphQL::Language::Nodes::InlineFragment
|
41
42
|
next_fields = field.selections
|
42
|
-
|
43
|
+
when GraphQL::Language::Nodes::FragmentSpread
|
43
44
|
if visited_fragments.include?(field.name)
|
44
45
|
next
|
45
46
|
else
|
@@ -47,11 +48,13 @@ module GraphQL
|
|
47
48
|
end
|
48
49
|
fragment_defn = context.fragments[field.name]
|
49
50
|
next_fields = fragment_defn ? fragment_defn.selections : []
|
50
|
-
|
51
|
+
when GraphQL::Language::Nodes::Field
|
51
52
|
name_in_selection = field.alias || field.name
|
52
53
|
field_map[name_in_selection] ||= []
|
53
54
|
field_map[name_in_selection].push(field)
|
54
55
|
next_fields = []
|
56
|
+
else
|
57
|
+
raise "Unexpected field for merging: #{field}"
|
55
58
|
end
|
56
59
|
gather_fields_by_name(next_fields, field_map, visited_fragments, context)
|
57
60
|
end
|
@@ -76,16 +79,6 @@ module GraphQL
|
|
76
79
|
errors << message("Field '#{name}' has an argument conflict: #{args.map {|a| JSON.dump(a) }.join(" or ")}?", defs.first, context: context)
|
77
80
|
end
|
78
81
|
|
79
|
-
directive_names = defs.map { |defn| defn.directives.map(&:name) }.uniq
|
80
|
-
if directive_names.length != 1
|
81
|
-
errors << message("Field '#{name}' has a directive conflict: #{directive_names.map {|names| "[#{names.join(", ")}]"}.join(" or ")}?", defs.first, context: context)
|
82
|
-
end
|
83
|
-
|
84
|
-
directive_args = defs.map {|defn| defn.directives.map {|d| reduce_list(d.arguments) } }.uniq
|
85
|
-
if directive_args.length != 1
|
86
|
-
errors << message("Field '#{name}' has a directive argument conflict: #{directive_args.map {|args| JSON.dump(args)}.join(" or ")}?", defs.first, context: context)
|
87
|
-
end
|
88
|
-
|
89
82
|
@errors = errors
|
90
83
|
end
|
91
84
|
|
data/lib/graphql/version.rb
CHANGED
@@ -84,9 +84,11 @@ describe GraphQL::Analysis do
|
|
84
84
|
if irep_node.ast_node.is_a?(GraphQL::Language::Nodes::Field)
|
85
85
|
irep_node.definitions.each do |type_defn, field_defn|
|
86
86
|
if field_defn.resolve_proc.is_a?(GraphQL::Relay::ConnectionResolve)
|
87
|
-
memo[
|
87
|
+
memo[:connection] ||= 0
|
88
|
+
memo[:connection] += 1
|
88
89
|
else
|
89
|
-
memo[
|
90
|
+
memo[:field] ||= 0
|
91
|
+
memo[:field] += 1
|
90
92
|
end
|
91
93
|
end
|
92
94
|
end
|
@@ -109,11 +111,105 @@ describe GraphQL::Analysis do
|
|
109
111
|
it "knows which fields are connections" do
|
110
112
|
connection_counts = reduce_result.first
|
111
113
|
expected_connection_counts = {
|
112
|
-
|
113
|
-
|
114
|
+
:field => 5,
|
115
|
+
:connection => 2
|
114
116
|
}
|
115
117
|
assert_equal expected_connection_counts, connection_counts
|
116
118
|
end
|
117
119
|
end
|
118
120
|
end
|
121
|
+
|
122
|
+
describe ".visit_analyzers" do
|
123
|
+
class IdCatcher
|
124
|
+
def call(memo, visit_type, irep_node)
|
125
|
+
if visit_type == :enter
|
126
|
+
if irep_node.ast_node.name == "id"
|
127
|
+
raise GraphQL::AnalysisError.new("Don't use the id field.", ast_node: irep_node.ast_node)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
memo
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
class FlavorCatcher
|
135
|
+
def initial_value(query)
|
136
|
+
{
|
137
|
+
:errors => []
|
138
|
+
}
|
139
|
+
end
|
140
|
+
|
141
|
+
def call(memo, visit_type, irep_node)
|
142
|
+
if visit_type == :enter
|
143
|
+
if irep_node.ast_node.name == "flavor"
|
144
|
+
memo[:errors] << GraphQL::AnalysisError.new("Don't use the flavor field.", ast_node: irep_node.ast_node)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
memo
|
148
|
+
end
|
149
|
+
|
150
|
+
def final_value(memo)
|
151
|
+
memo[:errors]
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
let(:id_catcher) { IdCatcher.new }
|
156
|
+
let(:flavor_catcher) { FlavorCatcher.new }
|
157
|
+
let(:analyzers) { [id_catcher, flavor_catcher] }
|
158
|
+
let(:reduce_result) { GraphQL::Analysis.analyze_query(query, analyzers) }
|
159
|
+
let(:query) { GraphQL::Query.new(DummySchema, query_string) }
|
160
|
+
let(:query_string) {%|
|
161
|
+
{
|
162
|
+
cheese(id: 1) {
|
163
|
+
id
|
164
|
+
flavor
|
165
|
+
}
|
166
|
+
}
|
167
|
+
|}
|
168
|
+
let(:schema) { DummySchema }
|
169
|
+
let(:result) { schema.execute(query_string) }
|
170
|
+
let(:query_string) {%|
|
171
|
+
{
|
172
|
+
cheese(id: 1) {
|
173
|
+
id
|
174
|
+
flavor
|
175
|
+
}
|
176
|
+
}
|
177
|
+
|}
|
178
|
+
|
179
|
+
before do
|
180
|
+
@previous_query_analyzers = DummySchema.query_analyzers.dup
|
181
|
+
DummySchema.query_analyzers.clear
|
182
|
+
DummySchema.query_analyzers << id_catcher << flavor_catcher
|
183
|
+
end
|
184
|
+
|
185
|
+
after do
|
186
|
+
DummySchema.query_analyzers.clear
|
187
|
+
DummySchema.query_analyzers.push(*@previous_query_analyzers)
|
188
|
+
end
|
189
|
+
|
190
|
+
it "groups all errors together" do
|
191
|
+
data = result["data"]
|
192
|
+
errors = result["errors"]
|
193
|
+
|
194
|
+
id_error_hash = errors[0]
|
195
|
+
flavor_error_hash = errors[1]
|
196
|
+
|
197
|
+
id_error_response = {
|
198
|
+
"message"=>"Don't use the id field.",
|
199
|
+
"locations"=>[{"line"=>4, "column"=>11}]
|
200
|
+
}
|
201
|
+
flavor_error_response = {
|
202
|
+
"message"=>"Don't use the flavor field.",
|
203
|
+
"locations"=>[{"line"=>5, "column"=>11}]
|
204
|
+
}
|
205
|
+
|
206
|
+
assert_nil data
|
207
|
+
|
208
|
+
assert_equal id_error_response["message"], id_error_hash["message"]
|
209
|
+
assert_equal id_error_response["locations"], id_error_hash["locations"]
|
210
|
+
|
211
|
+
assert_equal flavor_error_response["message"], flavor_error_hash["message"]
|
212
|
+
assert_equal flavor_error_response["locations"], flavor_error_hash["locations"]
|
213
|
+
end
|
214
|
+
end
|
119
215
|
end
|
@@ -36,8 +36,6 @@ describe GraphQL::StaticValidation::FieldsWillMerge do
|
|
36
36
|
|
37
37
|
it "finds field naming conflicts" do
|
38
38
|
expected_errors = [
|
39
|
-
"Field 'id' has a directive conflict: [] or [someFlag]?", # different directives
|
40
|
-
"Field 'id' has a directive argument conflict: [] or [{}]?", # not sure this is a great way to handle it but here we are!
|
41
39
|
"Field 'nickname' has a field conflict: name or fatContent?", # alias conflict in query
|
42
40
|
"Field 'fatContent' has a field conflict: fatContent or name?", # alias/name conflict in query and fragment
|
43
41
|
"Field 'similarCheese' has an argument conflict: {\"source\":\"sourceVar\"} or {\"source\":\"SHEEP\"}?", # different arguments
|
@@ -48,6 +48,37 @@ describe GraphQL::StaticValidation::Validator do
|
|
48
48
|
it "handles infinite fragment spreads" do
|
49
49
|
assert_equal(1, errors.length)
|
50
50
|
end
|
51
|
+
|
52
|
+
describe "nested spreads" do
|
53
|
+
let(:query_string) {%|
|
54
|
+
{
|
55
|
+
allEdible {
|
56
|
+
... on Cheese {
|
57
|
+
... cheeseFields
|
58
|
+
}
|
59
|
+
}
|
60
|
+
}
|
61
|
+
|
62
|
+
fragment cheeseFields on Cheese {
|
63
|
+
similarCheese(source: COW) {
|
64
|
+
similarCheese(source: COW) {
|
65
|
+
... cheeseFields
|
66
|
+
}
|
67
|
+
}
|
68
|
+
}
|
69
|
+
|}
|
70
|
+
|
71
|
+
it "finds an error on the nested spread" do
|
72
|
+
expected = [
|
73
|
+
{
|
74
|
+
"message"=>"Fragment cheeseFields contains an infinite loop",
|
75
|
+
"locations"=>[{"line"=>10, "column"=>9}],
|
76
|
+
"path"=>["fragment cheeseFields"]
|
77
|
+
}
|
78
|
+
]
|
79
|
+
assert_equal(expected, errors)
|
80
|
+
end
|
81
|
+
end
|
51
82
|
end
|
52
83
|
|
53
84
|
describe "fragment spreads with no selections" do
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: graphql
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.18.
|
4
|
+
version: 0.18.14
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Robert Mosolgo
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-09-
|
11
|
+
date: 2016-09-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: codeclimate-test-reporter
|
@@ -251,6 +251,7 @@ files:
|
|
251
251
|
- lib/graphql/analysis/max_query_depth.rb
|
252
252
|
- lib/graphql/analysis/query_complexity.rb
|
253
253
|
- lib/graphql/analysis/query_depth.rb
|
254
|
+
- lib/graphql/analysis/reducer_state.rb
|
254
255
|
- lib/graphql/analysis_error.rb
|
255
256
|
- lib/graphql/argument.rb
|
256
257
|
- lib/graphql/base_type.rb
|