graphql 1.5.3 → 1.5.4
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 +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
|