graphql 1.5.3 → 1.5.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/graphql/define/assign_enum_value.rb +1 -1
- data/lib/graphql/execution/directive_checks.rb +5 -5
- data/lib/graphql/internal_representation.rb +1 -0
- data/lib/graphql/internal_representation/node.rb +117 -16
- data/lib/graphql/internal_representation/rewrite.rb +39 -94
- data/lib/graphql/internal_representation/scope.rb +88 -0
- data/lib/graphql/introspection/schema_field.rb +5 -10
- data/lib/graphql/introspection/type_by_name_field.rb +8 -13
- data/lib/graphql/introspection/typename_field.rb +5 -10
- data/lib/graphql/query.rb +24 -155
- data/lib/graphql/query/arguments_cache.rb +25 -0
- data/lib/graphql/query/validation_pipeline.rb +114 -0
- data/lib/graphql/query/variables.rb +18 -14
- data/lib/graphql/schema.rb +4 -3
- data/lib/graphql/schema/mask.rb +55 -0
- data/lib/graphql/schema/possible_types.rb +2 -2
- data/lib/graphql/schema/type_expression.rb +19 -4
- data/lib/graphql/schema/warden.rb +1 -3
- data/lib/graphql/static_validation/rules/fields_will_merge.rb +3 -2
- data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +4 -2
- data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +3 -1
- data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +1 -20
- data/lib/graphql/static_validation/validation_context.rb +6 -18
- data/lib/graphql/version.rb +1 -1
- data/spec/graphql/enum_type_spec.rb +12 -0
- data/spec/graphql/internal_representation/rewrite_spec.rb +26 -5
- data/spec/graphql/query_spec.rb +23 -3
- data/spec/graphql/static_validation/rules/fields_will_merge_spec.rb +2 -2
- data/spec/graphql/static_validation/rules/variable_default_values_are_correctly_typed_spec.rb +12 -0
- data/spec/graphql/static_validation/rules/variables_are_input_types_spec.rb +14 -0
- metadata +6 -2
@@ -19,21 +19,25 @@ module GraphQL
|
|
19
19
|
# - Then, fall back to the default value from the query string
|
20
20
|
# If it's still nil, raise an error if it's required.
|
21
21
|
variable_type = @schema.type_from_ast(ast_variable.type)
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
22
|
+
if variable_type.nil?
|
23
|
+
# Pass -- it will get handled by a validator
|
24
|
+
else
|
25
|
+
variable_name = ast_variable.name
|
26
|
+
default_value = ast_variable.default_value
|
27
|
+
provided_value = @provided_variables[variable_name]
|
28
|
+
value_was_provided = @provided_variables.key?(variable_name)
|
26
29
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
30
|
+
validation_result = variable_type.validate_input(provided_value, @warden)
|
31
|
+
if !validation_result.valid?
|
32
|
+
# This finds variables that were required but not provided
|
33
|
+
@errors << GraphQL::Query::VariableValidationError.new(ast_variable, variable_type, provided_value, validation_result)
|
34
|
+
elsif value_was_provided
|
35
|
+
# Add the variable if a value was provided
|
36
|
+
memo[variable_name] = variable_type.coerce_input(provided_value)
|
37
|
+
elsif default_value
|
38
|
+
# Add the variable if it wasn't provided but it has a default value (including `null`)
|
39
|
+
memo[variable_name] = GraphQL::Query::LiteralInput.coerce(variable_type, default_value, {})
|
40
|
+
end
|
37
41
|
end
|
38
42
|
end
|
39
43
|
end
|
data/lib/graphql/schema.rb
CHANGED
@@ -6,6 +6,7 @@ require "graphql/schema/default_type_error"
|
|
6
6
|
require "graphql/schema/invalid_type_error"
|
7
7
|
require "graphql/schema/instrumented_field_map"
|
8
8
|
require "graphql/schema/middleware_chain"
|
9
|
+
require "graphql/schema/mask"
|
9
10
|
require "graphql/schema/null_mask"
|
10
11
|
require "graphql/schema/possible_types"
|
11
12
|
require "graphql/schema/rescue_middleware"
|
@@ -212,11 +213,11 @@ module GraphQL
|
|
212
213
|
if defined_field
|
213
214
|
defined_field
|
214
215
|
elsif field_name == "__typename"
|
215
|
-
GraphQL::Introspection::TypenameField
|
216
|
+
GraphQL::Introspection::TypenameField
|
216
217
|
elsif field_name == "__schema" && parent_type == query
|
217
|
-
GraphQL::Introspection::SchemaField
|
218
|
+
GraphQL::Introspection::SchemaField
|
218
219
|
elsif field_name == "__type" && parent_type == query
|
219
|
-
GraphQL::Introspection::TypeByNameField
|
220
|
+
GraphQL::Introspection::TypeByNameField
|
220
221
|
else
|
221
222
|
nil
|
222
223
|
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module GraphQL
|
3
|
+
class Schema
|
4
|
+
# Tools for working with schema masks (`only` / `except`).
|
5
|
+
#
|
6
|
+
# In general, these are functions which, when they return `true`,
|
7
|
+
# the `member` is hidden for the current query.
|
8
|
+
#
|
9
|
+
# @api private
|
10
|
+
module Mask
|
11
|
+
module_function
|
12
|
+
|
13
|
+
# Combine a schema's default_mask with query-level masks.
|
14
|
+
def combine(default_mask, except:, only:)
|
15
|
+
query_mask = if except
|
16
|
+
except
|
17
|
+
elsif only
|
18
|
+
InvertedMask.new(only)
|
19
|
+
end
|
20
|
+
|
21
|
+
if query_mask && (default_mask != GraphQL::Schema::NullMask)
|
22
|
+
EitherMask.new(default_mask, query_mask)
|
23
|
+
else
|
24
|
+
query_mask || default_mask
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# @api private
|
29
|
+
# Returns true when the inner mask returned false
|
30
|
+
# Returns false when the inner mask returned true
|
31
|
+
class InvertedMask
|
32
|
+
def initialize(inner_mask)
|
33
|
+
@inner_mask = inner_mask
|
34
|
+
end
|
35
|
+
|
36
|
+
def call(member, ctx)
|
37
|
+
!@inner_mask.call(member, ctx)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Hides `member` if _either_ mask would hide the member.
|
42
|
+
# @api private
|
43
|
+
class EitherMask
|
44
|
+
def initialize(first_mask, second_mask)
|
45
|
+
@first_mask = first_mask
|
46
|
+
@second_mask = second_mask
|
47
|
+
end
|
48
|
+
|
49
|
+
def call(member, ctx)
|
50
|
+
@first_mask.call(member, ctx) || @second_mask.call(member, ctx)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -26,10 +26,10 @@ module GraphQL
|
|
26
26
|
type_defn.possible_types
|
27
27
|
when GraphQL::InterfaceType
|
28
28
|
@interface_implementers[type_defn]
|
29
|
-
when GraphQL::
|
29
|
+
when GraphQL::BaseType
|
30
30
|
[type_defn]
|
31
31
|
else
|
32
|
-
raise "#{type_defn}
|
32
|
+
raise "Unexpected possible_types object: #{type_defn}"
|
33
33
|
end
|
34
34
|
end
|
35
35
|
end
|
@@ -1,18 +1,33 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module GraphQL
|
3
3
|
class Schema
|
4
|
+
# @api private
|
4
5
|
module TypeExpression
|
6
|
+
# Fetch a type from a type map by its AST specification.
|
7
|
+
# Return `nil` if not found.
|
8
|
+
# @param types [GraphQL::Schema::TypeMap]
|
9
|
+
# @param ast_node [GraphQL::Language::Nodes::AbstractNode]
|
10
|
+
# @return [GraphQL::BaseType, nil]
|
5
11
|
def self.build_type(types, ast_node)
|
6
12
|
case ast_node
|
7
13
|
when GraphQL::Language::Nodes::TypeName
|
8
|
-
|
9
|
-
types[type_name]
|
14
|
+
types.fetch(ast_node.name, nil)
|
10
15
|
when GraphQL::Language::Nodes::NonNullType
|
11
16
|
ast_inner_type = ast_node.of_type
|
12
|
-
build_type(types, ast_inner_type)
|
17
|
+
inner_type = build_type(types, ast_inner_type)
|
18
|
+
wrap_type(inner_type, GraphQL::NonNullType)
|
13
19
|
when GraphQL::Language::Nodes::ListType
|
14
20
|
ast_inner_type = ast_node.of_type
|
15
|
-
build_type(types, ast_inner_type)
|
21
|
+
inner_type = build_type(types, ast_inner_type)
|
22
|
+
wrap_type(inner_type, GraphQL::ListType)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.wrap_type(type, wrapper)
|
27
|
+
if type.nil?
|
28
|
+
nil
|
29
|
+
else
|
30
|
+
wrapper.new(of_type: type)
|
16
31
|
end
|
17
32
|
end
|
18
33
|
end
|
@@ -43,9 +43,8 @@ module GraphQL
|
|
43
43
|
# @param schema [GraphQL::Schema]
|
44
44
|
# @param deep_check [Boolean]
|
45
45
|
def initialize(mask, context:, schema:)
|
46
|
-
@mask = mask
|
47
|
-
@context = context
|
48
46
|
@schema = schema
|
47
|
+
@visibility_cache = read_through { |m| !mask.call(m, context) }
|
49
48
|
end
|
50
49
|
|
51
50
|
# @return [Array<GraphQL::BaseType>] Visible types in the schema
|
@@ -136,7 +135,6 @@ module GraphQL
|
|
136
135
|
end
|
137
136
|
|
138
137
|
def visible?(member)
|
139
|
-
@visibility_cache ||= read_through { |m| !@mask.call(m, @context) }
|
140
138
|
@visibility_cache[member]
|
141
139
|
end
|
142
140
|
|
@@ -5,10 +5,11 @@ module GraphQL
|
|
5
5
|
def validate(context)
|
6
6
|
context.each_irep_node do |node|
|
7
7
|
if node.ast_nodes.size > 1
|
8
|
+
defn_names = Set.new(node.ast_nodes.map(&:name))
|
8
9
|
|
9
10
|
# Check for more than one GraphQL::Field backing this node:
|
10
|
-
if
|
11
|
-
defn_names =
|
11
|
+
if defn_names.size > 1
|
12
|
+
defn_names = defn_names.sort.join(" or ")
|
12
13
|
msg = "Field '#{node.name}' has a field conflict: #{defn_names}?"
|
13
14
|
context.errors << GraphQL::StaticValidation::Message.new(msg, nodes: node.ast_nodes.to_a)
|
14
15
|
end
|
@@ -43,8 +43,10 @@ module GraphQL
|
|
43
43
|
# It's not a valid fragment type, this error was handled someplace else
|
44
44
|
return
|
45
45
|
end
|
46
|
-
|
47
|
-
|
46
|
+
parent_types = context.warden.possible_types(parent_type.unwrap)
|
47
|
+
child_types = context.warden.possible_types(child_type.unwrap)
|
48
|
+
|
49
|
+
if child_types.none? { |c| parent_types.include?(c) }
|
48
50
|
name = node.respond_to?(:name) ? " #{node.name}" : ""
|
49
51
|
context.errors << message("Fragment#{name} on #{child_type.name} can't be spread inside #{parent_type.name}", node, path: path)
|
50
52
|
end
|
@@ -18,7 +18,9 @@ module GraphQL
|
|
18
18
|
context.errors << message("Non-null variable $#{node.name} can't have a default value", node, context: context)
|
19
19
|
else
|
20
20
|
type = context.schema.type_from_ast(node.type)
|
21
|
-
if
|
21
|
+
if type.nil?
|
22
|
+
# This is handled by another validator
|
23
|
+
elsif !context.valid_literal?(value, type)
|
22
24
|
context.errors << message("Default value for $#{node.name} doesn't match type #{type}", node, context: context)
|
23
25
|
end
|
24
26
|
end
|
@@ -33,7 +33,7 @@ module GraphQL
|
|
33
33
|
private
|
34
34
|
|
35
35
|
def validate_usage(arguments, arg_node, ast_var, context)
|
36
|
-
var_type =
|
36
|
+
var_type = context.schema.type_from_ast(ast_var.type)
|
37
37
|
if var_type.nil?
|
38
38
|
return
|
39
39
|
end
|
@@ -56,25 +56,6 @@ module GraphQL
|
|
56
56
|
end
|
57
57
|
end
|
58
58
|
|
59
|
-
def to_query_type(ast_type, warden)
|
60
|
-
case ast_type
|
61
|
-
when GraphQL::Language::Nodes::NonNullType
|
62
|
-
wrap_query_type(to_query_type(ast_type.of_type, warden), GraphQL::NonNullType)
|
63
|
-
when GraphQL::Language::Nodes::ListType
|
64
|
-
wrap_query_type(to_query_type(ast_type.of_type, warden), GraphQL::ListType)
|
65
|
-
else
|
66
|
-
warden.get_type(ast_type.name)
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
def wrap_query_type(type, wrapper)
|
71
|
-
if type.nil?
|
72
|
-
nil
|
73
|
-
else
|
74
|
-
wrapper.new(of_type: type)
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
59
|
def create_error(error_message, var_type, ast_var, arg_defn, arg_node, context)
|
79
60
|
message("#{error_message} on variable $#{ast_var.name} and argument #{arg_node.name} (#{var_type.to_s} / #{arg_defn.type.to_s})", arg_node, context: context)
|
80
61
|
end
|
@@ -12,28 +12,17 @@ module GraphQL
|
|
12
12
|
# It also provides limited access to the {TypeStack} instance,
|
13
13
|
# which tracks state as you climb in and out of different fields.
|
14
14
|
class ValidationContext
|
15
|
+
extend Forwardable
|
16
|
+
|
15
17
|
attr_reader :query, :schema,
|
16
18
|
:document, :errors, :visitor,
|
17
|
-
:
|
18
|
-
|
19
|
+
:warden, :dependencies, :each_irep_node_handlers
|
20
|
+
|
21
|
+
def_delegators :@query, :schema, :document, :fragments, :operations, :warden
|
19
22
|
|
20
23
|
def initialize(query)
|
21
24
|
@query = query
|
22
|
-
@
|
23
|
-
@document = query.document
|
24
|
-
@fragments = {}
|
25
|
-
@operations = {}
|
26
|
-
@warden = query.warden
|
27
|
-
|
28
|
-
document.definitions.each do |definition|
|
29
|
-
case definition
|
30
|
-
when GraphQL::Language::Nodes::FragmentDefinition
|
31
|
-
@fragments[definition.name] = definition
|
32
|
-
when GraphQL::Language::Nodes::OperationDefinition
|
33
|
-
@operations[definition.name] = definition
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
25
|
+
@literal_validator = LiteralValidator.new(warden: warden)
|
37
26
|
@errors = []
|
38
27
|
@visitor = GraphQL::Language::Visitor.new(document)
|
39
28
|
@type_stack = GraphQL::StaticValidation::TypeStack.new(schema, visitor)
|
@@ -93,7 +82,6 @@ module GraphQL
|
|
93
82
|
end
|
94
83
|
|
95
84
|
def valid_literal?(ast_value, type)
|
96
|
-
@literal_validator ||= LiteralValidator.new(warden: @warden)
|
97
85
|
@literal_validator.validate(ast_value, type)
|
98
86
|
end
|
99
87
|
end
|
data/lib/graphql/version.rb
CHANGED
@@ -67,6 +67,18 @@ describe GraphQL::EnumType do
|
|
67
67
|
end
|
68
68
|
end
|
69
69
|
|
70
|
+
it "accepts a symbol as a variant and Ruby-land value" do
|
71
|
+
enum = GraphQL::EnumType.define do
|
72
|
+
name 'MessageFormat'
|
73
|
+
value :markdown
|
74
|
+
end
|
75
|
+
|
76
|
+
variant = enum.values['markdown']
|
77
|
+
|
78
|
+
assert_equal(variant.name, 'markdown')
|
79
|
+
assert_equal(variant.value, :markdown)
|
80
|
+
end
|
81
|
+
|
70
82
|
it "has value description" do
|
71
83
|
assert_equal("Animal with horns", enum.values["GOAT"].description)
|
72
84
|
end
|
@@ -59,7 +59,6 @@ describe GraphQL::InternalRepresentation::Rewrite do
|
|
59
59
|
res[:errors].any? && raise(res[:errors].map(&:message).join("; "))
|
60
60
|
res[:irep]
|
61
61
|
}
|
62
|
-
# TODO: make sure all rewrite specs are covered
|
63
62
|
|
64
63
|
describe "building a tree over concrete types with fragments" do
|
65
64
|
let(:query_string) {
|
@@ -107,6 +106,9 @@ describe GraphQL::InternalRepresentation::Rewrite do
|
|
107
106
|
it "groups selections by object types which they apply to" do
|
108
107
|
doc = rewrite_result["getPlant"]
|
109
108
|
|
109
|
+
plant_scoped_selection = doc.scoped_children[schema.types["Query"]]["plant"]
|
110
|
+
assert_equal ["Fruit", "Nut", "Plant", "Tree"], plant_scoped_selection.scoped_children.keys.map(&:name).sort
|
111
|
+
|
110
112
|
plant_selection = doc.typed_children[schema.types["Query"]]["plant"]
|
111
113
|
assert_equal ["Fruit", "Grain", "Nut", "Vegetable"], plant_selection.typed_children.keys.map(&:name).sort
|
112
114
|
|
@@ -121,6 +123,25 @@ describe GraphQL::InternalRepresentation::Rewrite do
|
|
121
123
|
habitats_selections = nut_selections["habitats"].typed_children[schema.types["Habitat"]]
|
122
124
|
assert_equal ["averageWeight", "seasons"], habitats_selections.keys
|
123
125
|
end
|
126
|
+
|
127
|
+
it "tracks parent nodes" do
|
128
|
+
doc = rewrite_result["getPlant"]
|
129
|
+
assert_equal nil, doc.parent
|
130
|
+
|
131
|
+
plant_selection = doc.typed_children[schema.types["Query"]]["plant"]
|
132
|
+
assert_equal doc, plant_selection.parent
|
133
|
+
|
134
|
+
leaf_type_selection = plant_selection.typed_children[schema.types["Nut"]]["leafType"]
|
135
|
+
assert_equal plant_selection, leaf_type_selection.parent
|
136
|
+
|
137
|
+
habitats_selection = plant_selection.typed_children[schema.types["Nut"]]["habitats"]
|
138
|
+
assert_equal plant_selection, habitats_selection.parent
|
139
|
+
|
140
|
+
seasons_selection = habitats_selection.typed_children[schema.types["Habitat"]]["seasons"]
|
141
|
+
average_weight_selection = habitats_selection.typed_children[schema.types["Habitat"]]["averageWeight"]
|
142
|
+
assert_equal habitats_selection, seasons_selection.parent
|
143
|
+
assert_equal habitats_selection, average_weight_selection.parent
|
144
|
+
end
|
124
145
|
end
|
125
146
|
|
126
147
|
describe "tracking directives on fragment spreads" do
|
@@ -253,14 +274,14 @@ describe GraphQL::InternalRepresentation::Rewrite do
|
|
253
274
|
assert_equal 3, cheeses.length
|
254
275
|
assert_equal 1, milks.length
|
255
276
|
|
256
|
-
expected_cheese_fields = ["
|
277
|
+
expected_cheese_fields = ["cheeseFragmentOrigin", "cheeseInlineOrigin", "edibleInlineOrigin", "untypedInlineOrigin"]
|
257
278
|
cheeses.each do |cheese|
|
258
|
-
assert_equal expected_cheese_fields, cheese["selfAsEdible"].keys
|
279
|
+
assert_equal expected_cheese_fields, cheese["selfAsEdible"].keys.sort
|
259
280
|
end
|
260
281
|
|
261
|
-
expected_milk_fields = ["
|
282
|
+
expected_milk_fields = ["edibleInlineOrigin", "milkFragmentOrigin", "milkInlineOrigin", "untypedInlineOrigin"]
|
262
283
|
milks.each do |milk|
|
263
|
-
assert_equal expected_milk_fields, milk["selfAsEdible"].keys
|
284
|
+
assert_equal expected_milk_fields, milk["selfAsEdible"].keys.sort
|
264
285
|
end
|
265
286
|
end
|
266
287
|
end
|
data/spec/graphql/query_spec.rb
CHANGED
@@ -401,16 +401,36 @@ describe GraphQL::Query do
|
|
401
401
|
end
|
402
402
|
end
|
403
403
|
|
404
|
-
describe "
|
404
|
+
describe "max_depth" do
|
405
|
+
let(:query_string) {
|
406
|
+
<<-GRAPHQL
|
407
|
+
{
|
408
|
+
cheese(id: 1) {
|
409
|
+
similarCheese(source: SHEEP) {
|
410
|
+
similarCheese(source: SHEEP) {
|
411
|
+
similarCheese(source: SHEEP) {
|
412
|
+
similarCheese(source: SHEEP) {
|
413
|
+
id
|
414
|
+
}
|
415
|
+
}
|
416
|
+
}
|
417
|
+
}
|
418
|
+
}
|
419
|
+
}
|
420
|
+
GRAPHQL
|
421
|
+
}
|
422
|
+
|
405
423
|
it "defaults to the schema's max_depth" do
|
406
|
-
|
424
|
+
# Constrained by schema's setting of 5
|
425
|
+
assert_equal 1, result["errors"].length
|
407
426
|
end
|
408
427
|
|
409
428
|
describe "overriding max_depth" do
|
410
429
|
let(:max_depth) { 12 }
|
411
430
|
|
412
431
|
it "overrides the schema's max_depth" do
|
413
|
-
|
432
|
+
assert result["data"].key?("cheese")
|
433
|
+
assert_equal nil, result["errors"]
|
414
434
|
end
|
415
435
|
end
|
416
436
|
end
|
@@ -559,7 +559,7 @@ describe GraphQL::StaticValidation::FieldsWillMerge do
|
|
559
559
|
}
|
560
560
|
}
|
561
561
|
fragment X on SomeBox {
|
562
|
-
scalar
|
562
|
+
scalar: deepBox { unreleatedField }
|
563
563
|
}
|
564
564
|
fragment Y on SomeBox {
|
565
565
|
scalar: unrelatedField
|
@@ -567,7 +567,7 @@ describe GraphQL::StaticValidation::FieldsWillMerge do
|
|
567
567
|
|}
|
568
568
|
|
569
569
|
it "fails rule" do
|
570
|
-
assert_includes error_messages, "Field 'scalar' has a field conflict:
|
570
|
+
assert_includes error_messages, "Field 'scalar' has a field conflict: deepBox or unrelatedField?"
|
571
571
|
end
|
572
572
|
end
|
573
573
|
end
|