graphql 0.15.3 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql.rb +4 -1
  3. data/lib/graphql/analysis.rb +5 -0
  4. data/lib/graphql/analysis/analyze_query.rb +73 -0
  5. data/lib/graphql/analysis/max_query_complexity.rb +25 -0
  6. data/lib/graphql/analysis/max_query_depth.rb +25 -0
  7. data/lib/graphql/analysis/query_complexity.rb +122 -0
  8. data/lib/graphql/analysis/query_depth.rb +54 -0
  9. data/lib/graphql/analysis_error.rb +4 -0
  10. data/lib/graphql/base_type.rb +7 -0
  11. data/lib/graphql/define/assign_object_field.rb +2 -1
  12. data/lib/graphql/field.rb +25 -3
  13. data/lib/graphql/input_object_type.rb +1 -1
  14. data/lib/graphql/internal_representation.rb +2 -0
  15. data/lib/graphql/internal_representation/node.rb +81 -0
  16. data/lib/graphql/internal_representation/rewrite.rb +177 -0
  17. data/lib/graphql/language/visitor.rb +15 -9
  18. data/lib/graphql/object_type.rb +1 -1
  19. data/lib/graphql/query.rb +66 -7
  20. data/lib/graphql/query/context.rb +10 -3
  21. data/lib/graphql/query/directive_resolution.rb +5 -5
  22. data/lib/graphql/query/serial_execution.rb +5 -3
  23. data/lib/graphql/query/serial_execution/field_resolution.rb +22 -15
  24. data/lib/graphql/query/serial_execution/operation_resolution.rb +7 -5
  25. data/lib/graphql/query/serial_execution/selection_resolution.rb +20 -105
  26. data/lib/graphql/query/serial_execution/value_resolution.rb +15 -12
  27. data/lib/graphql/schema.rb +7 -2
  28. data/lib/graphql/schema/timeout_middleware.rb +67 -0
  29. data/lib/graphql/static_validation/all_rules.rb +0 -1
  30. data/lib/graphql/static_validation/type_stack.rb +7 -11
  31. data/lib/graphql/static_validation/validation_context.rb +11 -1
  32. data/lib/graphql/static_validation/validator.rb +14 -4
  33. data/lib/graphql/version.rb +1 -1
  34. data/readme.md +10 -9
  35. data/spec/graphql/analysis/analyze_query_spec.rb +50 -0
  36. data/spec/graphql/analysis/max_query_complexity_spec.rb +62 -0
  37. data/spec/graphql/{static_validation/rules/document_does_not_exceed_max_depth_spec.rb → analysis/max_query_depth_spec.rb} +20 -21
  38. data/spec/graphql/analysis/query_complexity_spec.rb +235 -0
  39. data/spec/graphql/analysis/query_depth_spec.rb +80 -0
  40. data/spec/graphql/directive_spec.rb +1 -0
  41. data/spec/graphql/internal_representation/rewrite_spec.rb +120 -0
  42. data/spec/graphql/introspection/schema_type_spec.rb +1 -0
  43. data/spec/graphql/language/visitor_spec.rb +14 -4
  44. data/spec/graphql/non_null_type_spec.rb +31 -0
  45. data/spec/graphql/query/context_spec.rb +24 -1
  46. data/spec/graphql/query_spec.rb +6 -2
  47. data/spec/graphql/schema/timeout_middleware_spec.rb +180 -0
  48. data/spec/graphql/static_validation/rules/argument_literals_are_compatible_spec.rb +1 -1
  49. data/spec/graphql/static_validation/rules/arguments_are_defined_spec.rb +1 -1
  50. data/spec/graphql/static_validation/rules/directives_are_defined_spec.rb +1 -1
  51. data/spec/graphql/static_validation/rules/directives_are_in_valid_locations_spec.rb +1 -1
  52. data/spec/graphql/static_validation/rules/fields_are_defined_on_type_spec.rb +1 -1
  53. data/spec/graphql/static_validation/rules/fields_have_appropriate_selections_spec.rb +1 -1
  54. data/spec/graphql/static_validation/rules/fields_will_merge_spec.rb +1 -1
  55. data/spec/graphql/static_validation/rules/fragment_spreads_are_possible_spec.rb +1 -1
  56. data/spec/graphql/static_validation/rules/fragment_types_exist_spec.rb +1 -1
  57. data/spec/graphql/static_validation/rules/fragments_are_finite_spec.rb +1 -1
  58. data/spec/graphql/static_validation/rules/fragments_are_on_composite_types_spec.rb +1 -1
  59. data/spec/graphql/static_validation/rules/fragments_are_used_spec.rb +1 -1
  60. data/spec/graphql/static_validation/rules/required_arguments_are_present_spec.rb +1 -1
  61. data/spec/graphql/static_validation/rules/variable_default_values_are_correctly_typed_spec.rb +1 -1
  62. data/spec/graphql/static_validation/rules/variable_usages_are_allowed_spec.rb +1 -1
  63. data/spec/graphql/static_validation/rules/variables_are_input_types_spec.rb +1 -1
  64. data/spec/graphql/static_validation/rules/variables_are_used_and_defined_spec.rb +1 -1
  65. data/spec/graphql/static_validation/validator_spec.rb +1 -1
  66. data/spec/support/dairy_app.rb +22 -1
  67. metadata +29 -5
  68. data/lib/graphql/static_validation/rules/document_does_not_exceed_max_depth.rb +0 -79
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8a448fdb0e6f68de9172e04ff16c5bcfe5844f5e
4
- data.tar.gz: 02b41b4bedeb44c838fc9d895ac70633c19e617e
3
+ metadata.gz: f8d3d315c998e0d44c6ebbcd186825a1d7c86639
4
+ data.tar.gz: adfa16f977635d988505202bee3ea8fe54ede003
5
5
  SHA512:
6
- metadata.gz: 5663e57b70cc3b3b3e93cdbde3a3b9e14dfa42cff4d2554b47f60187ffb0bba138c5ed8f94bc7a4f84ddc7a6afaff3423a6931aea8946b4e4c823b9024f9d8f2
7
- data.tar.gz: 8f65610a1923187c03a52847352ba66ed227094569b8191e8be7238a1ec578860756652195d1d27edb5e07c0bb17c3a273835a111259c65fcee7663706b722ac
6
+ metadata.gz: 34bad6ba6d2ff0e811b2d9abbddf3e834df69e8fb8d6a1b06581f446b8bffee325717df2731874bcec57581cac8d7808a9c97937d43e1d45386edf41f8824a4f
7
+ data.tar.gz: e11a5741e64138f68cac3ad51d14243bfa47c8f56b3b03144b685b1d2d71d4fea657221bcdffcfdaef5edd485b7df6262eb018b38ac4dcb0e01b22648c55a080
@@ -31,6 +31,7 @@ end
31
31
 
32
32
  # Order matters for these:
33
33
 
34
+ require "graphql/execution_error"
34
35
  require "graphql/define"
35
36
  require "graphql/base_type"
36
37
  require "graphql/object_type"
@@ -56,13 +57,15 @@ require "graphql/directive"
56
57
 
57
58
  require "graphql/introspection"
58
59
  require "graphql/language"
60
+ require "graphql/analysis"
59
61
  require "graphql/schema"
60
62
  require "graphql/schema/printer"
61
63
 
62
64
  # Order does not matter for these:
63
65
 
64
- require "graphql/execution_error"
66
+ require "graphql/analysis_error"
65
67
  require "graphql/invalid_null_error"
66
68
  require "graphql/query"
69
+ require "graphql/internal_representation"
67
70
  require "graphql/static_validation"
68
71
  require "graphql/version"
@@ -0,0 +1,5 @@
1
+ require "graphql/analysis/max_query_complexity"
2
+ require "graphql/analysis/max_query_depth"
3
+ require "graphql/analysis/query_complexity"
4
+ require "graphql/analysis/query_depth"
5
+ require "graphql/analysis/analyze_query"
@@ -0,0 +1,73 @@
1
+ module GraphQL
2
+ module Analysis
3
+ module_function
4
+ # Visit `query`'s internal representation, calling `analyzers` along the way.
5
+ #
6
+ # - First, query analyzers are initialized by calling `.initial_value(query)`, if they respond to that method.
7
+ # - Then, they receive `.call(memo, visit_type, irep_node)`, where visit type is `:enter` or `:leave`.
8
+ # - Last, they receive `.final_value(memo)`, if they respond to that method.
9
+ #
10
+ # It returns an array of final `memo` values in the order that `analyzers` were passed in.
11
+ #
12
+ # @param query [GraphQL::Query]
13
+ # @param analyzers [Array<#call>] Objects that respond to `#call(memo, visit_type, irep_node)`
14
+ # @return [Array<Any>] Results from those analyzers
15
+ def analyze_query(query, analyzers)
16
+ analyzers_and_values = analyzers.map { |r| initialize_reducer(r, query) }
17
+
18
+ irep = query.internal_representation
19
+
20
+ irep.each do |name, op_node|
21
+ reduce_node(op_node, analyzers_and_values)
22
+ end
23
+
24
+ analyzers_and_values.map { |(r, value)| finalize_reducer(r, value) }
25
+ end
26
+
27
+ private
28
+
29
+ module_function
30
+
31
+ # Enter the node, visit its children, then leave the node.
32
+ def reduce_node(irep_node, analyzers_and_values)
33
+ visit_analyzers(:enter, irep_node, analyzers_and_values)
34
+
35
+ irep_node.children.each do |name, child_irep_node|
36
+ reduce_node(child_irep_node, analyzers_and_values)
37
+ end
38
+
39
+ visit_analyzers(:leave, irep_node, analyzers_and_values)
40
+ end
41
+
42
+ def visit_analyzers(visit_type, irep_node, analyzers_and_values)
43
+ analyzers_and_values.each do |reducer_and_value|
44
+ reducer = reducer_and_value[0]
45
+ memo = reducer_and_value[1]
46
+ next_memo = reducer.call(memo, visit_type, irep_node)
47
+ reducer_and_value[1] = next_memo
48
+ end
49
+ end
50
+
51
+ # If the reducer has an `initial_value` method, call it and store
52
+ # the result as `memo`. Otherwise, use `nil` as memo.
53
+ # @return [Array<(#call, Any)>] reducer-memo pairs
54
+ def initialize_reducer(reducer, query)
55
+ if reducer.respond_to?(:initial_value)
56
+ [reducer, reducer.initial_value(query)]
57
+ else
58
+ [reducer, nil]
59
+ end
60
+ end
61
+
62
+ # If the reducer accepts `final_value`, send it the last memo value.
63
+ # Otherwise, use the last value from the traversal.
64
+ # @return [Any] final memo value
65
+ def finalize_reducer(reducer, reduced_value)
66
+ if reducer.respond_to?(:final_value)
67
+ reducer.final_value(reduced_value)
68
+ else
69
+ reduced_value
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,25 @@
1
+ require_relative "./query_complexity"
2
+ module GraphQL
3
+ module Analysis
4
+ # Used under the hood to implement complexity validation,
5
+ # see {Schema#max_complexity} and {Query#max_complexity}
6
+ #
7
+ # @example Assert max complexity of 10
8
+ # # DON'T actually do this, graphql-ruby
9
+ # # Does this for you based on your `max_complexity` setting
10
+ # MySchema.query_analyzers << GraphQL::Analysis::MaxQueryComplexity.new(10)
11
+ #
12
+ class MaxQueryComplexity < GraphQL::Analysis::QueryComplexity
13
+ def initialize(max_complexity)
14
+ disallow_excessive_complexity = -> (query, complexity) {
15
+ if complexity > max_complexity
16
+ GraphQL::AnalysisError.new("Query has complexity of #{complexity}, which exceeds max complexity of #{max_complexity}")
17
+ else
18
+ nil
19
+ end
20
+ }
21
+ super(&disallow_excessive_complexity)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ require_relative "./query_depth"
2
+ module GraphQL
3
+ module Analysis
4
+ # Used under the hood to implement depth validation,
5
+ # see {Schema#max_depth} and {Query#max_depth}
6
+ #
7
+ # @example Assert max depth of 10
8
+ # # DON'T actually do this, graphql-ruby
9
+ # # Does this for you based on your `max_depth` setting
10
+ # MySchema.query_analyzers << GraphQL::Analysis::MaxQueryDepth.new(10)
11
+ #
12
+ class MaxQueryDepth < GraphQL::Analysis::QueryDepth
13
+ def initialize(max_depth)
14
+ disallow_excessive_depth = -> (query, depth) {
15
+ if depth > max_depth
16
+ GraphQL::AnalysisError.new("Query has depth of #{depth}, which exceeds max depth of #{max_depth}")
17
+ else
18
+ nil
19
+ end
20
+ }
21
+ super(&disallow_excessive_depth)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,122 @@
1
+ module GraphQL
2
+ module Analysis
3
+ # Calculate the complexity of a query, using {Field#complexity} values.
4
+ #
5
+ # @example Log the complexity of incoming queries
6
+ # MySchema.query_analyzers << GraphQL::AnalysisQueryComplexity.new do |query, complexity|
7
+ # Rails.logger.info("Complexity: #{complexity}")
8
+ # end
9
+ #
10
+ class QueryComplexity
11
+ # @yield [query, complexity] Called for each query analyzed by the schema, before executing it
12
+ # @yieldparam query [GraphQL::Query] The query that was analyzed
13
+ # @yieldparam complexity [Numeric] The complexity for this query
14
+ def initialize(&block)
15
+ @complexity_handler = block
16
+ end
17
+
18
+ # State for the query complexity calcuation:
19
+ # - `query` is needed for variables, then passed to handler
20
+ # - `complexities_on_type` holds complexity scores for each type in an IRep node
21
+ def initial_value(query)
22
+ {
23
+ query: query,
24
+ complexities_on_type: [TypeComplexity.new],
25
+ }
26
+ end
27
+
28
+ # Implement the query analyzer API
29
+ def call(memo, visit_type, irep_node)
30
+ if irep_node.ast_node.is_a?(GraphQL::Language::Nodes::Field)
31
+ if visit_type == :enter
32
+ memo[:complexities_on_type].push(TypeComplexity.new)
33
+ else
34
+ type_complexities = memo[:complexities_on_type].pop
35
+ own_complexity = if GraphQL::Query::DirectiveResolution.include_node?(irep_node, memo[:query])
36
+ child_complexity = type_complexities.max_possible_complexity
37
+ get_complexity(irep_node, memo[:query], child_complexity)
38
+ else
39
+ 0
40
+ end
41
+ memo[:complexities_on_type].last.merge(irep_node.on_types, own_complexity)
42
+ end
43
+ end
44
+ memo
45
+ end
46
+
47
+ # Send the query and complexity to the block
48
+ # @return [Object, GraphQL::AnalysisError] Whatever the handler returns
49
+ def final_value(reduced_value)
50
+ total_complexity = reduced_value[:complexities_on_type].pop.max_possible_complexity
51
+ @complexity_handler.call(reduced_value[:query], total_complexity)
52
+ end
53
+
54
+ private
55
+
56
+ # Get a complexity value for a field,
57
+ # by getting the number or calling its proc
58
+ def get_complexity(irep_node, query, child_complexity)
59
+ field_defn = irep_node.definition
60
+ defined_complexity = field_defn.complexity
61
+ case defined_complexity
62
+ when Proc
63
+ args = query.arguments_for(irep_node)
64
+ defined_complexity.call(query.context, args, child_complexity)
65
+ when Numeric
66
+ defined_complexity + (child_complexity || 0)
67
+ else
68
+ raise("Invalid complexity: #{defined_complexity.inspect} on #{field_defn.name}")
69
+ end
70
+ end
71
+
72
+ # Selections on an object may apply differently depending on what is _actually_ returned by the resolve function.
73
+ # Find the maximum possible complexity among those combinations.
74
+ class TypeComplexity
75
+ def initialize
76
+ @types = Hash.new { |h, k| h[k] = 0 }
77
+ end
78
+
79
+ # Return the max possible complexity for types in this selection
80
+ def max_possible_complexity
81
+ max_complexity = 0
82
+
83
+ @types.each do |type_defn, own_complexity|
84
+ type_complexity = @types.reduce(0) do |memo, (other_type, other_complexity)|
85
+ if types_overlap?(type_defn, other_type)
86
+ memo + other_complexity
87
+ else
88
+ memo
89
+ end
90
+ end
91
+
92
+ if type_complexity > max_complexity
93
+ max_complexity = type_complexity
94
+ end
95
+ end
96
+ max_complexity
97
+ end
98
+
99
+ # Store the complexity score for each of `types`
100
+ def merge(types, complexity)
101
+ types.each { |t| @types[t] += complexity }
102
+ end
103
+
104
+ private
105
+ # True if:
106
+ # - type_1 is type_2
107
+ # - type_1 is a member of type_2's possible types
108
+ def types_overlap?(type_1, type_2)
109
+ if type_1 == type_2
110
+ true
111
+ elsif type_2.kind.union?
112
+ type_2.include?(type_1)
113
+ elsif type_1.kind.object? && type_2.kind.interface?
114
+ type_1.interfaces.include?(type_2)
115
+ else
116
+ false
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,54 @@
1
+ module GraphQL
2
+ module Analysis
3
+ # A query reducer for measuring the depth of a given query.
4
+ #
5
+ # @example Logging the depth of a query
6
+ # Schema.query_analyzers << GraphQL::Analysis::QueryDepth.new { |depth| puts "GraphQL query depth: #{depth}" }
7
+ # Schema.execute(query_str)
8
+ # # GraphQL query depth: 8
9
+ #
10
+ class QueryDepth
11
+ def initialize(&block)
12
+ @depth_handler = block
13
+ end
14
+
15
+ def initial_value(query)
16
+ {
17
+ max_depth: 0,
18
+ current_depth: 0,
19
+ skip_current_scope: false,
20
+ query: query,
21
+ }
22
+ end
23
+
24
+ def call(memo, visit_type, irep_node)
25
+ if irep_node.ast_node.is_a?(GraphQL::Language::Nodes::Field)
26
+ if visit_type == :enter
27
+ if GraphQL::Schema::DYNAMIC_FIELDS.include?(irep_node.definition.name)
28
+ # Don't validate introspection fields
29
+ memo[:skip_current_scope] = true
30
+ elsif memo[:skip_current_scope]
31
+ # we're inside an introspection query
32
+ elsif GraphQL::Query::DirectiveResolution.include_node?(irep_node, memo[:query])
33
+ memo[:current_depth] += 1
34
+ end
35
+ else
36
+ if GraphQL::Schema::DYNAMIC_FIELDS.include?(irep_node.definition.name)
37
+ memo[:skip_current_scope] = false
38
+ elsif GraphQL::Query::DirectiveResolution.include_node?(irep_node, memo[:query])
39
+ if memo[:max_depth] < memo[:current_depth]
40
+ memo[:max_depth] = memo[:current_depth]
41
+ end
42
+ memo[:current_depth] -= 1
43
+ end
44
+ end
45
+ end
46
+ memo
47
+ end
48
+
49
+ def final_value(memo)
50
+ @depth_handler.call(memo[:query], memo[:max_depth])
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,4 @@
1
+ module GraphQL
2
+ class AnalysisError < GraphQL::ExecutionError
3
+ end
4
+ end
@@ -89,6 +89,13 @@ module GraphQL
89
89
  coerce_non_null_input(value)
90
90
  end
91
91
 
92
+ # Types with fields may override this
93
+ # @param name [String] field name to lookup for this type
94
+ # @return [GraphQL::Field, nil]
95
+ def get_field(name)
96
+ nil
97
+ end
98
+
92
99
  # During schema definition, types can be defined inside procs or as strings.
93
100
  # This function converts it to a type instance
94
101
  # @return [GraphQL::BaseType]
@@ -2,7 +2,7 @@ module GraphQL
2
2
  module Define
3
3
  # Turn field configs into a {GraphQL::Field} and attach it to a {GraphQL::ObjectType} or {GraphQL::InterfaceType}
4
4
  module AssignObjectField
5
- def self.call(fields_type, name, type = nil, desc = nil, field: nil, deprecation_reason: nil, property: nil, &block)
5
+ def self.call(fields_type, name, type = nil, desc = nil, field: nil, deprecation_reason: nil, property: nil, complexity: nil, &block)
6
6
  if block_given?
7
7
  field = GraphQL::Field.define(&block)
8
8
  else
@@ -11,6 +11,7 @@ module GraphQL
11
11
  type && field.type = type
12
12
  desc && field.description = desc
13
13
  property && field.property = property
14
+ complexity && field.complexity = complexity
14
15
  deprecation_reason && field.deprecation_reason = deprecation_reason
15
16
  field.name ||= name.to_s
16
17
  fields_type.fields[name.to_s] = field
@@ -3,7 +3,6 @@ module GraphQL
3
3
  #
4
4
  # They're usually created with the `field` helper. If you create it by hand, make sure {#name} is a String.
5
5
  #
6
- #
7
6
  # @example creating a field
8
7
  # GraphQL::ObjectType.define do
9
8
  # field :name, types.String, "The name of this thing "
@@ -38,20 +37,43 @@ module GraphQL
38
37
  # field :name, field: name_field
39
38
  # end
40
39
  #
40
+ # @example Custom complexity values
41
+ # # Complexity can be a number or a proc.
42
+ #
43
+ # # Complexity can be defined with a keyword:
44
+ # field :expensive_calculation, !types.Int, complexity: 10
45
+ #
46
+ # # Or inside the block:
47
+ # field :expensive_calculation_2, !types.Int do
48
+ # complexity -> (ctx, args, child_complexity) { ctx[:current_user].staff? ? 0 : 10 }
49
+ # end
50
+ #
51
+ # @example Calculating the complexity of a list field
52
+ # field :items, types[ItemType] do
53
+ # argument :limit, !types.Int
54
+ # # Mulitply the child complexity by the possible items on the list
55
+ # complexity -> (ctx, args, child_complexity) { child_complexity * args[:limit] }
56
+ # end
57
+ #
41
58
  class Field
42
59
  include GraphQL::Define::InstanceDefinable
43
- accepts_definitions :name, :description, :resolve, :type, :property, :deprecation_reason, argument: GraphQL::Define::AssignArgument
60
+ accepts_definitions :name, :description, :resolve, :type, :property, :deprecation_reason, :complexity, argument: GraphQL::Define::AssignArgument
44
61
 
45
62
  attr_accessor :deprecation_reason, :name, :description, :property
63
+
46
64
  attr_reader :resolve_proc
47
65
 
48
66
  # @return [String] The name of this field on its {GraphQL::ObjectType} (or {GraphQL::InterfaceType})
49
67
  attr_reader :name
50
68
 
51
- # @return [Hash<String, GraphQL::Argument>] Map String argument names to their {GraphQL::Argument} implementations
69
+ # @return [Hash<String => GraphQL::Argument>] Map String argument names to their {GraphQL::Argument} implementations
52
70
  attr_accessor :arguments
53
71
 
72
+ # @return [Numeric, Proc] The complexity for this field (default: 1), as a constant or a proc like `-> (query_ctx, args, child_complexity) { } # Numeric`
73
+ attr_accessor :complexity
74
+
54
75
  def initialize
76
+ @complexity = 1
55
77
  @arguments = {}
56
78
  @resolve_proc = build_default_resolver
57
79
  end
@@ -14,7 +14,7 @@ module GraphQL
14
14
  argument: GraphQL::Define::AssignArgument
15
15
  )
16
16
 
17
- # @return [Hash<String, GraphQL::Argument>] Map String argument names to their {GraphQL::Argument} implementations
17
+ # @return [Hash<String => GraphQL::Argument>] Map String argument names to their {GraphQL::Argument} implementations
18
18
  attr_accessor :arguments
19
19
 
20
20
  alias :input_fields :arguments