graphql 1.4.4 → 1.4.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/graphql/execution/execute.rb +7 -0
- data/lib/graphql/language/parser.rb +666 -654
- data/lib/graphql/language/parser.y +12 -2
- data/lib/graphql/query.rb +10 -6
- data/lib/graphql/static_validation/all_rules.rb +1 -0
- data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +27 -10
- data/lib/graphql/static_validation/rules/operation_names_are_valid.rb +28 -0
- data/lib/graphql/version.rb +1 -1
- data/spec/graphql/execution/execute_spec.rb +74 -0
- data/spec/graphql/query/executor_spec.rb +13 -1
- data/spec/graphql/static_validation/rules/fields_have_appropriate_selections_spec.rb +14 -0
- data/spec/graphql/static_validation/rules/operation_names_are_valid_spec.rb +79 -0
- metadata +5 -4
- data/spec/tmp/app/graphql/types/bird_type.rb +0 -5
@@ -26,11 +26,21 @@ rule
|
|
26
26
|
}
|
27
27
|
)
|
28
28
|
}
|
29
|
-
|
|
29
|
+
| LCURLY selection_list RCURLY {
|
30
30
|
return make_node(
|
31
31
|
:OperationDefinition, {
|
32
32
|
operation_type: "query",
|
33
|
-
selections: val[
|
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
|
}
|
data/lib/graphql/query.rb
CHANGED
@@ -14,8 +14,12 @@ module GraphQL
|
|
14
14
|
extend Forwardable
|
15
15
|
|
16
16
|
class OperationNameMissingError < GraphQL::ExecutionError
|
17
|
-
def initialize(
|
18
|
-
msg =
|
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(@
|
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
|
248
|
+
elsif !operations.key?(operation_name)
|
245
249
|
nil
|
246
250
|
else
|
247
|
-
operations
|
251
|
+
operations.fetch(operation_name)
|
248
252
|
end
|
249
253
|
end
|
250
254
|
|
@@ -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
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
-
|
31
|
+
nil
|
27
32
|
end
|
28
33
|
|
29
|
-
if
|
30
|
-
|
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
|
data/lib/graphql/version.rb
CHANGED
@@ -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" => "
|
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
|
+
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-
|
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
|