graphql 0.11.0 → 0.11.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql/base_type.rb +6 -2
  3. data/lib/graphql/definition_helpers.rb +0 -5
  4. data/lib/graphql/definition_helpers/defined_by_config.rb +106 -102
  5. data/lib/graphql/definition_helpers/non_null_with_bang.rb +13 -9
  6. data/lib/graphql/definition_helpers/string_named_hash.rb +19 -15
  7. data/lib/graphql/definition_helpers/type_definer.rb +25 -21
  8. data/lib/graphql/enum_type.rb +8 -2
  9. data/lib/graphql/float_type.rb +1 -1
  10. data/lib/graphql/input_object_type.rb +27 -6
  11. data/lib/graphql/interface_type.rb +10 -0
  12. data/lib/graphql/introspection/fields_field.rb +2 -2
  13. data/lib/graphql/list_type.rb +12 -2
  14. data/lib/graphql/non_null_type.rb +11 -1
  15. data/lib/graphql/object_type.rb +19 -0
  16. data/lib/graphql/query.rb +2 -14
  17. data/lib/graphql/query/input_validation_result.rb +23 -0
  18. data/lib/graphql/query/literal_input.rb +3 -1
  19. data/lib/graphql/query/serial_execution/field_resolution.rb +3 -1
  20. data/lib/graphql/query/serial_execution/selection_resolution.rb +21 -15
  21. data/lib/graphql/query/variable_validation_error.rb +18 -0
  22. data/lib/graphql/query/variables.rb +4 -8
  23. data/lib/graphql/scalar_type.rb +10 -4
  24. data/lib/graphql/schema.rb +4 -2
  25. data/lib/graphql/schema/printer.rb +12 -3
  26. data/lib/graphql/schema/type_reducer.rb +4 -3
  27. data/lib/graphql/static_validation.rb +1 -0
  28. data/lib/graphql/static_validation/all_rules.rb +1 -0
  29. data/lib/graphql/static_validation/rules/document_does_not_exceed_max_depth.rb +79 -0
  30. data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +1 -1
  31. data/lib/graphql/static_validation/validation_context.rb +63 -0
  32. data/lib/graphql/static_validation/validator.rb +1 -52
  33. data/lib/graphql/version.rb +1 -1
  34. data/readme.md +21 -22
  35. data/spec/graphql/enum_type_spec.rb +8 -0
  36. data/spec/graphql/input_object_type_spec.rb +101 -3
  37. data/spec/graphql/introspection/schema_type_spec.rb +6 -6
  38. data/spec/graphql/introspection/type_type_spec.rb +6 -6
  39. data/spec/graphql/language/transform_spec.rb +9 -5
  40. data/spec/graphql/list_type_spec.rb +23 -0
  41. data/spec/graphql/object_type_spec.rb +11 -4
  42. data/spec/graphql/query/executor_spec.rb +34 -5
  43. data/spec/graphql/query_spec.rb +22 -3
  44. data/spec/graphql/scalar_type_spec.rb +28 -0
  45. data/spec/graphql/schema/type_reducer_spec.rb +2 -2
  46. data/spec/graphql/static_validation/complexity_validator_spec.rb +15 -0
  47. data/spec/graphql/static_validation/rules/document_does_not_exceed_max_depth_spec.rb +93 -0
  48. data/spec/support/dairy_app.rb +2 -2
  49. data/spec/support/minimum_input_object.rb +8 -5
  50. metadata +10 -4
  51. data/spec/graphql/static_validation/complexity_validator.rb +0 -15
@@ -23,4 +23,14 @@ class GraphQL::InterfaceType < GraphQL::BaseType
23
23
  def possible_types
24
24
  @possible_types ||= []
25
25
  end
26
+
27
+ # @return [GraphQL::Field] The defined field for `field_name`
28
+ def get_field(field_name)
29
+ fields[field_name]
30
+ end
31
+
32
+ # @return [Array<GraphQL::Field>] All fields on this type
33
+ def all_fields
34
+ fields.values
35
+ end
26
36
  end
@@ -4,10 +4,10 @@ GraphQL::Introspection::FieldsField = GraphQL::Field.define do
4
4
  argument :includeDeprecated, GraphQL::BOOLEAN_TYPE, default_value: false
5
5
  resolve -> (object, arguments, context) {
6
6
  return nil if !object.kind.fields?
7
- fields = object.fields.values
7
+ fields = object.all_fields
8
8
  if !arguments["includeDeprecated"]
9
9
  fields = fields.select {|f| !f.deprecation_reason }
10
10
  end
11
- fields
11
+ fields.sort_by { |f| f.name }
12
12
  }
13
13
  end
@@ -17,10 +17,20 @@ class GraphQL::ListType < GraphQL::BaseType
17
17
  "[#{of_type.to_s}]"
18
18
  end
19
19
 
20
- def valid_non_null_input?(value)
21
- ensure_array(value).all?{ |item| of_type.valid_input?(item) }
20
+ def validate_non_null_input(value)
21
+ result = GraphQL::Query::InputValidationResult.new
22
+
23
+ ensure_array(value).each_with_index do |item, index|
24
+ item_result = of_type.validate_input(item)
25
+ if !item_result.valid?
26
+ result.merge_result!(index, item_result)
27
+ end
28
+ end
29
+
30
+ result
22
31
  end
23
32
 
33
+
24
34
  def coerce_non_null_input(value)
25
35
  ensure_array(value).map{ |item| of_type.coerce_input(item) }
26
36
  end
@@ -14,7 +14,17 @@ class GraphQL::NonNullType < GraphQL::BaseType
14
14
  end
15
15
 
16
16
  def valid_input?(value)
17
- !value.nil? && of_type.valid_input?(value)
17
+ validate_input(value).valid?
18
+ end
19
+
20
+ def validate_input(value)
21
+ if value.nil?
22
+ result = GraphQL::Query::InputValidationResult.new
23
+ result.add_problem("Expected value to not be null")
24
+ result
25
+ else
26
+ of_type.validate_input(value)
27
+ end
18
28
  end
19
29
 
20
30
  def coerce_input(value)
@@ -42,4 +42,23 @@ class GraphQL::ObjectType < GraphQL::BaseType
42
42
  def kind
43
43
  GraphQL::TypeKinds::OBJECT
44
44
  end
45
+
46
+ # @return [GraphQL::Field] The field definition for `field_name` (may be inherited from interfaces)
47
+ def get_field(field_name)
48
+ fields[field_name] || interface_fields[field_name]
49
+ end
50
+
51
+ # @return [Array<GraphQL::Field>] All fields, including ones inherited from interfaces
52
+ def all_fields
53
+ interface_fields.merge(self.fields).values
54
+ end
55
+
56
+ private
57
+
58
+ # Create a {name => defn} hash for fields inherited from interfaces
59
+ def interface_fields
60
+ interfaces.reduce({}) do |memo, iface|
61
+ memo.merge!(iface.fields)
62
+ end
63
+ end
45
64
  end
@@ -6,20 +6,6 @@ class GraphQL::Query
6
6
  end
7
7
  end
8
8
 
9
- class VariableValidationError < GraphQL::ExecutionError
10
- def initialize(variable_ast, type, reason)
11
- msg = "Variable #{variable_ast.name} of type #{type} #{reason}"
12
- super(msg)
13
- self.ast_node = variable_ast
14
- end
15
- end
16
-
17
- class VariableMissingError < VariableValidationError
18
- def initialize(variable_ast, type)
19
- super(variable_ast, type, "can't be null")
20
- end
21
- end
22
-
23
9
  # If a resolve function returns `GraphQL::Query::DEFAULT_RESOLVE`,
24
10
  # The executor will send the field's name to the target object
25
11
  # and use the result.
@@ -111,3 +97,5 @@ require 'graphql/query/literal_input'
111
97
  require 'graphql/query/serial_execution'
112
98
  require 'graphql/query/type_resolver'
113
99
  require 'graphql/query/variables'
100
+ require 'graphql/query/input_validation_result'
101
+ require 'graphql/query/variable_validation_error'
@@ -0,0 +1,23 @@
1
+ class GraphQL::Query
2
+ class InputValidationResult
3
+ attr_accessor :problems
4
+
5
+ def valid?
6
+ @problems.nil?
7
+ end
8
+
9
+ def add_problem(explanation, path = nil)
10
+ @problems ||= []
11
+ @problems.push({ 'path' => path || [], 'explanation' => explanation })
12
+ end
13
+
14
+ def merge_result!(path, inner_result)
15
+ return if inner_result.valid?
16
+
17
+ inner_result.problems.each do |p|
18
+ item_path = [path, *p['path']]
19
+ add_problem(p['explanation'], item_path)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -55,7 +55,9 @@ module GraphQL
55
55
  type.input_fields.each do |arg_name, arg_defn|
56
56
  if hash[arg_name].nil?
57
57
  value = LiteralInput.coerce(arg_defn.type, arg_defn.default_value, variables)
58
- hash[arg_name] = value unless value.nil?
58
+ if !value.nil?
59
+ hash[arg_name] = value
60
+ end
59
61
  end
60
62
  end
61
63
  Arguments.new(hash)
@@ -10,7 +10,9 @@ module GraphQL
10
10
  @target = target
11
11
  @execution_context = execution_context
12
12
  @field = execution_context.get_field(parent_type, ast_node.name)
13
- raise("No field found on #{parent_type.name} '#{parent_type}' for '#{ast_node.name}'") unless field
13
+ if @field.nil?
14
+ raise("No field found on #{parent_type.name} '#{parent_type}' for '#{ast_node.name}'")
15
+ end
14
16
  @arguments = GraphQL::Query::LiteralInput.from_arguments(
15
17
  ast_node.arguments,
16
18
  field.arguments,
@@ -18,7 +18,7 @@ module GraphQL
18
18
  result.merge(resolve_field(ast_node))
19
19
  }
20
20
  rescue GraphQL::InvalidNullError => err
21
- execution_context.add_error(err) unless err.parent_error?
21
+ err.parent_error? || execution_context.add_error(err)
22
22
  nil
23
23
  end
24
24
 
@@ -56,8 +56,11 @@ module GraphQL
56
56
  end
57
57
 
58
58
  def flatten_fragment(ast_fragment)
59
- return {} unless fragment_type_can_apply?(ast_fragment)
60
- flatten_and_merge_selections(ast_fragment.selections)
59
+ if fragment_type_can_apply?(ast_fragment)
60
+ flatten_and_merge_selections(ast_fragment.selections)
61
+ else
62
+ {}
63
+ end
61
64
  end
62
65
 
63
66
  def fragment_type_can_apply?(ast_fragment)
@@ -68,18 +71,21 @@ module GraphQL
68
71
 
69
72
  def merge_fields(field1, field2)
70
73
  field_type = execution_context.get_field(type, field2.name).type.unwrap
71
- return field2 unless field_type.kind.fields?
72
-
73
- # create a new ast field node merging selections from each field.
74
- # Because of static validation, we can assume that name, alias,
75
- # arguments, and directives are exactly the same for fields 1 and 2.
76
- GraphQL::Language::Nodes::Field.new(
77
- name: field2.name,
78
- alias: field2.alias,
79
- arguments: field2.arguments,
80
- directives: field2.directives,
81
- selections: field1.selections + field2.selections
82
- )
74
+
75
+ if field_type.kind.fields?
76
+ # create a new ast field node merging selections from each field.
77
+ # Because of static validation, we can assume that name, alias,
78
+ # arguments, and directives are exactly the same for fields 1 and 2.
79
+ GraphQL::Language::Nodes::Field.new(
80
+ name: field2.name,
81
+ alias: field2.alias,
82
+ arguments: field2.arguments,
83
+ directives: field2.directives,
84
+ selections: field1.selections + field2.selections
85
+ )
86
+ else
87
+ field2
88
+ end
83
89
  end
84
90
 
85
91
  def resolve_field(ast_node)
@@ -0,0 +1,18 @@
1
+ class GraphQL::Query
2
+ class VariableValidationError < GraphQL::ExecutionError
3
+ attr_accessor :value, :validation_result
4
+
5
+ def initialize(variable_ast, type, value, validation_result)
6
+ @value = value
7
+ @validation_result = validation_result
8
+
9
+ msg = "Variable #{variable_ast.name} of type #{type} was provided invalid value"
10
+ super(msg)
11
+ self.ast_node = variable_ast
12
+ end
13
+
14
+ def to_h
15
+ super.merge({ 'value' => value, 'problems' => validation_result.problems })
16
+ end
17
+ end
18
+ end
@@ -27,14 +27,10 @@ module GraphQL
27
27
  default_value = ast_variable.default_value
28
28
  provided_value = @provided_variables[variable_name]
29
29
 
30
- unless variable_type.valid_input?(provided_value)
31
- if provided_value.nil?
32
- raise GraphQL::Query::VariableMissingError.new(ast_variable, variable_type)
33
- else
34
- raise GraphQL::Query::VariableValidationError.new(ast_variable, variable_type, "was provided invalid value #{JSON.dump(provided_value)}")
35
- end
36
- end
37
- if provided_value.nil?
30
+ validation_result = variable_type.validate_input(provided_value)
31
+ if !validation_result.valid?
32
+ raise GraphQL::Query::VariableValidationError.new(ast_variable, variable_type, provided_value, validation_result)
33
+ elsif provided_value.nil?
38
34
  GraphQL::Query::LiteralInput.coerce(variable_type, default_value, {})
39
35
  else
40
36
  variable_type.coerce_input(provided_value)
@@ -19,8 +19,10 @@ module GraphQL
19
19
  self.coerce_result = proc
20
20
  end
21
21
 
22
- def valid_non_null_input?(value)
23
- !coerce_non_null_input(value).nil?
22
+ def validate_non_null_input(value)
23
+ result = Query::InputValidationResult.new
24
+ result.add_problem("Could not coerce value #{JSON.dump(value)} to #{name}") if coerce_non_null_input(value).nil?
25
+ result
24
26
  end
25
27
 
26
28
  def coerce_non_null_input(value)
@@ -28,7 +30,9 @@ module GraphQL
28
30
  end
29
31
 
30
32
  def coerce_input=(proc)
31
- @coerce_input_proc = proc unless proc.nil?
33
+ if !proc.nil?
34
+ @coerce_input_proc = proc
35
+ end
32
36
  end
33
37
 
34
38
  def coerce_result(value)
@@ -36,7 +40,9 @@ module GraphQL
36
40
  end
37
41
 
38
42
  def coerce_result=(proc)
39
- @coerce_result_proc = proc unless proc.nil?
43
+ if !proc.nil?
44
+ @coerce_result_proc = proc
45
+ end
40
46
  end
41
47
 
42
48
  def kind
@@ -6,6 +6,7 @@ class GraphQL::Schema
6
6
  DYNAMIC_FIELDS = ["__type", "__typename", "__schema"]
7
7
 
8
8
  attr_reader :query, :mutation, :subscription, :directives, :static_validator
9
+ attr_accessor :max_depth
9
10
  # Override these if you don't want the default executor:
10
11
  attr_accessor :query_execution_strategy,
11
12
  :mutation_execution_strategy,
@@ -17,10 +18,11 @@ class GraphQL::Schema
17
18
  # @param query [GraphQL::ObjectType] the query root for the schema
18
19
  # @param mutation [GraphQL::ObjectType] the mutation root for the schema
19
20
  # @param subscription [GraphQL::ObjectType] the subscription root for the schema
20
- def initialize(query:, mutation: nil, subscription: nil)
21
+ def initialize(query:, mutation: nil, subscription: nil, max_depth: nil)
21
22
  @query = query
22
23
  @mutation = mutation
23
24
  @subscription = subscription
25
+ @max_depth = max_depth
24
26
  @directives = DIRECTIVES.reduce({}) { |m, d| m[d.name] = d; m }
25
27
  @static_validator = GraphQL::StaticValidation::Validator.new(schema: self)
26
28
  @rescue_middleware = GraphQL::Schema::RescueMiddleware.new
@@ -49,7 +51,7 @@ class GraphQL::Schema
49
51
  # Resolve field named `field_name` for type `parent_type`.
50
52
  # Handles dynamic fields `__typename`, `__type` and `__schema`, too
51
53
  def get_field(parent_type, field_name)
52
- defined_field = parent_type.fields[field_name]
54
+ defined_field = parent_type.get_field(field_name)
53
55
  if defined_field
54
56
  defined_field
55
57
  elsif field_name == "__typename"
@@ -48,7 +48,7 @@ module GraphQL
48
48
  module TypeKindPrinters
49
49
  module FieldPrinter
50
50
  def print_fields(type)
51
- type.fields.values.map{ |field| " #{field.name}#{print_args(field)}: #{field.type}" }.join("\n")
51
+ type.all_fields.map{ |field| " #{field.name}#{print_args(field)}: #{field.type}" }.join("\n")
52
52
  end
53
53
 
54
54
  def print_args(field)
@@ -57,7 +57,12 @@ module GraphQL
57
57
  end
58
58
 
59
59
  def print_input_value(arg)
60
- default_string = " = #{print_value(arg.default_value, arg.type)}" unless arg.default_value.nil?
60
+ if arg.default_value.nil?
61
+ default_string = nil
62
+ else
63
+ default_string = " = #{print_value(arg.default_value, arg.type)}"
64
+ end
65
+
61
66
  "#{arg.name}: #{arg.type.to_s}#{default_string}"
62
67
  end
63
68
 
@@ -98,7 +103,11 @@ module GraphQL
98
103
  class ObjectPrinter
99
104
  extend FieldPrinter
100
105
  def self.print(type)
101
- implementations = " implements #{type.interfaces.map(&:to_s).join(", ")}" unless type.interfaces.empty?
106
+ if type.interfaces.any?
107
+ implementations = " implements #{type.interfaces.map(&:to_s).join(", ")}"
108
+ else
109
+ implementations = nil
110
+ end
102
111
  "type #{type.name}#{implementations} {\n#{print_fields(type)}\n}"
103
112
  end
104
113
  end
@@ -30,7 +30,7 @@ class GraphQL::Schema::TypeReducer
30
30
  def find_types(type, type_hash)
31
31
  type_hash[type.name] = type
32
32
  if type.kind.fields?
33
- type.fields.each do |name, field|
33
+ type.all_fields.each do |field|
34
34
  reduce_type(field.type, type_hash)
35
35
  field.arguments.each do |name, argument|
36
36
  reduce_type(argument.type, type_hash)
@@ -57,10 +57,11 @@ class GraphQL::Schema::TypeReducer
57
57
  end
58
58
 
59
59
  def reduce_type(type, type_hash)
60
- unless type.is_a?(GraphQL::BaseType)
60
+ if type.is_a?(GraphQL::BaseType)
61
+ self.class.new(type.unwrap, type_hash).result
62
+ else
61
63
  raise GraphQL::Schema::InvalidTypeError.new(type, ["Must be a GraphQL::BaseType"])
62
64
  end
63
- self.class.new(type.unwrap, type_hash).result
64
65
  end
65
66
 
66
67
  def validate_type(type)
@@ -5,6 +5,7 @@ require 'graphql/static_validation/message'
5
5
  require 'graphql/static_validation/arguments_validator'
6
6
  require 'graphql/static_validation/type_stack'
7
7
  require 'graphql/static_validation/validator'
8
+ require 'graphql/static_validation/validation_context'
8
9
  require 'graphql/static_validation/literal_validator'
9
10
 
10
11
  rules_glob = File.expand_path("../static_validation/rules/*.rb", __FILE__)
@@ -20,4 +20,5 @@ GraphQL::StaticValidation::ALL_RULES = [
20
20
  GraphQL::StaticValidation::VariableDefaultValuesAreCorrectlyTyped,
21
21
  GraphQL::StaticValidation::VariablesAreUsedAndDefined,
22
22
  GraphQL::StaticValidation::VariableUsagesAreAllowed,
23
+ GraphQL::StaticValidation::DocumentDoesNotExceedMaxDepth,
23
24
  ]
@@ -0,0 +1,79 @@
1
+ module GraphQL
2
+ module StaticValidation
3
+ class DocumentDoesNotExceedMaxDepth
4
+ include GraphQL::StaticValidation::Message::MessageHelper
5
+
6
+ def validate(context)
7
+ max_allowed_depth = context.schema.max_depth
8
+ return if max_allowed_depth.nil?
9
+
10
+ visitor = context.visitor
11
+
12
+ # operation or fragment name
13
+ current_field_scope = nil
14
+ current_depth = 0
15
+ skip_current_scope = false
16
+
17
+ # {name => depth} pairs for operations and fragments
18
+ depths = Hash.new { |h, k| h[k] = 0 }
19
+
20
+ # {name => [fragmentName...]} pairs
21
+ fragments = Hash.new { |h, k| h[k] = []}
22
+
23
+ visitor[GraphQL::Language::Nodes::Document].leave << -> (node, parent) {
24
+ context.errors.none? && assert_under_max_depth(context, max_allowed_depth, depths, fragments)
25
+ }
26
+
27
+ visitor[GraphQL::Language::Nodes::OperationDefinition] << -> (node, parent) {
28
+ current_field_scope = node.name
29
+ }
30
+
31
+ visitor[GraphQL::Language::Nodes::FragmentDefinition] << -> (node, parent) {
32
+ current_field_scope = node.name
33
+ }
34
+
35
+ visitor[GraphQL::Language::Nodes::Field] << -> (node, parent) {
36
+ # Don't validate queries on __schema, __type
37
+ skip_current_scope ||= context.skip_field?(node.name)
38
+
39
+ if node.selections.any? && !skip_current_scope
40
+ current_depth += 1
41
+ if current_depth > depths[current_field_scope]
42
+ depths[current_field_scope] = current_depth
43
+ end
44
+ end
45
+ }
46
+
47
+ visitor[GraphQL::Language::Nodes::Field].leave << -> (node, parent) {
48
+ if skip_current_scope && context.skip_field?(node.name)
49
+ skip_current_scope = false
50
+ elsif node.selections.any?
51
+ current_depth -= 1
52
+ end
53
+ }
54
+
55
+ visitor [GraphQL::Language::Nodes::FragmentSpread] << -> (node, parent) {
56
+ fragments[current_field_scope] << node.name
57
+ }
58
+ end
59
+
60
+ private
61
+
62
+ def assert_under_max_depth(context, max_allowed_depth, depths, fragments)
63
+ context.operations.each do |op_name, operation|
64
+ op_depth = get_total_depth(op_name, depths, fragments)
65
+ if op_depth > max_allowed_depth
66
+ op_name ||= "operation"
67
+ context.errors << message("#{op_name} has depth of #{op_depth}, which exceeds max depth of #{max_allowed_depth}", operation)
68
+ end
69
+ end
70
+ end
71
+
72
+ # Get the total depth of a given fragment or operation
73
+ def get_total_depth(scope_name, depths, fragments)
74
+ own_fragments = fragments[scope_name]
75
+ depths[scope_name] + own_fragments.reduce(0) { |memo, frag_name| memo + get_total_depth(frag_name, depths, fragments) }
76
+ end
77
+ end
78
+ end
79
+ end