graphql 2.3.4 → 2.3.6

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/generators/graphql/install/mutation_root_generator.rb +2 -2
  3. data/lib/graphql/analysis/analyzer.rb +89 -0
  4. data/lib/graphql/analysis/field_usage.rb +82 -0
  5. data/lib/graphql/analysis/max_query_complexity.rb +20 -0
  6. data/lib/graphql/analysis/max_query_depth.rb +20 -0
  7. data/lib/graphql/analysis/query_complexity.rb +183 -0
  8. data/lib/graphql/analysis/{ast/query_depth.rb → query_depth.rb} +23 -25
  9. data/lib/graphql/analysis/visitor.rb +282 -0
  10. data/lib/graphql/analysis.rb +92 -1
  11. data/lib/graphql/dataloader/async_dataloader.rb +2 -0
  12. data/lib/graphql/dataloader/null_dataloader.rb +1 -1
  13. data/lib/graphql/execution/interpreter/runtime/graphql_result.rb +7 -4
  14. data/lib/graphql/execution/interpreter/runtime.rb +40 -59
  15. data/lib/graphql/execution/interpreter.rb +2 -2
  16. data/lib/graphql/language/nodes.rb +17 -22
  17. data/lib/graphql/language/parser.rb +54 -13
  18. data/lib/graphql/query/validation_pipeline.rb +2 -2
  19. data/lib/graphql/query.rb +1 -1
  20. data/lib/graphql/rubocop/graphql/base_cop.rb +1 -1
  21. data/lib/graphql/schema/addition.rb +21 -11
  22. data/lib/graphql/schema/argument.rb +19 -5
  23. data/lib/graphql/schema/directive.rb +2 -0
  24. data/lib/graphql/schema/field.rb +8 -0
  25. data/lib/graphql/schema/has_single_input_argument.rb +1 -0
  26. data/lib/graphql/schema/input_object.rb +1 -0
  27. data/lib/graphql/schema/introspection_system.rb +2 -2
  28. data/lib/graphql/schema/late_bound_type.rb +4 -0
  29. data/lib/graphql/schema/list.rb +2 -2
  30. data/lib/graphql/schema/member/has_arguments.rb +2 -35
  31. data/lib/graphql/schema/member/has_directives.rb +1 -1
  32. data/lib/graphql/schema/member/relay_shortcuts.rb +1 -1
  33. data/lib/graphql/schema/member/type_system_helpers.rb +1 -2
  34. data/lib/graphql/schema/resolver.rb +1 -0
  35. data/lib/graphql/schema/warden.rb +2 -3
  36. data/lib/graphql/schema.rb +20 -20
  37. data/lib/graphql/static_validation/rules/fields_will_merge.rb +1 -1
  38. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +1 -1
  39. data/lib/graphql/subscriptions/broadcast_analyzer.rb +1 -1
  40. data/lib/graphql/subscriptions.rb +1 -1
  41. data/lib/graphql/type_kinds.rb +1 -1
  42. data/lib/graphql/version.rb +1 -1
  43. data/lib/graphql.rb +0 -8
  44. metadata +10 -11
  45. data/lib/graphql/analysis/ast/analyzer.rb +0 -91
  46. data/lib/graphql/analysis/ast/field_usage.rb +0 -84
  47. data/lib/graphql/analysis/ast/max_query_complexity.rb +0 -22
  48. data/lib/graphql/analysis/ast/max_query_depth.rb +0 -22
  49. data/lib/graphql/analysis/ast/query_complexity.rb +0 -185
  50. data/lib/graphql/analysis/ast/visitor.rb +0 -284
  51. data/lib/graphql/analysis/ast.rb +0 -94
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c8f2d38dd0c349521f7f22050bb70cee1f06a7832de74d5afe523a35310ed318
4
- data.tar.gz: da047a5e11a1441060411c47468daad16300db1dc7064ab1b1a76d2e519e76fd
3
+ metadata.gz: 96871397d0d4258c406b0492a304ca71129cc3be7effb2892ee225896e8e266f
4
+ data.tar.gz: b3d7b30d09f4e19505549ce6292cdc7643971cfe285f498be98f585531ae8c3c
5
5
  SHA512:
6
- metadata.gz: 73bd380530a4183241587083beb2ef6c96709657fb18e702a294a37c22f26e2141b2111b96c79570b9588d330e9c8d851fd532cdaf6d6fbd55f9dd5b30c6fe4e
7
- data.tar.gz: 917e88d0cec3cbc760ec89714cf31325088f94f0a93094b3fcbf1cc3c07f52a99e30f0e418e561518c0a7deef0358dfa0ae457b8bad88fb50883aacb9d7dae7f
6
+ metadata.gz: 6b79b06fecef3325b8df579183a0cf71c137b77a27003fead0abb6a198731243aa6b55d43c022b62cac1dbc2f6544c3d28dc5fadfa97f33b2043a6e8a18020a8
7
+ data.tar.gz: b79eeb7912a70ddb7b072d095d2b61e1e8cc84cf50a975e0227dbd6f0f17728fa61d17f0269fbaa1ebdd219d13a3453e1b67b59ede085d020e3ca105de94186a
@@ -9,7 +9,7 @@ module Graphql
9
9
  class MutationRootGenerator < Rails::Generators::Base
10
10
  include Core
11
11
 
12
- desc "Create mutation base type, mutation root tipe, and adds the latter to the schema"
12
+ desc "Create mutation base type, mutation root type, and adds the latter to the schema"
13
13
  source_root File.expand_path('../templates', __FILE__)
14
14
 
15
15
  class_option :schema,
@@ -31,4 +31,4 @@ module Graphql
31
31
  end
32
32
  end
33
33
  end
34
- end
34
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ module Analysis
4
+ # Query analyzer for query ASTs. Query analyzers respond to visitor style methods
5
+ # but are prefixed by `enter` and `leave`.
6
+ #
7
+ # When an analyzer is initialized with a Multiplex, you can always get the current query from
8
+ # `visitor.query` in the visit methods.
9
+ #
10
+ # @param [GraphQL::Query, GraphQL::Execution::Multiplex] The query or multiplex to analyze
11
+ class Analyzer
12
+ def initialize(subject)
13
+ @subject = subject
14
+
15
+ if subject.is_a?(GraphQL::Query)
16
+ @query = subject
17
+ @multiplex = nil
18
+ else
19
+ @multiplex = subject
20
+ @query = nil
21
+ end
22
+ end
23
+
24
+ # Analyzer hook to decide at analysis time whether a query should
25
+ # be analyzed or not.
26
+ # @return [Boolean] If the query should be analyzed or not
27
+ def analyze?
28
+ true
29
+ end
30
+
31
+ # Analyzer hook to decide at analysis time whether analysis
32
+ # requires a visitor pass; can be disabled for precomputed results.
33
+ # @return [Boolean] If analysis requires visitation or not
34
+ def visit?
35
+ true
36
+ end
37
+
38
+ # The result for this analyzer. Returning {GraphQL::AnalysisError} results
39
+ # in a query error.
40
+ # @return [Any] The analyzer result
41
+ def result
42
+ raise GraphQL::RequiredImplementationMissingError
43
+ end
44
+
45
+ class << self
46
+ private
47
+
48
+ def build_visitor_hooks(member_name)
49
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
50
+ def on_enter_#{member_name}(node, parent, visitor)
51
+ end
52
+
53
+ def on_leave_#{member_name}(node, parent, visitor)
54
+ end
55
+ EOS
56
+ end
57
+ end
58
+
59
+ build_visitor_hooks :argument
60
+ build_visitor_hooks :directive
61
+ build_visitor_hooks :document
62
+ build_visitor_hooks :enum
63
+ build_visitor_hooks :field
64
+ build_visitor_hooks :fragment_spread
65
+ build_visitor_hooks :inline_fragment
66
+ build_visitor_hooks :input_object
67
+ build_visitor_hooks :list_type
68
+ build_visitor_hooks :non_null_type
69
+ build_visitor_hooks :null_value
70
+ build_visitor_hooks :operation_definition
71
+ build_visitor_hooks :type_name
72
+ build_visitor_hooks :variable_definition
73
+ build_visitor_hooks :variable_identifier
74
+ build_visitor_hooks :abstract_node
75
+
76
+ protected
77
+
78
+ # @return [GraphQL::Query, GraphQL::Execution::Multiplex] Whatever this analyzer is analyzing
79
+ attr_reader :subject
80
+
81
+ # @return [GraphQL::Query, nil] `nil` if this analyzer is visiting a multiplex
82
+ # (When this is `nil`, use `visitor.query` inside visit methods to get the current query)
83
+ attr_reader :query
84
+
85
+ # @return [GraphQL::Execution::Multiplex, nil] `nil` if this analyzer is visiting a query
86
+ attr_reader :multiplex
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ module Analysis
4
+ class FieldUsage < Analyzer
5
+ def initialize(query)
6
+ super
7
+ @used_fields = Set.new
8
+ @used_deprecated_fields = Set.new
9
+ @used_deprecated_arguments = Set.new
10
+ @used_deprecated_enum_values = Set.new
11
+ end
12
+
13
+ def on_leave_field(node, parent, visitor)
14
+ field_defn = visitor.field_definition
15
+ field = "#{visitor.parent_type_definition.graphql_name}.#{field_defn.graphql_name}"
16
+ @used_fields << field
17
+ @used_deprecated_fields << field if field_defn.deprecation_reason
18
+ arguments = visitor.query.arguments_for(node, field_defn)
19
+ # If there was an error when preparing this argument object,
20
+ # then this might be an error or something:
21
+ if arguments.respond_to?(:argument_values)
22
+ extract_deprecated_arguments(arguments.argument_values)
23
+ end
24
+ end
25
+
26
+ def result
27
+ {
28
+ used_fields: @used_fields.to_a,
29
+ used_deprecated_fields: @used_deprecated_fields.to_a,
30
+ used_deprecated_arguments: @used_deprecated_arguments.to_a,
31
+ used_deprecated_enum_values: @used_deprecated_enum_values.to_a,
32
+ }
33
+ end
34
+
35
+ private
36
+
37
+ def extract_deprecated_arguments(argument_values)
38
+ argument_values.each_pair do |_argument_name, argument|
39
+ if argument.definition.deprecation_reason
40
+ @used_deprecated_arguments << argument.definition.path
41
+ end
42
+
43
+ arg_val = argument.value
44
+
45
+ next if arg_val.nil?
46
+
47
+ argument_type = argument.definition.type
48
+ if argument_type.non_null?
49
+ argument_type = argument_type.of_type
50
+ end
51
+
52
+ if argument_type.kind.input_object?
53
+ extract_deprecated_arguments(argument.original_value.arguments.argument_values) # rubocop:disable Development/ContextIsPassedCop -- runtime args instance
54
+ elsif argument_type.kind.enum?
55
+ extract_deprecated_enum_value(argument_type, arg_val)
56
+ elsif argument_type.list?
57
+ inner_type = argument_type.unwrap
58
+ case inner_type.kind
59
+ when TypeKinds::INPUT_OBJECT
60
+ argument.original_value.each do |value|
61
+ extract_deprecated_arguments(value.arguments.argument_values) # rubocop:disable Development/ContextIsPassedCop -- runtime args instance
62
+ end
63
+ when TypeKinds::ENUM
64
+ arg_val.each do |value|
65
+ extract_deprecated_enum_value(inner_type, value)
66
+ end
67
+ else
68
+ # Not a kind of input that we track
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ def extract_deprecated_enum_value(enum_type, value)
75
+ enum_value = @query.warden.enum_values(enum_type).find { |ev| ev.value == value }
76
+ if enum_value&.deprecation_reason
77
+ @used_deprecated_enum_values << enum_value.path
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
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
+ class MaxQueryComplexity < QueryComplexity
7
+ def result
8
+ return if subject.max_complexity.nil?
9
+
10
+ total_complexity = max_possible_complexity
11
+
12
+ if total_complexity > subject.max_complexity
13
+ GraphQL::AnalysisError.new("Query has complexity of #{total_complexity}, which exceeds max complexity of #{subject.max_complexity}")
14
+ else
15
+ nil
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ module Analysis
4
+ class MaxQueryDepth < QueryDepth
5
+ def result
6
+ configured_max_depth = if query
7
+ query.max_depth
8
+ else
9
+ multiplex.schema.max_depth
10
+ end
11
+
12
+ if configured_max_depth && @max_depth > configured_max_depth
13
+ GraphQL::AnalysisError.new("Query has depth of #{@max_depth}, which exceeds max depth of #{configured_max_depth}")
14
+ else
15
+ nil
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+ module GraphQL
3
+ module Analysis
4
+ # Calculate the complexity of a query, using {Field#complexity} values.
5
+ class QueryComplexity < Analyzer
6
+ # State for the query complexity calculation:
7
+ # - `complexities_on_type` holds complexity scores for each type
8
+ def initialize(query)
9
+ super
10
+ @skip_introspection_fields = !query.schema.max_complexity_count_introspection_fields
11
+ @complexities_on_type_by_query = {}
12
+ end
13
+
14
+ # Override this method to use the complexity result
15
+ def result
16
+ max_possible_complexity
17
+ end
18
+
19
+ # ScopedTypeComplexity models a tree of GraphQL types mapped to inner selections, ie:
20
+ # Hash<GraphQL::BaseType, Hash<String, ScopedTypeComplexity>>
21
+ class ScopedTypeComplexity < Hash
22
+ # A proc for defaulting empty namespace requests as a new scope hash.
23
+ DEFAULT_PROC = ->(h, k) { h[k] = {} }
24
+
25
+ attr_reader :field_definition, :response_path, :query
26
+
27
+ # @param parent_type [Class] The owner of `field_definition`
28
+ # @param field_definition [GraphQL::Field, GraphQL::Schema::Field] Used for getting the `.complexity` configuration
29
+ # @param query [GraphQL::Query] Used for `query.possible_types`
30
+ # @param response_path [Array<String>] The path to the response key for the field
31
+ # @return [Hash<GraphQL::BaseType, Hash<String, ScopedTypeComplexity>>]
32
+ def initialize(parent_type, field_definition, query, response_path)
33
+ super(&DEFAULT_PROC)
34
+ @parent_type = parent_type
35
+ @field_definition = field_definition
36
+ @query = query
37
+ @response_path = response_path
38
+ @nodes = []
39
+ end
40
+
41
+ # @return [Array<GraphQL::Language::Nodes::Field>]
42
+ attr_reader :nodes
43
+
44
+ def own_complexity(child_complexity)
45
+ @field_definition.calculate_complexity(query: @query, nodes: @nodes, child_complexity: child_complexity)
46
+ end
47
+ end
48
+
49
+ def on_enter_field(node, parent, visitor)
50
+ # We don't want to visit fragment definitions,
51
+ # we'll visit them when we hit the spreads instead
52
+ return if visitor.visiting_fragment_definition?
53
+ return if visitor.skipping?
54
+ return if @skip_introspection_fields && visitor.field_definition.introspection?
55
+ parent_type = visitor.parent_type_definition
56
+ field_key = node.alias || node.name
57
+
58
+ # Find or create a complexity scope stack for this query.
59
+ scopes_stack = @complexities_on_type_by_query[visitor.query] ||= [ScopedTypeComplexity.new(nil, nil, query, visitor.response_path)]
60
+
61
+ # Find or create the complexity costing node for this field.
62
+ scope = scopes_stack.last[parent_type][field_key] ||= ScopedTypeComplexity.new(parent_type, visitor.field_definition, visitor.query, visitor.response_path)
63
+ scope.nodes.push(node)
64
+ scopes_stack.push(scope)
65
+ end
66
+
67
+ def on_leave_field(node, parent, visitor)
68
+ # We don't want to visit fragment definitions,
69
+ # we'll visit them when we hit the spreads instead
70
+ return if visitor.visiting_fragment_definition?
71
+ return if visitor.skipping?
72
+ return if @skip_introspection_fields && visitor.field_definition.introspection?
73
+ scopes_stack = @complexities_on_type_by_query[visitor.query]
74
+ scopes_stack.pop
75
+ end
76
+
77
+ private
78
+
79
+ # @return [Integer]
80
+ def max_possible_complexity
81
+ @complexities_on_type_by_query.reduce(0) do |total, (query, scopes_stack)|
82
+ total + merged_max_complexity_for_scopes(query, [scopes_stack.first])
83
+ end
84
+ end
85
+
86
+ # @param query [GraphQL::Query] Used for `query.possible_types`
87
+ # @param scopes [Array<ScopedTypeComplexity>] Array of scoped type complexities
88
+ # @return [Integer]
89
+ def merged_max_complexity_for_scopes(query, scopes)
90
+ # Aggregate a set of all possible scope types encountered (scope keys).
91
+ # Use a hash, but ignore the values; it's just a fast way to work with the keys.
92
+ possible_scope_types = scopes.each_with_object({}) do |scope, memo|
93
+ memo.merge!(scope)
94
+ end
95
+
96
+ # Expand abstract scope types into their concrete implementations;
97
+ # overlapping abstracts coalesce through their intersecting types.
98
+ possible_scope_types.keys.each do |possible_scope_type|
99
+ next unless possible_scope_type.kind.abstract?
100
+
101
+ query.possible_types(possible_scope_type).each do |impl_type|
102
+ possible_scope_types[impl_type] ||= true
103
+ end
104
+ possible_scope_types.delete(possible_scope_type)
105
+ end
106
+
107
+ # Aggregate the lexical selections that may apply to each possible type,
108
+ # and then return the maximum cost among possible typed selections.
109
+ possible_scope_types.each_key.reduce(0) do |max, possible_scope_type|
110
+ # Collect inner selections from all scopes that intersect with this possible type.
111
+ all_inner_selections = scopes.each_with_object([]) do |scope, memo|
112
+ scope.each do |scope_type, inner_selections|
113
+ memo << inner_selections if types_intersect?(query, scope_type, possible_scope_type)
114
+ end
115
+ end
116
+
117
+ # Find the maximum complexity for the scope type among possible lexical branches.
118
+ complexity = merged_max_complexity(query, all_inner_selections)
119
+ complexity > max ? complexity : max
120
+ end
121
+ end
122
+
123
+ def types_intersect?(query, a, b)
124
+ return true if a == b
125
+
126
+ a_types = query.possible_types(a)
127
+ query.possible_types(b).any? { |t| a_types.include?(t) }
128
+ end
129
+
130
+ # A hook which is called whenever a field's max complexity is calculated.
131
+ # Override this method to capture individual field complexity details.
132
+ #
133
+ # @param scoped_type_complexity [ScopedTypeComplexity]
134
+ # @param max_complexity [Numeric] Field's maximum complexity including child complexity
135
+ # @param child_complexity [Numeric, nil] Field's child complexity
136
+ def field_complexity(scoped_type_complexity, max_complexity:, child_complexity: nil)
137
+ end
138
+
139
+ # @param inner_selections [Array<Hash<String, ScopedTypeComplexity>>] Field selections for a scope
140
+ # @return [Integer] Total complexity value for all these selections in the parent scope
141
+ def merged_max_complexity(query, inner_selections)
142
+ # Aggregate a set of all unique field selection keys across all scopes.
143
+ # Use a hash, but ignore the values; it's just a fast way to work with the keys.
144
+ unique_field_keys = inner_selections.each_with_object({}) do |inner_selection, memo|
145
+ memo.merge!(inner_selection)
146
+ end
147
+
148
+ # Add up the total cost for each unique field name's coalesced selections
149
+ unique_field_keys.each_key.reduce(0) do |total, field_key|
150
+ composite_scopes = nil
151
+ field_cost = 0
152
+
153
+ # Collect composite selection scopes for further aggregation,
154
+ # leaf selections report their costs directly.
155
+ inner_selections.each do |inner_selection|
156
+ child_scope = inner_selection[field_key]
157
+ next unless child_scope
158
+
159
+ # Empty child scopes are leaf nodes with zero child complexity.
160
+ if child_scope.empty?
161
+ field_cost = child_scope.own_complexity(0)
162
+ field_complexity(child_scope, max_complexity: field_cost, child_complexity: nil)
163
+ else
164
+ composite_scopes ||= []
165
+ composite_scopes << child_scope
166
+ end
167
+ end
168
+
169
+ if composite_scopes
170
+ child_complexity = merged_max_complexity_for_scopes(query, composite_scopes)
171
+
172
+ # This is the last composite scope visited; assume it's representative (for backwards compatibility).
173
+ # Note: it would be more correct to score each composite scope and use the maximum possibility.
174
+ field_cost = composite_scopes.last.own_complexity(child_complexity)
175
+ field_complexity(composite_scopes.last, max_complexity: field_cost, child_complexity: child_complexity)
176
+ end
177
+
178
+ total + field_cost
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
@@ -23,37 +23,35 @@ module GraphQL
23
23
  # Schema.execute(query_str)
24
24
  # # GraphQL query depth: 8
25
25
  #
26
- module AST
27
- class QueryDepth < Analyzer
28
- def initialize(query)
29
- @max_depth = 0
30
- @current_depth = 0
31
- @count_introspection_fields = query.schema.count_introspection_fields
32
- super
33
- end
26
+ class QueryDepth < Analyzer
27
+ def initialize(query)
28
+ @max_depth = 0
29
+ @current_depth = 0
30
+ @count_introspection_fields = query.schema.count_introspection_fields
31
+ super
32
+ end
34
33
 
35
- def on_enter_field(node, parent, visitor)
36
- return if visitor.skipping? ||
37
- visitor.visiting_fragment_definition? ||
38
- (@count_introspection_fields == false && visitor.field_definition.introspection?)
34
+ def on_enter_field(node, parent, visitor)
35
+ return if visitor.skipping? ||
36
+ visitor.visiting_fragment_definition? ||
37
+ (@count_introspection_fields == false && visitor.field_definition.introspection?)
39
38
 
40
- @current_depth += 1
41
- end
39
+ @current_depth += 1
40
+ end
42
41
 
43
- def on_leave_field(node, parent, visitor)
44
- return if visitor.skipping? ||
45
- visitor.visiting_fragment_definition? ||
46
- (@count_introspection_fields == false && visitor.field_definition.introspection?)
42
+ def on_leave_field(node, parent, visitor)
43
+ return if visitor.skipping? ||
44
+ visitor.visiting_fragment_definition? ||
45
+ (@count_introspection_fields == false && visitor.field_definition.introspection?)
47
46
 
48
- if @max_depth < @current_depth
49
- @max_depth = @current_depth
50
- end
51
- @current_depth -= 1
47
+ if @max_depth < @current_depth
48
+ @max_depth = @current_depth
52
49
  end
50
+ @current_depth -= 1
51
+ end
53
52
 
54
- def result
55
- @max_depth
56
- end
53
+ def result
54
+ @max_depth
57
55
  end
58
56
  end
59
57
  end