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