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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4ede8bf16e041625d9150b4a02ca1203e5f77f67
4
- data.tar.gz: be963dcb88acc294fd826798a1d1e480d3abb8f1
3
+ metadata.gz: d8692b90c4aa941fe5a1139719790eb1ba55c92e
4
+ data.tar.gz: 84c924ce93a593f26a7419f9ed69e5220380201c
5
5
  SHA512:
6
- metadata.gz: 6e440fc7cc28cf50d1817e5a7bf9d04b3a4e0cb7fc85708f8b7820b1a239305494015e487fbfd3417e24cfe3198ee40507daa709e66267913569f08f7df94a4a
7
- data.tar.gz: c3f9943e6016f805f6f2340065ab703c30d96b8130be5369b6095814f6c68b248d4a586dda1a35d9554294ba449698ac18a0e4817b4402a103f69752efe481bc
6
+ metadata.gz: 26a2968e15530d07b78b293f41bc103fdc3026036de39fd84e0f48434659d23558d806e67b7d07288cd5eb9b6f08cfbb06e76d809b865d35c70355cd7ad51ae7
7
+ data.tar.gz: 9c3401de8f427e468db33e2403b729ec5ccc291b53e95d8d853ef86e28744cfec291c5df778f34c8497149d82f4aa3054ddb0ff8059516e973f0fbc2441e999d
@@ -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
- analyzers_and_values = analyzers.map { |r| initialize_reducer(r, query) }
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, analyzers_and_values)
21
+ reduce_node(op_node, reducer_states)
22
22
  end
23
23
 
24
- analyzers_and_values.map { |(r, value)| finalize_reducer(r, value) }
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, analyzers_and_values)
33
- visit_analyzers(:enter, irep_node, analyzers_and_values)
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, analyzers_and_values)
36
+ reduce_node(child_irep_node, reducer_states)
37
37
  end
38
38
 
39
- visit_analyzers(:leave, irep_node, analyzers_and_values)
39
+ visit_analyzers(:leave, irep_node, reducer_states)
40
40
  end
41
41
 
42
- def visit_analyzers(visit_type, irep_node, analyzers_and_values)
43
- analyzers_and_values.each do |reducer_and_value|
44
- reducer = reducer_and_value[0]
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
- # If the reducer accepts `final_value`, send it the last memo value.
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 = {
@@ -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.select { |r| r.is_a?(GraphQL::AnalysisError) }.map(&:to_h)
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
- find_conflicts(subfield_map, context)
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
- if field.is_a?(GraphQL::Language::Nodes::InlineFragment)
40
+ case field
41
+ when GraphQL::Language::Nodes::InlineFragment
41
42
  next_fields = field.selections
42
- elsif field.is_a?(GraphQL::Language::Nodes::FragmentSpread)
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
- else
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
 
@@ -1,3 +1,3 @@
1
1
  module GraphQL
2
- VERSION = "0.18.13"
2
+ VERSION = "0.18.14"
3
3
  end
@@ -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["connection"] += 1
87
+ memo[:connection] ||= 0
88
+ memo[:connection] += 1
88
89
  else
89
- memo["field"] += 1
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
- "field" => 5,
113
- "connection" => 2
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
@@ -20,7 +20,9 @@ describe GraphQL::StaticValidation::FragmentsAreFinite do
20
20
  fragment flavorField on Cheese {
21
21
  flavor,
22
22
  similarCheese {
23
- ... sourceField
23
+ ... on Cheese {
24
+ ... sourceField
25
+ }
24
26
  }
25
27
  }
26
28
  fragment idField on Cheese {
@@ -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.13
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-19 00:00:00.000000000 Z
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