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.
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