graphql 1.5.3 → 1.5.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql/define/assign_enum_value.rb +1 -1
  3. data/lib/graphql/execution/directive_checks.rb +5 -5
  4. data/lib/graphql/internal_representation.rb +1 -0
  5. data/lib/graphql/internal_representation/node.rb +117 -16
  6. data/lib/graphql/internal_representation/rewrite.rb +39 -94
  7. data/lib/graphql/internal_representation/scope.rb +88 -0
  8. data/lib/graphql/introspection/schema_field.rb +5 -10
  9. data/lib/graphql/introspection/type_by_name_field.rb +8 -13
  10. data/lib/graphql/introspection/typename_field.rb +5 -10
  11. data/lib/graphql/query.rb +24 -155
  12. data/lib/graphql/query/arguments_cache.rb +25 -0
  13. data/lib/graphql/query/validation_pipeline.rb +114 -0
  14. data/lib/graphql/query/variables.rb +18 -14
  15. data/lib/graphql/schema.rb +4 -3
  16. data/lib/graphql/schema/mask.rb +55 -0
  17. data/lib/graphql/schema/possible_types.rb +2 -2
  18. data/lib/graphql/schema/type_expression.rb +19 -4
  19. data/lib/graphql/schema/warden.rb +1 -3
  20. data/lib/graphql/static_validation/rules/fields_will_merge.rb +3 -2
  21. data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +4 -2
  22. data/lib/graphql/static_validation/rules/variable_default_values_are_correctly_typed.rb +3 -1
  23. data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +1 -20
  24. data/lib/graphql/static_validation/validation_context.rb +6 -18
  25. data/lib/graphql/version.rb +1 -1
  26. data/spec/graphql/enum_type_spec.rb +12 -0
  27. data/spec/graphql/internal_representation/rewrite_spec.rb +26 -5
  28. data/spec/graphql/query_spec.rb +23 -3
  29. data/spec/graphql/static_validation/rules/fields_will_merge_spec.rb +2 -2
  30. data/spec/graphql/static_validation/rules/variable_default_values_are_correctly_typed_spec.rb +12 -0
  31. data/spec/graphql/static_validation/rules/variables_are_input_types_spec.rb +14 -0
  32. 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
- variable_name = ast_variable.name
23
- default_value = ast_variable.default_value
24
- provided_value = @provided_variables[variable_name]
25
- value_was_provided = @provided_variables.key?(variable_name)
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
- validation_result = variable_type.validate_input(provided_value, @warden)
28
- if !validation_result.valid?
29
- # This finds variables that were required but not provided
30
- @errors << GraphQL::Query::VariableValidationError.new(ast_variable, variable_type, provided_value, validation_result)
31
- elsif value_was_provided
32
- # Add the variable if a value was provided
33
- memo[variable_name] = variable_type.coerce_input(provided_value)
34
- elsif default_value
35
- # Add the variable if it wasn't provided but it has a default value (including `null`)
36
- memo[variable_name] = GraphQL::Query::LiteralInput.coerce(variable_type, default_value, {})
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
@@ -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.create(parent_type)
216
+ GraphQL::Introspection::TypenameField
216
217
  elsif field_name == "__schema" && parent_type == query
217
- GraphQL::Introspection::SchemaField.create(self)
218
+ GraphQL::Introspection::SchemaField
218
219
  elsif field_name == "__type" && parent_type == query
219
- GraphQL::Introspection::TypeByNameField.create(self)
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::ObjectType
29
+ when GraphQL::BaseType
30
30
  [type_defn]
31
31
  else
32
- raise "#{type_defn} doesn't have possible types"
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
- type_name = ast_node.name
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).to_non_null_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).to_list_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 node.definitions.size > 1
11
- defn_names = node.definitions.map { |d| d.name }.sort.join(" or ")
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
- intersecting_types = context.warden.possible_types(parent_type.unwrap) & context.warden.possible_types(child_type.unwrap)
47
- if intersecting_types.none?
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 !context.valid_literal?(value, type)
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 = to_query_type(ast_var.type, context.query.warden)
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
- :fragments, :operations, :warden,
18
- :dependencies, :each_irep_node_handlers
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
- @schema = query.schema
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
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module GraphQL
3
- VERSION = "1.5.3"
3
+ VERSION = "1.5.4"
4
4
  end
@@ -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 = ["cheeseInlineOrigin", "edibleInlineOrigin", "untypedInlineOrigin", "cheeseFragmentOrigin"]
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 = ["milkInlineOrigin", "edibleInlineOrigin", "untypedInlineOrigin", "milkFragmentOrigin"]
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
@@ -401,16 +401,36 @@ describe GraphQL::Query do
401
401
  end
402
402
  end
403
403
 
404
- describe "#max_depth" do
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
- assert_equal 5, query.max_depth
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
- assert_equal 12, query.max_depth
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: scalar or unrelatedField?"
570
+ assert_includes error_messages, "Field 'scalar' has a field conflict: deepBox or unrelatedField?"
571
571
  end
572
572
  end
573
573
  end