graphql 0.15.3 → 0.16.0

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