graphql 0.18.13 → 0.18.14
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/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
|