graphql 2.0.27 → 2.2.6

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 (130) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/install/templates/base_mutation.erb +2 -0
  3. data/lib/generators/graphql/install/templates/mutation_type.erb +2 -0
  4. data/lib/generators/graphql/install_generator.rb +3 -0
  5. data/lib/generators/graphql/templates/base_argument.erb +2 -0
  6. data/lib/generators/graphql/templates/base_connection.erb +2 -0
  7. data/lib/generators/graphql/templates/base_edge.erb +2 -0
  8. data/lib/generators/graphql/templates/base_enum.erb +2 -0
  9. data/lib/generators/graphql/templates/base_field.erb +2 -0
  10. data/lib/generators/graphql/templates/base_input_object.erb +2 -0
  11. data/lib/generators/graphql/templates/base_interface.erb +2 -0
  12. data/lib/generators/graphql/templates/base_object.erb +2 -0
  13. data/lib/generators/graphql/templates/base_resolver.erb +6 -0
  14. data/lib/generators/graphql/templates/base_scalar.erb +2 -0
  15. data/lib/generators/graphql/templates/base_union.erb +2 -0
  16. data/lib/generators/graphql/templates/graphql_controller.erb +2 -0
  17. data/lib/generators/graphql/templates/loader.erb +2 -0
  18. data/lib/generators/graphql/templates/mutation.erb +2 -0
  19. data/lib/generators/graphql/templates/node_type.erb +2 -0
  20. data/lib/generators/graphql/templates/query_type.erb +2 -0
  21. data/lib/generators/graphql/templates/schema.erb +2 -0
  22. data/lib/graphql/analysis/ast/analyzer.rb +7 -0
  23. data/lib/graphql/analysis/ast/field_usage.rb +32 -7
  24. data/lib/graphql/analysis/ast/query_complexity.rb +80 -128
  25. data/lib/graphql/analysis/ast/query_depth.rb +7 -2
  26. data/lib/graphql/analysis/ast/visitor.rb +2 -2
  27. data/lib/graphql/analysis/ast.rb +21 -11
  28. data/lib/graphql/backtrace/trace.rb +12 -15
  29. data/lib/graphql/coercion_error.rb +1 -9
  30. data/lib/graphql/dataloader/async_dataloader.rb +85 -0
  31. data/lib/graphql/dataloader/source.rb +11 -3
  32. data/lib/graphql/dataloader.rb +109 -142
  33. data/lib/graphql/duration_encoding_error.rb +16 -0
  34. data/lib/graphql/execution/interpreter/runtime/graphql_result.rb +170 -0
  35. data/lib/graphql/execution/interpreter/runtime.rb +70 -248
  36. data/lib/graphql/execution/interpreter.rb +91 -157
  37. data/lib/graphql/execution/lookahead.rb +88 -21
  38. data/lib/graphql/introspection/dynamic_fields.rb +1 -1
  39. data/lib/graphql/introspection/entry_points.rb +11 -5
  40. data/lib/graphql/introspection/schema_type.rb +3 -1
  41. data/lib/graphql/language/block_string.rb +34 -18
  42. data/lib/graphql/language/definition_slice.rb +1 -1
  43. data/lib/graphql/language/document_from_schema_definition.rb +37 -37
  44. data/lib/graphql/language/lexer.rb +271 -177
  45. data/lib/graphql/language/nodes.rb +74 -56
  46. data/lib/graphql/language/parser.rb +697 -1986
  47. data/lib/graphql/language/printer.rb +299 -146
  48. data/lib/graphql/language/sanitized_printer.rb +20 -22
  49. data/lib/graphql/language/static_visitor.rb +167 -0
  50. data/lib/graphql/language/visitor.rb +20 -81
  51. data/lib/graphql/language.rb +1 -0
  52. data/lib/graphql/load_application_object_failed_error.rb +5 -1
  53. data/lib/graphql/pagination/array_connection.rb +3 -3
  54. data/lib/graphql/pagination/connection.rb +28 -1
  55. data/lib/graphql/pagination/mongoid_relation_connection.rb +1 -2
  56. data/lib/graphql/pagination/relation_connection.rb +3 -3
  57. data/lib/graphql/query/context/scoped_context.rb +101 -0
  58. data/lib/graphql/query/context.rb +36 -98
  59. data/lib/graphql/query/null_context.rb +4 -11
  60. data/lib/graphql/query/validation_pipeline.rb +2 -2
  61. data/lib/graphql/query/variables.rb +3 -3
  62. data/lib/graphql/query.rb +13 -22
  63. data/lib/graphql/railtie.rb +9 -6
  64. data/lib/graphql/rake_task.rb +3 -12
  65. data/lib/graphql/schema/argument.rb +6 -1
  66. data/lib/graphql/schema/build_from_definition.rb +0 -11
  67. data/lib/graphql/schema/directive/one_of.rb +12 -0
  68. data/lib/graphql/schema/directive/specified_by.rb +14 -0
  69. data/lib/graphql/schema/directive.rb +1 -1
  70. data/lib/graphql/schema/enum.rb +3 -3
  71. data/lib/graphql/schema/field/connection_extension.rb +1 -15
  72. data/lib/graphql/schema/field/scope_extension.rb +8 -1
  73. data/lib/graphql/schema/field.rb +8 -5
  74. data/lib/graphql/schema/has_single_input_argument.rb +156 -0
  75. data/lib/graphql/schema/input_object.rb +2 -2
  76. data/lib/graphql/schema/interface.rb +10 -10
  77. data/lib/graphql/schema/introspection_system.rb +2 -0
  78. data/lib/graphql/schema/loader.rb +0 -2
  79. data/lib/graphql/schema/member/base_dsl_methods.rb +2 -1
  80. data/lib/graphql/schema/member/has_arguments.rb +61 -38
  81. data/lib/graphql/schema/member/has_fields.rb +8 -5
  82. data/lib/graphql/schema/member/has_interfaces.rb +23 -9
  83. data/lib/graphql/schema/member/scoped.rb +19 -0
  84. data/lib/graphql/schema/member/validates_input.rb +3 -3
  85. data/lib/graphql/schema/object.rb +8 -0
  86. data/lib/graphql/schema/printer.rb +8 -7
  87. data/lib/graphql/schema/relay_classic_mutation.rb +6 -128
  88. data/lib/graphql/schema/resolver.rb +7 -3
  89. data/lib/graphql/schema/scalar.rb +3 -3
  90. data/lib/graphql/schema/subscription.rb +11 -4
  91. data/lib/graphql/schema/union.rb +1 -1
  92. data/lib/graphql/schema/warden.rb +96 -94
  93. data/lib/graphql/schema.rb +219 -72
  94. data/lib/graphql/static_validation/all_rules.rb +1 -1
  95. data/lib/graphql/static_validation/base_visitor.rb +1 -1
  96. data/lib/graphql/static_validation/literal_validator.rb +1 -1
  97. data/lib/graphql/static_validation/rules/fields_will_merge.rb +1 -1
  98. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +1 -1
  99. data/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb +1 -1
  100. data/lib/graphql/static_validation/validation_context.rb +5 -5
  101. data/lib/graphql/static_validation/validator.rb +3 -0
  102. data/lib/graphql/static_validation.rb +0 -1
  103. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +3 -2
  104. data/lib/graphql/subscriptions/event.rb +8 -2
  105. data/lib/graphql/subscriptions.rb +14 -12
  106. data/lib/graphql/testing/helpers.rb +125 -0
  107. data/lib/graphql/testing.rb +2 -0
  108. data/lib/graphql/tracing/appoptics_trace.rb +2 -2
  109. data/lib/graphql/tracing/appoptics_tracing.rb +2 -2
  110. data/lib/graphql/tracing/data_dog_trace.rb +21 -34
  111. data/lib/graphql/tracing/data_dog_tracing.rb +7 -21
  112. data/lib/graphql/tracing/legacy_hooks_trace.rb +74 -0
  113. data/lib/graphql/tracing/platform_tracing.rb +2 -0
  114. data/lib/graphql/tracing/{prometheus_tracing → prometheus_trace}/graphql_collector.rb +3 -1
  115. data/lib/graphql/tracing/sentry_trace.rb +94 -0
  116. data/lib/graphql/tracing/trace.rb +1 -0
  117. data/lib/graphql/tracing.rb +3 -1
  118. data/lib/graphql/types/iso_8601_duration.rb +77 -0
  119. data/lib/graphql/types/relay/connection_behaviors.rb +32 -2
  120. data/lib/graphql/types/relay/edge_behaviors.rb +7 -0
  121. data/lib/graphql/types.rb +1 -0
  122. data/lib/graphql/version.rb +1 -1
  123. data/lib/graphql.rb +3 -3
  124. data/readme.md +12 -2
  125. metadata +33 -25
  126. data/lib/graphql/deprecation.rb +0 -9
  127. data/lib/graphql/filter.rb +0 -59
  128. data/lib/graphql/language/parser.y +0 -560
  129. data/lib/graphql/static_validation/type_stack.rb +0 -216
  130. data/lib/graphql/subscriptions/instrumentation.rb +0 -28
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e62749a86dd0b2913ebcb9a8be343d7f0eb7199f1e301bc6b584227ddfa6bdec
4
- data.tar.gz: d9de79c247f14d316741a66162c3ff96d5621dd689ab837f488277d53701d66d
3
+ metadata.gz: ba7d4c9ad987cbae751625aced364d2ace39bfb84ee22e32ab07f33c335f4ebf
4
+ data.tar.gz: 40773fa35507e6cbeefb468c4ad153e1561e813092187abaf6f246c5e06802d6
5
5
  SHA512:
6
- metadata.gz: 11adf0c6bf4279b840b2b5ba1627ec11f4cca4d08957faac1956b261d01b914e7f8c9cc9a737c4ce02fb404bb3abac1904c9fe74255ecf367b7b88dc64133fcf
7
- data.tar.gz: b04cfe4e331da788f7c5706671e53fd2f0bfae680da896198a3d127922f6b92609cd6f720e98d7d2fb03cdea532b5a4f1536fd1bd6e154c5beb98b010dcedce5
6
+ metadata.gz: fb0a00a30aec2c81a6a0e239f8f7c3313747e190c06d821395a75868a46d84b65f699e4dbf40342455549b2b1b7f92ab5630dcdd2bebfbe95aef66a574a5bd5d
7
+ data.tar.gz: 6b1b63f5004336b304adbfb6b946d0b8ba87bd60a5356859f38fae3df92f15f092d07c22f1bd9857fdf04883b8b7d2134f38fb67bf11afe08df009528dd3c0ed
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  <% module_namespacing_when_supported do -%>
2
4
  module Mutations
3
5
  class BaseMutation < GraphQL::Schema::RelayClassicMutation
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  <% module_namespacing_when_supported do -%>
2
4
  module Types
3
5
  class MutationType < Types::BaseObject
@@ -105,6 +105,9 @@ module Graphql
105
105
  template("#{base_type}.erb", "#{options[:directory]}/types/#{base_type}.rb")
106
106
  end
107
107
 
108
+ # All resolvers are defined as living in their own module, including this class.
109
+ template("base_resolver.erb", "#{options[:directory]}/resolvers/base_resolver.rb")
110
+
108
111
  # Note: You can't have a schema without the query type, otherwise introspection breaks
109
112
  template("query_type.erb", "#{options[:directory]}/types/query_type.rb")
110
113
  insert_root_type('query', 'QueryType')
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  <% module_namespacing_when_supported do -%>
2
4
  module Types
3
5
  class BaseArgument < GraphQL::Schema::Argument
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  <% module_namespacing_when_supported do -%>
2
4
  module Types
3
5
  class BaseConnection < Types::BaseObject
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  <% module_namespacing_when_supported do -%>
2
4
  module Types
3
5
  class BaseEdge < Types::BaseObject
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  <% module_namespacing_when_supported do -%>
2
4
  module Types
3
5
  class BaseEnum < GraphQL::Schema::Enum
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  <% module_namespacing_when_supported do -%>
2
4
  module Types
3
5
  class BaseField < GraphQL::Schema::Field
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  <% module_namespacing_when_supported do -%>
2
4
  module Types
3
5
  class BaseInputObject < GraphQL::Schema::InputObject
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  <% module_namespacing_when_supported do -%>
2
4
  module Types
3
5
  module BaseInterface
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  <% module_namespacing_when_supported do -%>
2
4
  module Types
3
5
  class BaseObject < GraphQL::Schema::Object
@@ -0,0 +1,6 @@
1
+ <% module_namespacing_when_supported do -%>
2
+ module Resolvers
3
+ class BaseResolver < GraphQL::Schema::Resolver
4
+ end
5
+ end
6
+ <% end -%>
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  <% module_namespacing_when_supported do -%>
2
4
  module Types
3
5
  class BaseScalar < GraphQL::Schema::Scalar
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  <% module_namespacing_when_supported do -%>
2
4
  module Types
3
5
  class BaseUnion < GraphQL::Schema::Union
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  <% module_namespacing_when_supported do -%>
2
4
  class GraphqlController < ApplicationController
3
5
  # If accessing from outside this domain, nullify the session
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  <% module_namespacing_when_supported do -%>
2
4
  module Loaders
3
5
  class <%= class_name %> < GraphQL::Batch::Loader
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  <% module_namespacing_when_supported do -%>
2
4
  module Mutations
3
5
  class <%= class_name %> < BaseMutation
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  <% module_namespacing_when_supported do -%>
2
4
  module Types
3
5
  module NodeType
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  <% module_namespacing_when_supported do -%>
2
4
  module Types
3
5
  class QueryType < Types::BaseObject
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  <% module_namespacing_when_supported do -%>
2
4
  class <%= schema_name %> < GraphQL::Schema
3
5
  query(Types::QueryType)
@@ -29,6 +29,13 @@ module GraphQL
29
29
  true
30
30
  end
31
31
 
32
+ # Analyzer hook to decide at analysis time whether analysis
33
+ # requires a visitor pass; can be disabled for precomputed results.
34
+ # @return [Boolean] If analysis requires visitation or not
35
+ def visit?
36
+ true
37
+ end
38
+
32
39
  # The result for this analyzer. Returning {GraphQL::AnalysisError} results
33
40
  # in a query error.
34
41
  # @return [Any] The analyzer result
@@ -8,6 +8,7 @@ module GraphQL
8
8
  @used_fields = Set.new
9
9
  @used_deprecated_fields = Set.new
10
10
  @used_deprecated_arguments = Set.new
11
+ @used_deprecated_enum_values = Set.new
11
12
  end
12
13
 
13
14
  def on_leave_field(node, parent, visitor)
@@ -15,7 +16,7 @@ module GraphQL
15
16
  field = "#{visitor.parent_type_definition.graphql_name}.#{field_defn.graphql_name}"
16
17
  @used_fields << field
17
18
  @used_deprecated_fields << field if field_defn.deprecation_reason
18
- arguments = visitor.query.arguments_for(node, visitor.field_definition)
19
+ arguments = visitor.query.arguments_for(node, field_defn)
19
20
  # If there was an error when preparing this argument object,
20
21
  # then this might be an error or something:
21
22
  if arguments.respond_to?(:argument_values)
@@ -28,6 +29,7 @@ module GraphQL
28
29
  used_fields: @used_fields.to_a,
29
30
  used_deprecated_fields: @used_deprecated_fields.to_a,
30
31
  used_deprecated_arguments: @used_deprecated_arguments.to_a,
32
+ used_deprecated_enum_values: @used_deprecated_enum_values.to_a,
31
33
  }
32
34
  end
33
35
 
@@ -41,16 +43,39 @@ module GraphQL
41
43
 
42
44
  next if argument.value.nil?
43
45
 
44
- if argument.definition.type.kind.input_object?
46
+ argument_type = argument.definition.type
47
+ if argument_type.non_null?
48
+ argument_type = argument_type.of_type
49
+ end
50
+
51
+ if argument_type.kind.input_object?
45
52
  extract_deprecated_arguments(argument.value.arguments.argument_values) # rubocop:disable Development/ContextIsPassedCop -- runtime args instance
46
- elsif argument.definition.type.list?
47
- argument
48
- .value
49
- .select { |value| value.respond_to?(:arguments) }
50
- .each { |value| extract_deprecated_arguments(value.arguments.argument_values) } # rubocop:disable Development/ContextIsPassedCop -- runtime args instance
53
+ elsif argument_type.kind.enum?
54
+ extract_deprecated_enum_value(argument_type, argument.value)
55
+ elsif argument_type.list?
56
+ inner_type = argument_type.unwrap
57
+ case inner_type.kind
58
+ when TypeKinds::INPUT_OBJECT
59
+ argument.value.each do |value|
60
+ extract_deprecated_arguments(value.arguments.argument_values) # rubocop:disable Development/ContextIsPassedCop -- runtime args instance
61
+ end
62
+ when TypeKinds::ENUM
63
+ argument.value.each do |value|
64
+ extract_deprecated_enum_value(inner_type, value)
65
+ end
66
+ else
67
+ # Not a kind of input that we track
68
+ end
51
69
  end
52
70
  end
53
71
  end
72
+
73
+ def extract_deprecated_enum_value(enum_type, value)
74
+ enum_value = @query.warden.enum_values(enum_type).find { |ev| ev.value == value }
75
+ if enum_value&.deprecation_reason
76
+ @used_deprecated_enum_values << enum_value.path
77
+ end
78
+ end
54
79
  end
55
80
  end
56
81
  end
@@ -16,10 +16,11 @@ module GraphQL
16
16
  max_possible_complexity
17
17
  end
18
18
 
19
- class ScopedTypeComplexity
20
- # A single proc for {#scoped_children} hashes. Use this to avoid repeated allocations,
21
- # since the lexical binding isn't important.
22
- HASH_CHILDREN = ->(h, k) { h[k] = {} }
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] = {} }
23
24
 
24
25
  attr_reader :field_definition, :response_path, :query
25
26
 
@@ -27,32 +28,19 @@ module GraphQL
27
28
  # @param field_definition [GraphQL::Field, GraphQL::Schema::Field] Used for getting the `.complexity` configuration
28
29
  # @param query [GraphQL::Query] Used for `query.possible_types`
29
30
  # @param response_path [Array<String>] The path to the response key for the field
31
+ # @return [Hash<GraphQL::BaseType, Hash<String, ScopedTypeComplexity>>]
30
32
  def initialize(parent_type, field_definition, query, response_path)
33
+ super(&DEFAULT_PROC)
31
34
  @parent_type = parent_type
32
35
  @field_definition = field_definition
33
36
  @query = query
34
37
  @response_path = response_path
35
- @scoped_children = nil
36
38
  @nodes = []
37
39
  end
38
40
 
39
41
  # @return [Array<GraphQL::Language::Nodes::Field>]
40
42
  attr_reader :nodes
41
43
 
42
- # Returns true if this field has no selections, ie, it's a scalar.
43
- # We need a quick way to check whether we should continue traversing.
44
- def terminal?
45
- @scoped_children.nil?
46
- end
47
-
48
- # This value is only calculated when asked for to avoid needless hash allocations.
49
- # Also, if it's never asked for, we determine that this scope complexity
50
- # is a scalar field ({#terminal?}).
51
- # @return [Hash<Hash<Class => ScopedTypeComplexity>]
52
- def scoped_children
53
- @scoped_children ||= Hash.new(&HASH_CHILDREN)
54
- end
55
-
56
44
  def own_complexity(child_complexity)
57
45
  @field_definition.calculate_complexity(query: @query, nodes: @nodes, child_complexity: child_complexity)
58
46
  end
@@ -65,19 +53,14 @@ module GraphQL
65
53
  return if visitor.skipping?
66
54
  parent_type = visitor.parent_type_definition
67
55
  field_key = node.alias || node.name
68
- # Find the complexity calculation for this field --
69
- # if we're re-entering a selection, we'll already have one.
70
- # Otherwise, make a new one and store it.
71
- #
72
- # `node` and `visitor.field_definition` may appear from a cache,
73
- # but I think that's ok. If the arguments _didn't_ match,
74
- # then the query would have been rejected as invalid.
75
- complexities_on_type = @complexities_on_type_by_query[visitor.query] ||= [ScopedTypeComplexity.new(nil, nil, query, visitor.response_path)]
76
-
77
- complexity = complexities_on_type.last.scoped_children[parent_type][field_key] ||= ScopedTypeComplexity.new(parent_type, visitor.field_definition, visitor.query, visitor.response_path)
78
- complexity.nodes.push(node)
79
- # Push it on the stack.
80
- complexities_on_type.push(complexity)
56
+
57
+ # Find or create a complexity scope stack for this query.
58
+ scopes_stack = @complexities_on_type_by_query[visitor.query] ||= [ScopedTypeComplexity.new(nil, nil, query, visitor.response_path)]
59
+
60
+ # Find or create the complexity costing node for this field.
61
+ scope = scopes_stack.last[parent_type][field_key] ||= ScopedTypeComplexity.new(parent_type, visitor.field_definition, visitor.query, visitor.response_path)
62
+ scope.nodes.push(node)
63
+ scopes_stack.push(scope)
81
64
  end
82
65
 
83
66
  def on_leave_field(node, parent, visitor)
@@ -85,89 +68,61 @@ module GraphQL
85
68
  # we'll visit them when we hit the spreads instead
86
69
  return if visitor.visiting_fragment_definition?
87
70
  return if visitor.skipping?
88
- complexities_on_type = @complexities_on_type_by_query[visitor.query]
89
- complexities_on_type.pop
71
+ scopes_stack = @complexities_on_type_by_query[visitor.query]
72
+ scopes_stack.pop
90
73
  end
91
74
 
92
75
  private
93
76
 
94
77
  # @return [Integer]
95
78
  def max_possible_complexity
96
- @complexities_on_type_by_query.reduce(0) do |total, (query, complexities_on_type)|
97
- root_complexity = complexities_on_type.last
98
- # Use this entry point to calculate the total complexity
99
- total_complexity_for_query = merged_max_complexity_for_scopes(query, [root_complexity.scoped_children])
100
- total + total_complexity_for_query
79
+ @complexities_on_type_by_query.reduce(0) do |total, (query, scopes_stack)|
80
+ total + merged_max_complexity_for_scopes(query, [scopes_stack.first])
101
81
  end
102
82
  end
103
83
 
104
84
  # @param query [GraphQL::Query] Used for `query.possible_types`
105
- # @param scoped_children_hashes [Array<Hash>] Array of scoped children hashes
85
+ # @param scopes [Array<ScopedTypeComplexity>] Array of scoped type complexities
106
86
  # @return [Integer]
107
- def merged_max_complexity_for_scopes(query, scoped_children_hashes)
108
- # Figure out what scopes are possible here.
87
+ def merged_max_complexity_for_scopes(query, scopes)
88
+ # Aggregate a set of all possible scope types encountered (scope keys).
109
89
  # Use a hash, but ignore the values; it's just a fast way to work with the keys.
110
- all_scopes = {}
111
- scoped_children_hashes.each do |h|
112
- all_scopes.merge!(h)
90
+ possible_scope_types = scopes.each_with_object({}) do |scope, memo|
91
+ memo.merge!(scope)
113
92
  end
114
93
 
115
- # If an abstract scope is present, but _all_ of its concrete types
116
- # are also in the list, remove it from the list of scopes to check,
117
- # because every possible type is covered by a concrete type.
118
- # (That is, there are no remainder types to check.)
119
- prev_keys = all_scopes.keys
120
- prev_keys.each do |scope|
121
- next unless scope.kind.abstract?
122
-
123
- missing_concrete_types = query.possible_types(scope).select { |t| !all_scopes.key?(t) }
124
- # This concrete type is possible _only_ as a member of the abstract type.
125
- # So, attribute to it the complexity which belongs to the abstract type.
126
- missing_concrete_types.each do |concrete_scope|
127
- all_scopes[concrete_scope] = all_scopes[scope]
94
+ # Expand abstract scope types into their concrete implementations;
95
+ # overlapping abstracts coalesce through their intersecting types.
96
+ possible_scope_types.keys.each do |possible_scope_type|
97
+ next unless possible_scope_type.kind.abstract?
98
+
99
+ query.possible_types(possible_scope_type).each do |impl_type|
100
+ possible_scope_types[impl_type] ||= true
128
101
  end
129
- all_scopes.delete(scope)
102
+ possible_scope_types.delete(possible_scope_type)
130
103
  end
131
104
 
132
- # This will hold `{ type => int }` pairs, one for each possible branch
133
- complexity_by_scope = {}
134
-
135
- # For each scope,
136
- # find the lexical selections that might apply to it,
137
- # and gather them together into an array.
138
- # Then, treat the set of selection hashes
139
- # as a set and calculate the complexity for them as a unit
140
- all_scopes.each do |scope, _|
141
- # These will be the selections on `scope`
142
- children_for_scope = []
143
- scoped_children_hashes.each do |sc_h|
144
- sc_h.each do |inner_scope, children_hash|
145
- if applies_to?(query, scope, inner_scope)
146
- children_for_scope << children_hash
147
- end
105
+ # Aggregate the lexical selections that may apply to each possible type,
106
+ # and then return the maximum cost among possible typed selections.
107
+ possible_scope_types.each_key.reduce(0) do |max, possible_scope_type|
108
+ # Collect inner selections from all scopes that intersect with this possible type.
109
+ all_inner_selections = scopes.each_with_object([]) do |scope, memo|
110
+ scope.each do |scope_type, inner_selections|
111
+ memo << inner_selections if types_intersect?(query, scope_type, possible_scope_type)
148
112
  end
149
113
  end
150
114
 
151
- # Calculate the complexity for `scope`, merging all
152
- # possible lexical branches.
153
- complexity_value = merged_max_complexity(query, children_for_scope)
154
- complexity_by_scope[scope] = complexity_value
115
+ # Find the maximum complexity for the scope type among possible lexical branches.
116
+ complexity = merged_max_complexity(query, all_inner_selections)
117
+ complexity > max ? complexity : max
155
118
  end
156
-
157
- # Return the max complexity among all scopes
158
- complexity_by_scope.each_value.max
159
119
  end
160
120
 
161
- def applies_to?(query, left_scope, right_scope)
162
- if left_scope == right_scope
163
- # This can happen when several branches are being analyzed together
164
- true
165
- else
166
- # Check if these two scopes have _any_ types in common.
167
- possible_right_types = query.possible_types(right_scope)
168
- possible_left_types = query.possible_types(left_scope)
169
- !(possible_right_types & possible_left_types).empty?
170
- end
121
+ def types_intersect?(query, a, b)
122
+ return true if a == b
123
+
124
+ a_types = query.possible_types(a)
125
+ query.possible_types(b).any? { |t| a_types.include?(t) }
171
126
  end
172
127
 
173
128
  # A hook which is called whenever a field's max complexity is calculated.
@@ -179,50 +134,47 @@ module GraphQL
179
134
  def field_complexity(scoped_type_complexity, max_complexity:, child_complexity: nil)
180
135
  end
181
136
 
182
- # @param children_for_scope [Array<Hash>] An array of `scoped_children[scope]` hashes
183
- # (`{field_key => complexity}`)
184
- # @return [Integer] Complexity value for all these selections in the current scope
185
- def merged_max_complexity(query, children_for_scope)
186
- all_keys = []
187
- children_for_scope.each do |c|
188
- all_keys.concat(c.keys)
137
+ # @param inner_selections [Array<Hash<String, ScopedTypeComplexity>>] Field selections for a scope
138
+ # @return [Integer] Total complexity value for all these selections in the parent scope
139
+ def merged_max_complexity(query, inner_selections)
140
+ # Aggregate a set of all unique field selection keys across all scopes.
141
+ # Use a hash, but ignore the values; it's just a fast way to work with the keys.
142
+ unique_field_keys = inner_selections.each_with_object({}) do |inner_selection, memo|
143
+ memo.merge!(inner_selection)
189
144
  end
190
- all_keys.uniq!
191
- complexity_for_keys = {}
192
-
193
- all_keys.each do |child_key|
194
- scoped_children_for_key = nil
195
- complexity_for_key = nil
196
- children_for_scope.each do |children_hash|
197
- next unless children_hash.key?(child_key)
198
-
199
- complexity_for_key = children_hash[child_key]
200
- if complexity_for_key.terminal?
201
- # Assume that all terminals would return the same complexity
202
- # Since it's a terminal, its child complexity is zero.
203
- complexity = complexity_for_key.own_complexity(0)
204
- complexity_for_keys[child_key] = complexity
205
-
206
- field_complexity(complexity_for_key, max_complexity: complexity, child_complexity: nil)
145
+
146
+ # Add up the total cost for each unique field name's coalesced selections
147
+ unique_field_keys.each_key.reduce(0) do |total, field_key|
148
+ composite_scopes = nil
149
+ field_cost = 0
150
+
151
+ # Collect composite selection scopes for further aggregation,
152
+ # leaf selections report their costs directly.
153
+ inner_selections.each do |inner_selection|
154
+ child_scope = inner_selection[field_key]
155
+ next unless child_scope
156
+
157
+ # Empty child scopes are leaf nodes with zero child complexity.
158
+ if child_scope.empty?
159
+ field_cost = child_scope.own_complexity(0)
160
+ field_complexity(child_scope, max_complexity: field_cost, child_complexity: nil)
207
161
  else
208
- scoped_children_for_key ||= []
209
- scoped_children_for_key << complexity_for_key.scoped_children
162
+ composite_scopes ||= []
163
+ composite_scopes << child_scope
210
164
  end
211
165
  end
212
166
 
213
- next unless scoped_children_for_key
167
+ if composite_scopes
168
+ child_complexity = merged_max_complexity_for_scopes(query, composite_scopes)
214
169
 
215
- child_complexity = merged_max_complexity_for_scopes(query, scoped_children_for_key)
216
- # This is the _last_ one we visited; assume it's representative.
217
- max_complexity = complexity_for_key.own_complexity(child_complexity)
218
-
219
- field_complexity(complexity_for_key, max_complexity: max_complexity, child_complexity: child_complexity)
170
+ # This is the last composite scope visited; assume it's representative (for backwards compatibility).
171
+ # Note: it would be more correct to score each composite scope and use the maximum possibility.
172
+ field_cost = composite_scopes.last.own_complexity(child_complexity)
173
+ field_complexity(composite_scopes.last, max_complexity: field_cost, child_complexity: child_complexity)
174
+ end
220
175
 
221
- complexity_for_keys[child_key] = max_complexity
176
+ total + field_cost
222
177
  end
223
-
224
- # Calculate the child complexity by summing the complexity of all selections
225
- complexity_for_keys.each_value.inject(0, &:+)
226
178
  end
227
179
  end
228
180
  end
@@ -28,17 +28,22 @@ module GraphQL
28
28
  def initialize(query)
29
29
  @max_depth = 0
30
30
  @current_depth = 0
31
+ @count_introspection_fields = query.schema.count_introspection_fields
31
32
  super
32
33
  end
33
34
 
34
35
  def on_enter_field(node, parent, visitor)
35
- return if visitor.skipping? || visitor.visiting_fragment_definition?
36
+ return if visitor.skipping? ||
37
+ visitor.visiting_fragment_definition? ||
38
+ (@count_introspection_fields == false && visitor.field_definition.introspection?)
36
39
 
37
40
  @current_depth += 1
38
41
  end
39
42
 
40
43
  def on_leave_field(node, parent, visitor)
41
- return if visitor.skipping? || visitor.visiting_fragment_definition?
44
+ return if visitor.skipping? ||
45
+ visitor.visiting_fragment_definition? ||
46
+ (@count_introspection_fields == false && visitor.field_definition.introspection?)
42
47
 
43
48
  if @max_depth < @current_depth
44
49
  @max_depth = @current_depth
@@ -5,12 +5,12 @@ module GraphQL
5
5
  # Depth first traversal through a query AST, calling AST analyzers
6
6
  # along the way.
7
7
  #
8
- # The visitor is a special case of GraphQL::Language::Visitor, visiting
8
+ # The visitor is a special case of GraphQL::Language::StaticVisitor, visiting
9
9
  # only the selected operation, providing helpers for common use cases such
10
10
  # as skipped fields and visiting fragment spreads.
11
11
  #
12
12
  # @see {GraphQL::Analysis::AST::Analyzer} AST Analyzers for queries
13
- class Visitor < GraphQL::Language::Visitor
13
+ class Visitor < GraphQL::Language::StaticVisitor
14
14
  def initialize(query:, analyzers:)
15
15
  @analyzers = analyzers
16
16
  @path = []
@@ -6,6 +6,7 @@ require "graphql/analysis/ast/query_complexity"
6
6
  require "graphql/analysis/ast/max_query_complexity"
7
7
  require "graphql/analysis/ast/query_depth"
8
8
  require "graphql/analysis/ast/max_query_depth"
9
+ require "timeout"
9
10
 
10
11
  module GraphQL
11
12
  module Analysis
@@ -51,30 +52,39 @@ module GraphQL
51
52
  query.current_trace.analyze_query(query: query) do
52
53
  query_analyzers = analyzers
53
54
  .map { |analyzer| analyzer.new(query) }
54
- .select { |analyzer| analyzer.analyze? }
55
+ .tap { _1.select!(&:analyze?) }
55
56
 
56
57
  analyzers_to_run = query_analyzers + multiplex_analyzers
57
58
  if analyzers_to_run.any?
58
- visitor = GraphQL::Analysis::AST::Visitor.new(
59
- query: query,
60
- analyzers: analyzers_to_run
61
- )
62
59
 
63
- visitor.visit
60
+ analyzers_to_run.select!(&:visit?)
61
+ if analyzers_to_run.any?
62
+ visitor = GraphQL::Analysis::AST::Visitor.new(
63
+ query: query,
64
+ analyzers: analyzers_to_run
65
+ )
64
66
 
65
- if visitor.rescued_errors.any?
66
- visitor.rescued_errors
67
- else
68
- query_analyzers.map(&:result)
67
+ # `nil` or `0` causes no timeout
68
+ Timeout::timeout(query.validate_timeout_remaining) do
69
+ visitor.visit
70
+ end
71
+
72
+ if visitor.rescued_errors.any?
73
+ return visitor.rescued_errors
74
+ end
69
75
  end
76
+
77
+ query_analyzers.map(&:result)
70
78
  else
71
79
  []
72
80
  end
73
81
  end
82
+ rescue Timeout::Error
83
+ [GraphQL::AnalysisError.new("Timeout on validation of query")]
74
84
  end
75
85
 
76
86
  def analysis_errors(results)
77
- results.flatten.select { |r| r.is_a?(GraphQL::AnalysisError) }
87
+ results.flatten.tap { _1.select! { |r| r.is_a?(GraphQL::AnalysisError) } }
78
88
  end
79
89
  end
80
90
  end