graphql 1.4.4 → 1.4.5

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.
@@ -26,11 +26,21 @@ rule
26
26
  }
27
27
  )
28
28
  }
29
- | selection_set {
29
+ | LCURLY selection_list RCURLY {
30
30
  return make_node(
31
31
  :OperationDefinition, {
32
32
  operation_type: "query",
33
- selections: val[0],
33
+ selections: val[1],
34
+ position_source: val[0],
35
+ }
36
+ )
37
+ }
38
+ | LCURLY RCURLY {
39
+ return make_node(
40
+ :OperationDefinition, {
41
+ operation_type: "query",
42
+ selections: [],
43
+ position_source: val[0],
34
44
  }
35
45
  )
36
46
  }
@@ -14,8 +14,12 @@ module GraphQL
14
14
  extend Forwardable
15
15
 
16
16
  class OperationNameMissingError < GraphQL::ExecutionError
17
- def initialize(names)
18
- msg = "You must provide an operation name from: #{names.join(", ")}"
17
+ def initialize(name)
18
+ msg = if name.nil?
19
+ %|An operation name is required|
20
+ else
21
+ %|No operation named "#{name}"|
22
+ end
19
23
  super(msg)
20
24
  end
21
25
  end
@@ -84,7 +88,7 @@ module GraphQL
84
88
  if @operations.any?
85
89
  @selected_operation = find_operation(@operations, @operation_name)
86
90
  if @selected_operation.nil?
87
- @validation_errors << GraphQL::Query::OperationNameMissingError.new(@operations.keys)
91
+ @validation_errors << GraphQL::Query::OperationNameMissingError.new(@operation_name)
88
92
  else
89
93
  @ast_variables = @selected_operation.variables
90
94
  @mutation = @selected_operation.operation_type == "mutation"
@@ -239,12 +243,12 @@ module GraphQL
239
243
  end
240
244
 
241
245
  def find_operation(operations, operation_name)
242
- if operations.length == 1
246
+ if operation_name.nil? && operations.length == 1
243
247
  operations.values.first
244
- elsif operations.length == 0 || !operations.key?(operation_name)
248
+ elsif !operations.key?(operation_name)
245
249
  nil
246
250
  else
247
- operations[operation_name]
251
+ operations.fetch(operation_name)
248
252
  end
249
253
  end
250
254
 
@@ -28,6 +28,7 @@ module GraphQL
28
28
  GraphQL::StaticValidation::VariableUsagesAreAllowed,
29
29
  GraphQL::StaticValidation::MutationRootExists,
30
30
  GraphQL::StaticValidation::SubscriptionRootExists,
31
+ GraphQL::StaticValidation::OperationNamesAreValid,
31
32
  ]
32
33
  end
33
34
  end
@@ -9,25 +9,42 @@ module GraphQL
9
9
  def validate(context)
10
10
  context.visitor[GraphQL::Language::Nodes::Field] << ->(node, parent) {
11
11
  field_defn = context.field_definition
12
- validate_field_selections(node, field_defn, context)
12
+ validate_field_selections(node, field_defn.type.unwrap, context)
13
+ }
14
+
15
+ context.visitor[GraphQL::Language::Nodes::OperationDefinition] << ->(node, parent) {
16
+ validate_field_selections(node, context.type_definition, context)
13
17
  }
14
18
  end
15
19
 
16
20
  private
17
21
 
18
- def validate_field_selections(ast_field, field_defn, context)
19
- resolved_type = field_defn.type.unwrap
20
22
 
21
- if resolved_type.kind.scalar? && ast_field.selections.any?
22
- error = message("Selections can't be made on scalars (field '#{ast_field.name}' returns #{resolved_type.name} but has selections [#{ast_field.selections.map(&:name).join(", ")}])", ast_field, context: context)
23
- elsif resolved_type.kind.object? && ast_field.selections.none?
24
- error = message("Objects must have selections (field '#{ast_field.name}' returns #{resolved_type.name} but has no selections)", ast_field, context: context)
23
+ def validate_field_selections(ast_node, resolved_type, context)
24
+ msg = if resolved_type.nil?
25
+ nil
26
+ elsif resolved_type.kind.scalar? && ast_node.selections.any?
27
+ "Selections can't be made on scalars (%{node_name} returns #{resolved_type.name} but has selections [#{ast_node.selections.map(&:name).join(", ")}])"
28
+ elsif resolved_type.kind.object? && ast_node.selections.none?
29
+ "Objects must have selections (%{node_name} returns #{resolved_type.name} but has no selections)"
25
30
  else
26
- error = nil
31
+ nil
27
32
  end
28
33
 
29
- if !error.nil?
30
- context.errors << error
34
+ if msg
35
+ node_name = case ast_node
36
+ when GraphQL::Language::Nodes::Field
37
+ "field '#{ast_node.name}'"
38
+ when GraphQL::Language::Nodes::OperationDefinition
39
+ if ast_node.name.nil?
40
+ "anonymous query"
41
+ else
42
+ "#{ast_node.operation_type} '#{ast_node.name}'"
43
+ end
44
+ else
45
+ raise("Unexpected node #{ast_node}")
46
+ end
47
+ context.errors << message(msg % { node_name: node_name }, ast_node, context: context)
31
48
  GraphQL::Language::Visitor::SKIP
32
49
  end
33
50
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ module StaticValidation
4
+ class OperationNamesAreValid
5
+ include GraphQL::StaticValidation::Message::MessageHelper
6
+
7
+ def validate(context)
8
+ op_names = Hash.new { |h, k| h[k] = [] }
9
+
10
+ context.visitor[GraphQL::Language::Nodes::OperationDefinition].enter << -> (node, _parent) {
11
+ op_names[node.name] << node
12
+ }
13
+
14
+ context.visitor[GraphQL::Language::Nodes::Document].leave << -> (node, _parent) {
15
+ op_count = op_names.values.inject(0) { |m, v| m += v.size }
16
+
17
+ op_names.each do |name, nodes|
18
+ if name.nil? && op_count > 1
19
+ context.errors << message(%|Operation name is required when multiple operations are present|, nodes, context: context)
20
+ elsif nodes.length > 1
21
+ context.errors << message(%|Operation name "#{name}" must be unique|, nodes, context: context)
22
+ end
23
+ end
24
+ }
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphQL
3
- VERSION = "1.4.4"
3
+ VERSION = "1.4.5"
4
4
  end
@@ -3,3 +3,77 @@ require "spec_helper"
3
3
 
4
4
  ExecuteSuite = GraphQL::Compatibility::ExecutionSpecification.build_suite(GraphQL::Execution::Execute)
5
5
  LazyExecuteSuite = GraphQL::Compatibility::LazyExecutionSpecification.build_suite(GraphQL::Execution::Execute)
6
+
7
+ describe GraphQL::Execution::Execute do
8
+ describe "null propagation on mutation root" do
9
+ module MutationNullTestRoot
10
+ INTS = []
11
+ def self.push(args, ctx)
12
+ if args[:int] == 13
13
+ nil
14
+ else
15
+ INTS << args[:int]
16
+ args[:int]
17
+ end
18
+ end
19
+
20
+ def ints
21
+ INTS
22
+ end
23
+ end
24
+
25
+ let(:root) { MutationNullTestRoot }
26
+
27
+ let(:query_str) do
28
+ <<-GRAPHQL
29
+ mutation {
30
+ one: push(int: 1)
31
+ thirteen: push(int: 13)
32
+ two: push(int: 2)
33
+ }
34
+ GRAPHQL
35
+ end
36
+
37
+ before do
38
+ MutationNullTestRoot::INTS.clear
39
+ end
40
+
41
+ describe "when root fields are non-nullable" do
42
+ let(:schema) { GraphQL::Schema.from_definition <<-GRAPHQL
43
+ type Mutation {
44
+ push(int: Int!): Int!
45
+ }
46
+
47
+ type Query {
48
+ ints: [Int!]
49
+ }
50
+ GRAPHQL
51
+ }
52
+
53
+ it "propagates null to the root mutation and halts mutation execution" do
54
+ res = schema.execute(query_str, root_value: root)
55
+ assert_equal [1], MutationNullTestRoot::INTS
56
+ assert_equal(nil, res["data"])
57
+ end
58
+ end
59
+
60
+ describe 'mutation fields are nullable' do
61
+ let(:schema) { GraphQL::Schema.from_definition <<-GRAPHQL
62
+ type Mutation {
63
+ push(int: Int!): Int
64
+ }
65
+
66
+ type Query {
67
+ ints: [Int!]
68
+ }
69
+ GRAPHQL
70
+ }
71
+
72
+ it 'does not halt execution and returns data for the successful mutations' do
73
+ res = schema.execute(query_str, root_value: root)
74
+ assert_equal [1, 2], MutationNullTestRoot::INTS
75
+ assert_equal({"one"=>1, "thirteen"=>nil, "two"=>2}, res["data"])
76
+ end
77
+ end
78
+ end
79
+ end
@@ -38,7 +38,19 @@ describe GraphQL::Query::Executor do
38
38
  it "returns an error" do
39
39
  expected = {
40
40
  "errors" => [
41
- {"message" => "You must provide an operation name from: getCheese1, getCheese2"}
41
+ {"message" => "An operation name is required"}
42
+ ]
43
+ }
44
+ assert_equal(expected, result)
45
+ end
46
+ end
47
+
48
+ describe "when the named operation is not present" do
49
+ let(:operation_name) { "nonsenseOperation" }
50
+ it "returns an error" do
51
+ expected = {
52
+ "errors" => [
53
+ {"message" => 'No operation named "nonsenseOperation"'}
42
54
  ]
43
55
  }
44
56
  assert_equal(expected, result)
@@ -28,4 +28,18 @@ describe GraphQL::StaticValidation::FieldsHaveAppropriateSelections do
28
28
  }
29
29
  assert_includes(errors, selection_required_error, "finds objects without selections")
30
30
  end
31
+
32
+ describe "anonymous operations" do
33
+ let(:query_string) { "{ }" }
34
+ it "requires selections" do
35
+ assert_equal(1, errors.length)
36
+
37
+ selections_required_error = {
38
+ "message"=> "Objects must have selections (anonymous query returns Query but has no selections)",
39
+ "locations"=>[{"line"=>1, "column"=>1}],
40
+ "fields"=>["query"]
41
+ }
42
+ assert_includes(errors, selections_required_error)
43
+ end
44
+ end
31
45
  end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+ require "spec_helper"
3
+
4
+ describe GraphQL::StaticValidation::OperationNamesAreValid do
5
+ include StaticValidationHelpers
6
+
7
+ describe "when there are multiple operations" do
8
+ let(:query_string) { <<-GRAPHQL
9
+ query getCheese {
10
+ cheese(id: 1) { flavor }
11
+ }
12
+
13
+ {
14
+ cheese(id: 2) { flavor }
15
+ }
16
+
17
+ {
18
+ cheese(id: 3) { flavor }
19
+ }
20
+ GRAPHQL
21
+ }
22
+
23
+ it "must have operation names" do
24
+ assert_equal 1, errors.length
25
+ requires_name_error = {
26
+ "message"=>"Operation name is required when multiple operations are present",
27
+ "locations"=>[{"line"=>5, "column"=>5}, {"line"=>9, "column"=>5}],
28
+ "fields"=>[],
29
+ }
30
+ assert_includes(errors, requires_name_error)
31
+ end
32
+ end
33
+
34
+ describe "when there are only unnamed operations" do
35
+ let(:query_string) { <<-GRAPHQL
36
+ {
37
+ cheese(id: 2) { flavor }
38
+ }
39
+
40
+ {
41
+ cheese(id: 3) { flavor }
42
+ }
43
+ GRAPHQL
44
+ }
45
+
46
+ it "requires names" do
47
+ assert_equal 1, errors.length
48
+ requires_name_error = {
49
+ "message"=>"Operation name is required when multiple operations are present",
50
+ "locations"=>[{"line"=>1, "column"=>5}, {"line"=>5, "column"=>5}],
51
+ "fields"=>[],
52
+ }
53
+ assert_includes(errors, requires_name_error)
54
+ end
55
+ end
56
+
57
+ describe "when multiple operations have names" do
58
+ let(:query_string) { <<-GRAPHQL
59
+ query getCheese {
60
+ cheese(id: 1) { flavor }
61
+ }
62
+
63
+ query getCheese {
64
+ cheese(id: 2) { flavor }
65
+ }
66
+ GRAPHQL
67
+ }
68
+
69
+ it "must be unique" do
70
+ assert_equal 1, errors.length
71
+ name_uniqueness_error = {
72
+ "message"=>'Operation name "getCheese" must be unique',
73
+ "locations"=>[{"line"=>1, "column"=>5}, {"line"=>5, "column"=>5}],
74
+ "fields"=>[],
75
+ }
76
+ assert_includes(errors, name_uniqueness_error)
77
+ end
78
+ end
79
+ end
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: 1.4.4
4
+ version: 1.4.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Mosolgo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-02-17 00:00:00.000000000 Z
11
+ date: 2017-03-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: codeclimate-test-reporter
@@ -433,6 +433,7 @@ files:
433
433
  - lib/graphql/static_validation/rules/fragments_are_on_composite_types.rb
434
434
  - lib/graphql/static_validation/rules/fragments_are_used.rb
435
435
  - lib/graphql/static_validation/rules/mutation_root_exists.rb
436
+ - lib/graphql/static_validation/rules/operation_names_are_valid.rb
436
437
  - lib/graphql/static_validation/rules/required_arguments_are_present.rb
437
438
  - lib/graphql/static_validation/rules/subscription_root_exists.rb
438
439
  - lib/graphql/static_validation/rules/unique_directives_per_location.rb
@@ -535,6 +536,7 @@ files:
535
536
  - spec/graphql/static_validation/rules/fragments_are_on_composite_types_spec.rb
536
537
  - spec/graphql/static_validation/rules/fragments_are_used_spec.rb
537
538
  - spec/graphql/static_validation/rules/mutation_root_exists_spec.rb
539
+ - spec/graphql/static_validation/rules/operation_names_are_valid_spec.rb
538
540
  - spec/graphql/static_validation/rules/required_arguments_are_present_spec.rb
539
541
  - spec/graphql/static_validation/rules/subscription_root_exists_spec.rb
540
542
  - spec/graphql/static_validation/rules/unique_directives_per_location_spec.rb
@@ -553,7 +555,6 @@ files:
553
555
  - spec/support/star_wars/data.rb
554
556
  - spec/support/star_wars/schema.rb
555
557
  - spec/support/static_validation_helpers.rb
556
- - spec/tmp/app/graphql/types/bird_type.rb
557
558
  homepage: http://github.com/rmosolgo/graphql-ruby
558
559
  licenses:
559
560
  - MIT
@@ -665,6 +666,7 @@ test_files:
665
666
  - spec/graphql/static_validation/rules/fragments_are_on_composite_types_spec.rb
666
667
  - spec/graphql/static_validation/rules/fragments_are_used_spec.rb
667
668
  - spec/graphql/static_validation/rules/mutation_root_exists_spec.rb
669
+ - spec/graphql/static_validation/rules/operation_names_are_valid_spec.rb
668
670
  - spec/graphql/static_validation/rules/required_arguments_are_present_spec.rb
669
671
  - spec/graphql/static_validation/rules/subscription_root_exists_spec.rb
670
672
  - spec/graphql/static_validation/rules/unique_directives_per_location_spec.rb
@@ -683,4 +685,3 @@ test_files:
683
685
  - spec/support/star_wars/data.rb
684
686
  - spec/support/star_wars/schema.rb
685
687
  - spec/support/static_validation_helpers.rb
686
- - spec/tmp/app/graphql/types/bird_type.rb
@@ -1,5 +0,0 @@
1
- Types::BirdType = GraphQL::InterfaceType.define do
2
- name "Bird"
3
- field :wingspan, !types.Int
4
- field :foliage, types[Types::ColorType]
5
- end