graphql 0.11.0 → 0.11.1

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