@@ -13,7 +13,32 @@ def initialize(query)
1313
1414 # Override this method to use the complexity result
1515 def result
16- max_possible_complexity
16+ case subject . schema . complexity_cost_calculation_mode_for ( subject . context )
17+ when :future
18+ max_possible_complexity
19+ when :legacy
20+ max_possible_complexity ( mode : :legacy )
21+ when :compare
22+ future_complexity = max_possible_complexity
23+ legacy_complexity = max_possible_complexity ( mode : :legacy )
24+ if future_complexity != legacy_complexity
25+ subject . schema . legacy_complexity_cost_calculation_mismatch ( subject , future_complexity , legacy_complexity )
26+ else
27+ future_complexity
28+ end
29+ when nil
30+ subject . logger . warn <<~GRAPHQL
31+ GraphQL-Ruby's complexity cost system is getting some "breaking fixes" in a future version. See the migration notes at https://graphql-ruby.org/api-docs/#{ GraphQL ::VERSION } /Schema.html#complexity_cost_cacluation_mode-class_method
32+
33+ To opt into the future behavior, configure your schema (#{ subject . schema . name ? subject . schema . name : subject . schema . ancestors } ) with:
34+
35+ complexity_cost_calculation_mode(:future) # or `:legacy`, `:compare`
36+
37+ GRAPHQL
38+ max_possible_complexity
39+ else
40+ raise ArgumentError , "Expected `:future`, `:legacy`, `:compare`, or `nil` from `#{ query . schema } .complexity_cost_calculation_mode_for` but got: #{ query . schema . complexity_cost_calculation_mode . inspect } "
41+ end
1742 end
1843
1944 # ScopedTypeComplexity models a tree of GraphQL types mapped to inner selections, ie:
@@ -44,6 +69,10 @@ def initialize(parent_type, field_definition, query, response_path)
4469 def own_complexity ( child_complexity )
4570 @field_definition . calculate_complexity ( query : @query , nodes : @nodes , child_complexity : child_complexity )
4671 end
72+
73+ def composite?
74+ !empty?
75+ end
4776 end
4877
4978 def on_enter_field ( node , parent , visitor )
@@ -77,16 +106,17 @@ def on_leave_field(node, parent, visitor)
77106 private
78107
79108 # @return [Integer]
80- def max_possible_complexity
109+ def max_possible_complexity ( mode : :future )
81110 @complexities_on_type_by_query . reduce ( 0 ) do |total , ( query , scopes_stack ) |
82- total + merged_max_complexity_for_scopes ( query , [ scopes_stack . first ] )
111+ total + merged_max_complexity_for_scopes ( query , [ scopes_stack . first ] , mode )
83112 end
84113 end
85114
86115 # @param query [GraphQL::Query] Used for `query.possible_types`
87116 # @param scopes [Array<ScopedTypeComplexity>] Array of scoped type complexities
117+ # @param mode [:future, :legacy]
88118 # @return [Integer]
89- def merged_max_complexity_for_scopes ( query , scopes )
119+ def merged_max_complexity_for_scopes ( query , scopes , mode )
90120 # Aggregate a set of all possible scope types encountered (scope keys).
91121 # Use a hash, but ignore the values; it's just a fast way to work with the keys.
92122 possible_scope_types = scopes . each_with_object ( { } ) do |scope , memo |
@@ -115,14 +145,20 @@ def merged_max_complexity_for_scopes(query, scopes)
115145 end
116146
117147 # Find the maximum complexity for the scope type among possible lexical branches.
118- complexity = merged_max_complexity ( query , all_inner_selections )
148+ complexity = case mode
149+ when :legacy
150+ legacy_merged_max_complexity ( query , all_inner_selections )
151+ when :future
152+ merged_max_complexity ( query , all_inner_selections )
153+ else
154+ raise ArgumentError , "Expected :legacy or :future, not: #{ mode . inspect } "
155+ end
119156 complexity > max ? complexity : max
120157 end
121158 end
122159
123160 def types_intersect? ( query , a , b )
124161 return true if a == b
125-
126162 a_types = query . types . possible_types ( a )
127163 query . types . possible_types ( b ) . any? { |t | a_types . include? ( t ) }
128164 end
@@ -145,6 +181,50 @@ def merged_max_complexity(query, inner_selections)
145181 memo . merge! ( inner_selection )
146182 end
147183
184+ # Add up the total cost for each unique field name's coalesced selections
185+ unique_field_keys . each_key . reduce ( 0 ) do |total , field_key |
186+ # Collect all child scopes for this field key;
187+ # all keys come with at least one scope.
188+ child_scopes = inner_selections . filter_map { _1 [ field_key ] }
189+
190+ # Compute maximum possible cost of child selections;
191+ # composites merge their maximums, while leaf scopes are always zero.
192+ # FieldsWillMerge validation assures all scopes are uniformly composite or leaf.
193+ maximum_children_cost = if child_scopes . any? ( &:composite? )
194+ merged_max_complexity_for_scopes ( query , child_scopes , :future )
195+ else
196+ 0
197+ end
198+
199+ # Identify the maximum cost and scope among possibilities
200+ maximum_cost = 0
201+ maximum_scope = child_scopes . reduce ( child_scopes . last ) do |max_scope , possible_scope |
202+ scope_cost = possible_scope . own_complexity ( maximum_children_cost )
203+ if scope_cost > maximum_cost
204+ maximum_cost = scope_cost
205+ possible_scope
206+ else
207+ max_scope
208+ end
209+ end
210+
211+ field_complexity (
212+ maximum_scope ,
213+ max_complexity : maximum_cost ,
214+ child_complexity : maximum_children_cost ,
215+ )
216+
217+ total + maximum_cost
218+ end
219+ end
220+
221+ def legacy_merged_max_complexity ( query , inner_selections )
222+ # Aggregate a set of all unique field selection keys across all scopes.
223+ # Use a hash, but ignore the values; it's just a fast way to work with the keys.
224+ unique_field_keys = inner_selections . each_with_object ( { } ) do |inner_selection , memo |
225+ memo . merge! ( inner_selection )
226+ end
227+
148228 # Add up the total cost for each unique field name's coalesced selections
149229 unique_field_keys . each_key . reduce ( 0 ) do |total , field_key |
150230 composite_scopes = nil
@@ -167,7 +247,7 @@ def merged_max_complexity(query, inner_selections)
167247 end
168248
169249 if composite_scopes
170- child_complexity = merged_max_complexity_for_scopes ( query , composite_scopes )
250+ child_complexity = merged_max_complexity_for_scopes ( query , composite_scopes , :legacy )
171251
172252 # This is the last composite scope visited; assume it's representative (for backwards compatibility).
173253 # Note: it would be more correct to score each composite scope and use the maximum possibility.
0 commit comments