graphql 2.5.2 → 2.5.3
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.
- checksums.yaml +4 -4
- data/lib/graphql/analysis/query_complexity.rb +87 -7
- data/lib/graphql/current.rb +1 -1
- data/lib/graphql/dig.rb +2 -1
- data/lib/graphql/execution/interpreter/runtime.rb +3 -2
- data/lib/graphql/execution/multiplex.rb +5 -0
- data/lib/graphql/invalid_null_error.rb +15 -2
- data/lib/graphql/query/context.rb +1 -0
- data/lib/graphql/query.rb +3 -9
- data/lib/graphql/schema/always_visible.rb +1 -0
- data/lib/graphql/schema/list.rb +1 -1
- data/lib/graphql/schema/member/has_dataloader.rb +4 -2
- data/lib/graphql/schema/visibility/profile.rb +3 -1
- data/lib/graphql/schema/visibility.rb +13 -14
- data/lib/graphql/schema.rb +175 -12
- data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +47 -13
- data/lib/graphql/static_validation/rules/fields_will_merge.rb +78 -16
- data/lib/graphql/static_validation/rules/fields_will_merge_error.rb +10 -2
- data/lib/graphql/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d2e4ec1acc9810683bb2628dc5403c74d729742612655f1be275267a89b107b3
|
4
|
+
data.tar.gz: fb092108f803095aea29b325859fc810feaa8e5fdb6c873c431f9824428fbc15
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 603e3da13be680884399546074dae65f44635382e13311758e237f7973bbdd96f4a1a46f6b17476af77d88177f5e82f3c76b227633e2e15a9c909365a818f97e
|
7
|
+
data.tar.gz: d631c5a9e1c91fb815d98e1981641405c344b589c6e823ae5609b25fd819a2d5dc9032f366c94a3da69c56a48aa7d8e9ee082cbfa217807e6cbf313d16cbd598
|
@@ -13,7 +13,32 @@ module GraphQL
|
|
13
13
|
|
14
14
|
# Override this method to use the complexity result
|
15
15
|
def result
|
16
|
-
|
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
|
17
42
|
end
|
18
43
|
|
19
44
|
# ScopedTypeComplexity models a tree of GraphQL types mapped to inner selections, ie:
|
@@ -44,6 +69,10 @@ module GraphQL
|
|
44
69
|
def own_complexity(child_complexity)
|
45
70
|
@field_definition.calculate_complexity(query: @query, nodes: @nodes, child_complexity: child_complexity)
|
46
71
|
end
|
72
|
+
|
73
|
+
def composite?
|
74
|
+
!empty?
|
75
|
+
end
|
47
76
|
end
|
48
77
|
|
49
78
|
def on_enter_field(node, parent, visitor)
|
@@ -77,16 +106,17 @@ module GraphQL
|
|
77
106
|
private
|
78
107
|
|
79
108
|
# @return [Integer]
|
80
|
-
def max_possible_complexity
|
109
|
+
def max_possible_complexity(mode: :future)
|
81
110
|
@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)
|
83
112
|
end
|
84
113
|
end
|
85
114
|
|
86
115
|
# @param query [GraphQL::Query] Used for `query.possible_types`
|
87
116
|
# @param scopes [Array<ScopedTypeComplexity>] Array of scoped type complexities
|
117
|
+
# @param mode [:future, :legacy]
|
88
118
|
# @return [Integer]
|
89
|
-
def merged_max_complexity_for_scopes(query, scopes)
|
119
|
+
def merged_max_complexity_for_scopes(query, scopes, mode)
|
90
120
|
# Aggregate a set of all possible scope types encountered (scope keys).
|
91
121
|
# Use a hash, but ignore the values; it's just a fast way to work with the keys.
|
92
122
|
possible_scope_types = scopes.each_with_object({}) do |scope, memo|
|
@@ -115,14 +145,20 @@ module GraphQL
|
|
115
145
|
end
|
116
146
|
|
117
147
|
# Find the maximum complexity for the scope type among possible lexical branches.
|
118
|
-
complexity =
|
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
|
119
156
|
complexity > max ? complexity : max
|
120
157
|
end
|
121
158
|
end
|
122
159
|
|
123
160
|
def types_intersect?(query, a, b)
|
124
161
|
return true if a == b
|
125
|
-
|
126
162
|
a_types = query.types.possible_types(a)
|
127
163
|
query.types.possible_types(b).any? { |t| a_types.include?(t) }
|
128
164
|
end
|
@@ -145,6 +181,50 @@ module GraphQL
|
|
145
181
|
memo.merge!(inner_selection)
|
146
182
|
end
|
147
183
|
|
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
|
+
|
148
228
|
# Add up the total cost for each unique field name's coalesced selections
|
149
229
|
unique_field_keys.each_key.reduce(0) do |total, field_key|
|
150
230
|
composite_scopes = nil
|
@@ -167,7 +247,7 @@ module GraphQL
|
|
167
247
|
end
|
168
248
|
|
169
249
|
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)
|
171
251
|
|
172
252
|
# This is the last composite scope visited; assume it's representative (for backwards compatibility).
|
173
253
|
# Note: it would be more correct to score each composite scope and use the maximum possibility.
|
data/lib/graphql/current.rb
CHANGED
@@ -22,7 +22,7 @@ module GraphQL
|
|
22
22
|
# ]
|
23
23
|
#
|
24
24
|
module Current
|
25
|
-
# @return [String, nil] Comma-joined operation names for the currently-running {Multiplex}. `nil` if all operations are anonymous.
|
25
|
+
# @return [String, nil] Comma-joined operation names for the currently-running {Execution::Multiplex}. `nil` if all operations are anonymous.
|
26
26
|
def self.operation_name
|
27
27
|
if (m = Fiber[:__graphql_current_multiplex])
|
28
28
|
m.context[:__graphql_current_operation_name] ||= begin
|
data/lib/graphql/dig.rb
CHANGED
@@ -5,7 +5,8 @@ module GraphQL
|
|
5
5
|
# so we can use some of the magic in Schema::InputObject and Interpreter::Arguments
|
6
6
|
# to handle stringified/symbolized keys.
|
7
7
|
#
|
8
|
-
# @param
|
8
|
+
# @param own_key [String, Symbol] A key to retrieve
|
9
|
+
# @param rest_keys [Array<[String, Symbol>] Retrieves the value object corresponding to the each key objects repeatedly
|
9
10
|
# @return [Object]
|
10
11
|
def dig(own_key, *rest_keys)
|
11
12
|
val = self[own_key]
|
@@ -475,9 +475,10 @@ module GraphQL
|
|
475
475
|
if is_non_null
|
476
476
|
set_result(selection_result, result_name, nil, false, is_non_null) do
|
477
477
|
# When this comes from a list item, use the parent object:
|
478
|
-
|
478
|
+
is_from_array = selection_result.is_a?(GraphQLResultArray)
|
479
|
+
parent_type = is_from_array ? selection_result.graphql_parent.graphql_result_type : selection_result.graphql_result_type
|
479
480
|
# This block is called if `result_name` is not dead. (Maybe a previous invalid nil caused it be marked dead.)
|
480
|
-
err = parent_type::InvalidNullError.new(parent_type, field, ast_node)
|
481
|
+
err = parent_type::InvalidNullError.new(parent_type, field, ast_node, is_from_array: is_from_array)
|
481
482
|
schema.type_error(err, context)
|
482
483
|
end
|
483
484
|
else
|
@@ -36,6 +36,11 @@ module GraphQL
|
|
36
36
|
@tracers = schema.tracers + (context[:tracers] || [])
|
37
37
|
@max_complexity = max_complexity
|
38
38
|
@current_trace = context[:trace] ||= schema.new_trace(multiplex: self)
|
39
|
+
@logger = nil
|
40
|
+
end
|
41
|
+
|
42
|
+
def logger
|
43
|
+
@logger ||= @schema.logger_for(context)
|
39
44
|
end
|
40
45
|
end
|
41
46
|
end
|
@@ -12,11 +12,24 @@ module GraphQL
|
|
12
12
|
# @return [GraphQL::Language::Nodes::Field] the field where the error occurred
|
13
13
|
attr_reader :ast_node
|
14
14
|
|
15
|
-
|
15
|
+
# @return [Boolean] indicates an array result caused the error
|
16
|
+
attr_reader :is_from_array
|
17
|
+
|
18
|
+
def initialize(parent_type, field, ast_node, is_from_array: false)
|
16
19
|
@parent_type = parent_type
|
17
20
|
@field = field
|
18
21
|
@ast_node = ast_node
|
19
|
-
|
22
|
+
@is_from_array = is_from_array
|
23
|
+
|
24
|
+
# For List elements, identify the non-null error is for an
|
25
|
+
# element and the required element type so it's not ambiguous
|
26
|
+
# whether it was caused by a null instead of the list or a
|
27
|
+
# null element.
|
28
|
+
if @is_from_array
|
29
|
+
super("Cannot return null for non-nullable element of type '#{@field.type.of_type.of_type.to_type_signature}' for #{@parent_type.graphql_name}.#{@field.graphql_name}")
|
30
|
+
else
|
31
|
+
super("Cannot return null for non-nullable field #{@parent_type.graphql_name}.#{@field.graphql_name}")
|
32
|
+
end
|
20
33
|
end
|
21
34
|
|
22
35
|
class << self
|
@@ -59,6 +59,7 @@ module GraphQL
|
|
59
59
|
@scoped_context = ScopedContext.new(self)
|
60
60
|
end
|
61
61
|
|
62
|
+
# Modify this hash to return extensions to client.
|
62
63
|
# @return [Hash] A hash that will be added verbatim to the result hash, as `"extensions" => { ... }`
|
63
64
|
def response_extensions
|
64
65
|
namespace(:__query_result_extensions__)
|
data/lib/graphql/query.rb
CHANGED
@@ -50,7 +50,7 @@ module GraphQL
|
|
50
50
|
# @return [GraphQL::StaticValidation::Validator] if present, the query will validate with these rules.
|
51
51
|
attr_reader :static_validator
|
52
52
|
|
53
|
-
# @param
|
53
|
+
# @param new_validator [GraphQL::StaticValidation::Validator] if present, the query will validate with these rules. This can't be reasssigned after validation.
|
54
54
|
def static_validator=(new_validator)
|
55
55
|
if defined?(@validation_pipeline) && @validation_pipeline && @validation_pipeline.has_validated?
|
56
56
|
raise ArgumentError, "Can't reassign Query#static_validator= after validation has run, remove this assignment."
|
@@ -172,13 +172,7 @@ module GraphQL
|
|
172
172
|
@result_values = nil
|
173
173
|
@executed = false
|
174
174
|
|
175
|
-
@logger =
|
176
|
-
Logger.new(IO::NULL)
|
177
|
-
elsif context && (l = context[:logger])
|
178
|
-
l
|
179
|
-
else
|
180
|
-
schema.default_logger
|
181
|
-
end
|
175
|
+
@logger = schema.logger_for(context)
|
182
176
|
end
|
183
177
|
|
184
178
|
# If a document was provided to `GraphQL::Schema#execute` instead of the raw query string, we will need to get it from the document
|
@@ -288,7 +282,7 @@ module GraphQL
|
|
288
282
|
# @param ast_node [GraphQL::Language::Nodes::AbstractNode]
|
289
283
|
# @param definition [GraphQL::Schema::Field]
|
290
284
|
# @param parent_object [GraphQL::Schema::Object]
|
291
|
-
# @return Hash{Symbol => Object}
|
285
|
+
# @return [Hash{Symbol => Object}]
|
292
286
|
def arguments_for(ast_node, definition, parent_object: nil)
|
293
287
|
arguments_cache.fetch(ast_node, definition, parent_object)
|
294
288
|
end
|
data/lib/graphql/schema/list.rb
CHANGED
@@ -4,7 +4,7 @@ module GraphQL
|
|
4
4
|
class Schema
|
5
5
|
# Represents a list type in the schema.
|
6
6
|
# Wraps a {Schema::Member} as a list type.
|
7
|
-
# @see
|
7
|
+
# @see Schema::Member::TypeSystemHelpers#to_list_type Create a list type from another GraphQL type
|
8
8
|
class List < GraphQL::Schema::Wrapper
|
9
9
|
include Schema::Member::ValidatesInput
|
10
10
|
|
@@ -3,6 +3,8 @@
|
|
3
3
|
module GraphQL
|
4
4
|
class Schema
|
5
5
|
class Member
|
6
|
+
# @api public
|
7
|
+
# Shared methods for working with {Dataloader} inside GraphQL runtime objects.
|
6
8
|
module HasDataloader
|
7
9
|
# @return [GraphQL::Dataloader] The dataloader for the currently-running query
|
8
10
|
def dataloader
|
@@ -37,7 +39,7 @@ module GraphQL
|
|
37
39
|
source.load(find_by_value)
|
38
40
|
end
|
39
41
|
|
40
|
-
# Look up an associated record using a Rails association
|
42
|
+
# Look up an associated record using a Rails association (via {Dataloader::ActiveRecordAssociationSource})
|
41
43
|
# @param association_name [Symbol] A `belongs_to` or `has_one` association. (If a `has_many` association is named here, it will be selected without pagination.)
|
42
44
|
# @param record [ActiveRecord::Base] The object that the association belongs to.
|
43
45
|
# @param scope [ActiveRecord::Relation] A scope to look up the associated record in
|
@@ -45,7 +47,7 @@ module GraphQL
|
|
45
47
|
# @example Looking up a belongs_to on the current object
|
46
48
|
# dataload_association(:parent) # Equivalent to `object.parent`, but dataloaded
|
47
49
|
# @example Looking up an associated record on some other object
|
48
|
-
# dataload_association(:post
|
50
|
+
# dataload_association(comment, :post) # Equivalent to `comment.post`, but dataloaded
|
49
51
|
def dataload_association(record = object, association_name, scope: nil)
|
50
52
|
source = if scope
|
51
53
|
dataloader.with(Dataloader::ActiveRecordAssociationSource, association_name, scope)
|
@@ -281,9 +281,11 @@ module GraphQL
|
|
281
281
|
|
282
282
|
def non_duplicate_items(definitions, visibility_cache)
|
283
283
|
non_dups = []
|
284
|
+
names = Set.new
|
284
285
|
definitions.each do |defn|
|
285
286
|
if visibility_cache[defn]
|
286
|
-
if (
|
287
|
+
if !names.add?(defn.graphql_name)
|
288
|
+
dup_defn = non_dups.find { |d| d.graphql_name == defn.graphql_name }
|
287
289
|
raise_duplicate_definition(dup_defn, defn)
|
288
290
|
end
|
289
291
|
non_dups << defn
|
@@ -148,23 +148,22 @@ module GraphQL
|
|
148
148
|
|
149
149
|
attr_reader :cached_profiles
|
150
150
|
|
151
|
-
def profile_for(context
|
151
|
+
def profile_for(context)
|
152
152
|
if !@profiles.empty?
|
153
|
-
|
154
|
-
|
155
|
-
if context.is_a?(Query::NullContext)
|
156
|
-
top_level_profile
|
157
|
-
else
|
158
|
-
@schema.visibility_profile_class.new(context: context, schema: @schema)
|
159
|
-
end
|
160
|
-
elsif !@profiles.empty?
|
161
|
-
raise ArgumentError, "#{@schema} expects a visibility profile, but `visibility_profile:` wasn't passed. Provide a `visibility_profile:` value or add `dynamic: true` to your visibility configuration."
|
162
|
-
end
|
163
|
-
elsif !@profiles.include?(visibility_profile)
|
164
|
-
raise ArgumentError, "`#{visibility_profile.inspect}` isn't allowed for `visibility_profile:` (must be one of #{@profiles.keys.map(&:inspect).join(", ")}). Or, add `#{visibility_profile.inspect}` to the list of profiles in the schema definition."
|
165
|
-
else
|
153
|
+
visibility_profile = context[:visibility_profile]
|
154
|
+
if @profiles.include?(visibility_profile)
|
166
155
|
profile_ctx = @profiles[visibility_profile]
|
167
156
|
@cached_profiles[visibility_profile] ||= @schema.visibility_profile_class.new(name: visibility_profile, context: profile_ctx, schema: @schema)
|
157
|
+
elsif @dynamic
|
158
|
+
if context.is_a?(Query::NullContext)
|
159
|
+
top_level_profile
|
160
|
+
else
|
161
|
+
@schema.visibility_profile_class.new(context: context, schema: @schema)
|
162
|
+
end
|
163
|
+
elsif !context.key?(:visibility_profile)
|
164
|
+
raise ArgumentError, "#{@schema} expects a visibility profile, but `visibility_profile:` wasn't passed. Provide a `visibility_profile:` value or add `dynamic: true` to your visibility configuration."
|
165
|
+
else
|
166
|
+
raise ArgumentError, "`#{visibility_profile.inspect}` isn't allowed for `visibility_profile:` (must be one of #{@profiles.keys.map(&:inspect).join(", ")}). Or, add `#{visibility_profile.inspect}` to the list of profiles in the schema definition."
|
168
167
|
end
|
169
168
|
elsif context.is_a?(Query::NullContext)
|
170
169
|
top_level_profile
|
data/lib/graphql/schema.rb
CHANGED
@@ -60,7 +60,7 @@ module GraphQL
|
|
60
60
|
# Any undiscoverable types may be provided with the `types` configuration.
|
61
61
|
#
|
62
62
|
# Schemas can restrict large incoming queries with `max_depth` and `max_complexity` configurations.
|
63
|
-
# (These configurations can be overridden by specific calls to {Schema
|
63
|
+
# (These configurations can be overridden by specific calls to {Schema.execute})
|
64
64
|
#
|
65
65
|
# @example defining a schema
|
66
66
|
# class MySchema < GraphQL::Schema
|
@@ -249,7 +249,7 @@ module GraphQL
|
|
249
249
|
|
250
250
|
|
251
251
|
# Returns the JSON response of {Introspection::INTROSPECTION_QUERY}.
|
252
|
-
# @see
|
252
|
+
# @see #as_json Return a Hash representation of the schema
|
253
253
|
# @return [String]
|
254
254
|
def to_json(**args)
|
255
255
|
JSON.pretty_generate(as_json(**args))
|
@@ -257,8 +257,6 @@ module GraphQL
|
|
257
257
|
|
258
258
|
# Return the Hash response of {Introspection::INTROSPECTION_QUERY}.
|
259
259
|
# @param context [Hash]
|
260
|
-
# @param only [<#call(member, ctx)>]
|
261
|
-
# @param except [<#call(member, ctx)>]
|
262
260
|
# @param include_deprecated_args [Boolean] If true, deprecated arguments will be included in the JSON response
|
263
261
|
# @param include_schema_description [Boolean] If true, the schema's description will be queried and included in the response
|
264
262
|
# @param include_is_repeatable [Boolean] If true, `isRepeatable: true|false` will be included with the schema's directives
|
@@ -1066,6 +1064,18 @@ module GraphQL
|
|
1066
1064
|
end
|
1067
1065
|
end
|
1068
1066
|
|
1067
|
+
# @param context [GraphQL::Query::Context, nil]
|
1068
|
+
# @return [Logger] A logger to use for this context configuration, falling back to {.default_logger}
|
1069
|
+
def logger_for(context)
|
1070
|
+
if context && context[:logger] == false
|
1071
|
+
Logger.new(IO::NULL)
|
1072
|
+
elsif context && (l = context[:logger])
|
1073
|
+
l
|
1074
|
+
else
|
1075
|
+
default_logger
|
1076
|
+
end
|
1077
|
+
end
|
1078
|
+
|
1069
1079
|
# @param new_context_class [Class<GraphQL::Query::Context>] A subclass to use when executing queries
|
1070
1080
|
def context_class(new_context_class = nil)
|
1071
1081
|
if new_context_class
|
@@ -1165,7 +1175,7 @@ module GraphQL
|
|
1165
1175
|
# GraphQL-Ruby calls this method during execution when it needs the application to determine the type to use for an object.
|
1166
1176
|
#
|
1167
1177
|
# Usually, this object was returned from a field whose return type is an {GraphQL::Schema::Interface} or a {GraphQL::Schema::Union}.
|
1168
|
-
# But this method is called in other cases, too -- for example, when {GraphQL::Schema::Argument
|
1178
|
+
# But this method is called in other cases, too -- for example, when {GraphQL::Schema::Argument#loads} cases an object to be directly loaded from the database.
|
1169
1179
|
#
|
1170
1180
|
# @example Returning a GraphQL type based on the object's class name
|
1171
1181
|
# class MySchema < GraphQL::Schema
|
@@ -1220,7 +1230,7 @@ module GraphQL
|
|
1220
1230
|
|
1221
1231
|
# Return a stable ID string for `object` so that it can be refetched later, using {.object_from_id}.
|
1222
1232
|
#
|
1223
|
-
#
|
1233
|
+
# [GlobalID](https://github.com/rails/globalid) and [SQIDs](https://sqids.org/ruby) can both be used to create IDs.
|
1224
1234
|
#
|
1225
1235
|
# @example Using Rails's GlobalID to generate IDs
|
1226
1236
|
# def self.id_from_object(application_object, graphql_type, context)
|
@@ -1297,13 +1307,13 @@ module GraphQL
|
|
1297
1307
|
# @return [void]
|
1298
1308
|
# @raise [GraphQL::ExecutionError] to return this error to the client
|
1299
1309
|
# @raise [GraphQL::Error] to crash the query and raise a developer-facing error
|
1300
|
-
def type_error(type_error,
|
1310
|
+
def type_error(type_error, context)
|
1301
1311
|
case type_error
|
1302
1312
|
when GraphQL::InvalidNullError
|
1303
1313
|
execution_error = GraphQL::ExecutionError.new(type_error.message, ast_node: type_error.ast_node)
|
1304
|
-
execution_error.path =
|
1314
|
+
execution_error.path = context[:current_path]
|
1305
1315
|
|
1306
|
-
|
1316
|
+
context.errors << execution_error
|
1307
1317
|
when GraphQL::UnresolvedTypeError, GraphQL::StringEncodingError, GraphQL::IntegerEncodingError
|
1308
1318
|
raise type_error
|
1309
1319
|
when GraphQL::IntegerDecodingError
|
@@ -1311,7 +1321,7 @@ module GraphQL
|
|
1311
1321
|
end
|
1312
1322
|
end
|
1313
1323
|
|
1314
|
-
# A function to call when {
|
1324
|
+
# A function to call when {.execute} receives an invalid query string
|
1315
1325
|
#
|
1316
1326
|
# The default is to add the error to `context.errors`
|
1317
1327
|
# @param parse_err [GraphQL::ParseError] The error encountered during parsing
|
@@ -1567,7 +1577,8 @@ module GraphQL
|
|
1567
1577
|
# @see {Query#initialize} for query keyword arguments
|
1568
1578
|
# @see {Execution::Multiplex#run_all} for multiplex keyword arguments
|
1569
1579
|
# @param queries [Array<Hash>] Keyword arguments for each query
|
1570
|
-
# @
|
1580
|
+
# @option kwargs [Hash] :context ({}) Multiplex-level context
|
1581
|
+
# @option kwargs [nil, Integer] :max_complexity (nil)
|
1571
1582
|
# @return [Array<GraphQL::Query::Result>] One result for each query in the input
|
1572
1583
|
def multiplex(queries, **kwargs)
|
1573
1584
|
GraphQL::Execution::Interpreter.run_all(self, queries, **kwargs)
|
@@ -1632,7 +1643,7 @@ module GraphQL
|
|
1632
1643
|
end
|
1633
1644
|
end
|
1634
1645
|
|
1635
|
-
# @return [Symbol, nil] The method name to lazily resolve `obj`, or nil if `obj`'s class wasn't registered with {
|
1646
|
+
# @return [Symbol, nil] The method name to lazily resolve `obj`, or nil if `obj`'s class wasn't registered with {.lazy_resolve}.
|
1636
1647
|
def lazy_method_name(obj)
|
1637
1648
|
lazy_methods.get(obj)
|
1638
1649
|
end
|
@@ -1670,6 +1681,158 @@ module GraphQL
|
|
1670
1681
|
end
|
1671
1682
|
end
|
1672
1683
|
|
1684
|
+
|
1685
|
+
# This setting controls how GraphQL-Ruby handles empty selections on Union types.
|
1686
|
+
#
|
1687
|
+
# To opt into future, spec-compliant behavior where these selections are rejected, set this to `false`.
|
1688
|
+
#
|
1689
|
+
# If you need to support previous, non-spec behavior which allowed selecting union fields
|
1690
|
+
# but *not* selecting any fields on that union, set this to `true` to continue allowing that behavior.
|
1691
|
+
#
|
1692
|
+
# If this is `true`, then {.legacy_invalid_empty_selections_on_union} will be called with {Query} objects
|
1693
|
+
# with that kind of selections. You must implement that method
|
1694
|
+
# @param new_value [Boolean]
|
1695
|
+
# @return [true, false, nil]
|
1696
|
+
def allow_legacy_invalid_empty_selections_on_union(new_value = NOT_CONFIGURED)
|
1697
|
+
if NOT_CONFIGURED.equal?(new_value)
|
1698
|
+
@allow_legacy_invalid_empty_selections_on_union
|
1699
|
+
else
|
1700
|
+
@allow_legacy_invalid_empty_selections_on_union = new_value
|
1701
|
+
end
|
1702
|
+
end
|
1703
|
+
|
1704
|
+
# This method is called during validation when a previously-allowed, but non-spec
|
1705
|
+
# query is encountered where a union field has no child selections on it.
|
1706
|
+
#
|
1707
|
+
# You should implement this method to log the violation so that you can contact clients
|
1708
|
+
# and notify them about changing their queries. Then return a suitable value to
|
1709
|
+
# tell GraphQL-Ruby how to continue.
|
1710
|
+
# @param query [GraphQL::Query]
|
1711
|
+
# @return [:return_validation_error] Let GraphQL-Ruby return the (new) normal validation error for this query
|
1712
|
+
# @return [String] A validation error to return for this query
|
1713
|
+
# @return [nil] Don't send the client an error, continue the legacy behavior (allow this query to execute)
|
1714
|
+
def legacy_invalid_empty_selections_on_union(query)
|
1715
|
+
raise "Implement `def self.legacy_invalid_empty_selections_on_union(query)` to handle this scenario"
|
1716
|
+
end
|
1717
|
+
|
1718
|
+
# This setting controls how GraphQL-Ruby handles overlapping selections on scalar types when the types
|
1719
|
+
# don't match.
|
1720
|
+
#
|
1721
|
+
# When set to `false`, GraphQL-Ruby will reject those queries with a validation error (as per the GraphQL spec).
|
1722
|
+
#
|
1723
|
+
# When set to `true`, GraphQL-Ruby will call {.legacy_invalid_return_type_conflicts} when the scenario is encountered.
|
1724
|
+
#
|
1725
|
+
# @param new_value [Boolean] `true` permits the legacy behavior, `false` rejects it.
|
1726
|
+
# @return [true, false, nil]
|
1727
|
+
def allow_legacy_invalid_return_type_conflicts(new_value = NOT_CONFIGURED)
|
1728
|
+
if NOT_CONFIGURED.equal?(new_value)
|
1729
|
+
@allow_legacy_invalid_return_type_conflicts
|
1730
|
+
else
|
1731
|
+
@allow_legacy_invalid_return_type_conflicts = new_value
|
1732
|
+
end
|
1733
|
+
end
|
1734
|
+
|
1735
|
+
# This method is called when the query contains fields which don't contain matching scalar types.
|
1736
|
+
# This was previously allowed by GraphQL-Ruby but it's a violation of the GraphQL spec.
|
1737
|
+
#
|
1738
|
+
# You should implement this method to log the violation so that you observe usage of these fields.
|
1739
|
+
# Fixing this scenario might mean adding new fields, and telling clients to use those fields.
|
1740
|
+
# (Changing the field return type would be a breaking change, but if it works for your client use cases,
|
1741
|
+
# that might work, too.)
|
1742
|
+
#
|
1743
|
+
# @param query [GraphQL::Query]
|
1744
|
+
# @param type1 [Module] A GraphQL type definition
|
1745
|
+
# @param type2 [Module] A GraphQL type definition
|
1746
|
+
# @param node1 [GraphQL::Language::Nodes::Field] This node is recognized as conflicting. You might call `.line` and `.col` for custom error reporting.
|
1747
|
+
# @param node2 [GraphQL::Language::Nodes::Field] The other node recognized as conflicting.
|
1748
|
+
# @return [:return_validation_error] Let GraphQL-Ruby return the (new) normal validation error for this query
|
1749
|
+
# @return [String] A validation error to return for this query
|
1750
|
+
# @return [nil] Don't send the client an error, continue the legacy behavior (allow this query to execute)
|
1751
|
+
def legacy_invalid_return_type_conflicts(query, type1, type2, node1, node2)
|
1752
|
+
raise "Implement #{self}.legacy_invalid_return_type_conflicts to handle this invalid selection"
|
1753
|
+
end
|
1754
|
+
|
1755
|
+
# The legacy complexity implementation included several bugs:
|
1756
|
+
#
|
1757
|
+
# - In some cases, it used the lexically _last_ field to determine a cost, instead of calculating the maximum among selections
|
1758
|
+
# - In some cases, it called field complexity hooks repeatedly (when it should have only called them once)
|
1759
|
+
#
|
1760
|
+
# The future implementation may produce higher total complexity scores, so it's not active by default yet. You can opt into
|
1761
|
+
# the future default behavior by configuring `:future` here. Or, you can choose a mode for each query with {.complexity_cost_calculation_mode_for}.
|
1762
|
+
#
|
1763
|
+
# The legacy mode is currently maintained alongside the future one, but it will be removed in a future GraphQL-Ruby version.
|
1764
|
+
#
|
1765
|
+
# If you choose `:compare`, you must also implement {.legacy_complexity_cost_calculation_mismatch} to handle the input somehow.
|
1766
|
+
#
|
1767
|
+
# @example Opting into the future calculation mode
|
1768
|
+
# complexity_cost_calculation_mode(:future)
|
1769
|
+
#
|
1770
|
+
# @example Choosing the legacy mode (which will work until that mode is removed...)
|
1771
|
+
# complexity_cost_calculation_mode(:legacy)
|
1772
|
+
#
|
1773
|
+
# @example Run both modes for every query, call {.legacy_complexity_cost_calculation_mismatch} when they don't match:
|
1774
|
+
# complexity_cost_calculation_mode(:compare)
|
1775
|
+
def complexity_cost_calculation_mode(new_mode = NOT_CONFIGURED)
|
1776
|
+
if NOT_CONFIGURED.equal?(new_mode)
|
1777
|
+
@complexity_cost_calculation_mode
|
1778
|
+
else
|
1779
|
+
@complexity_cost_calculation_mode = new_mode
|
1780
|
+
end
|
1781
|
+
end
|
1782
|
+
|
1783
|
+
# Implement this method to produce a per-query complexity cost calculation mode. (Technically, it's per-multiplex.)
|
1784
|
+
#
|
1785
|
+
# This is a way to check the compatibility of queries coming to your API without adding overhead of running `:compare`
|
1786
|
+
# for every query. You could sample traffic, turn it off/on with feature flags, or anything else.
|
1787
|
+
#
|
1788
|
+
# @example Sampling traffic
|
1789
|
+
# def self.complexity_cost_calculation_mode_for(_context)
|
1790
|
+
# if rand < 0.1 # 10% of the time
|
1791
|
+
# :compare
|
1792
|
+
# else
|
1793
|
+
# :legacy
|
1794
|
+
# end
|
1795
|
+
# end
|
1796
|
+
#
|
1797
|
+
# @example Using a feature flag to manage future mode
|
1798
|
+
# def complexity_cost_calculation_mode_for(context)
|
1799
|
+
# current_user = context[:current_user]
|
1800
|
+
# if Flipper.enabled?(:future_complexity_cost, current_user)
|
1801
|
+
# :future
|
1802
|
+
# elsif rand < 0.5 # 50%
|
1803
|
+
# :compare
|
1804
|
+
# else
|
1805
|
+
# :legacy
|
1806
|
+
# end
|
1807
|
+
# end
|
1808
|
+
#
|
1809
|
+
# @param multiplex_context [Hash] The context for the currently-running {Execution::Multiplex} (which contains one or more queries)
|
1810
|
+
# @return [:future] Use the new calculation algorithm -- may be higher than `:legacy`
|
1811
|
+
# @return [:legacy] Use the legacy calculation algorithm, warts and all
|
1812
|
+
# @return [:compare] Run both algorithms and call {.legacy_complexity_cost_calculation_mismatch} if they don't match
|
1813
|
+
def complexity_cost_calculation_mode_for(multiplex_context)
|
1814
|
+
complexity_cost_calculation_mode
|
1815
|
+
end
|
1816
|
+
|
1817
|
+
# Implement this method in your schema to handle mismatches when `:compare` is used.
|
1818
|
+
#
|
1819
|
+
# @example Logging the mismatch
|
1820
|
+
# def self.legacy_cost_calculation_mismatch(multiplex, future_cost, legacy_cost)
|
1821
|
+
# client_id = multiplex.context[:api_client].id
|
1822
|
+
# operation_names = multiplex.queries.map { |q| q.selected_operation_name || "anonymous" }.join(", ")
|
1823
|
+
# Stats.increment(:complexity_mismatch, tags: { client: client_id, ops: operation_names })
|
1824
|
+
# legacy_cost
|
1825
|
+
# end
|
1826
|
+
# @see Query::Context#add_error Adding an error to the response to notify the client
|
1827
|
+
# @see Query::Context#response_extensions Adding key-value pairs to the response `"extensions" => { ... }`
|
1828
|
+
# @param multiplex [GraphQL::Execution::Multiplex]
|
1829
|
+
# @param future_complexity_cost [Integer]
|
1830
|
+
# @param legacy_complexity_cost [Integer]
|
1831
|
+
# @return [Integer] the cost to use for this query (probably one of `future_complexity_cost` or `legacy_complexity_cost`)
|
1832
|
+
def legacy_complexity_cost_calculation_mismatch(multiplex, future_complexity_cost, legacy_complexity_cost)
|
1833
|
+
raise "Implement #{self}.legacy_complexity_cost(multiplex, future_complexity_cost, legacy_complexity_cost) to handle this mismatch (#{future_complexity_cost} vs. #{legacy_complexity_cost}) and return a value to use"
|
1834
|
+
end
|
1835
|
+
|
1673
1836
|
private
|
1674
1837
|
|
1675
1838
|
def add_trace_options_for(mode, new_options)
|
@@ -25,22 +25,56 @@ module GraphQL
|
|
25
25
|
def validate_field_selections(ast_node, resolved_type)
|
26
26
|
msg = if resolved_type.nil?
|
27
27
|
nil
|
28
|
-
elsif
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
28
|
+
elsif resolved_type.kind.leaf?
|
29
|
+
if !ast_node.selections.empty?
|
30
|
+
selection_strs = ast_node.selections.map do |n|
|
31
|
+
case n
|
32
|
+
when GraphQL::Language::Nodes::InlineFragment
|
33
|
+
"\"... on #{n.type.name} { ... }\""
|
34
|
+
when GraphQL::Language::Nodes::Field
|
35
|
+
"\"#{n.name}\""
|
36
|
+
when GraphQL::Language::Nodes::FragmentSpread
|
37
|
+
"\"#{n.name}\""
|
38
|
+
else
|
39
|
+
raise "Invariant: unexpected selection node: #{n}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
"Selections can't be made on #{resolved_type.kind.name.sub("_", " ").downcase}s (%{node_name} returns #{resolved_type.graphql_name} but has selections [#{selection_strs.join(", ")}])"
|
43
|
+
else
|
44
|
+
nil
|
45
|
+
end
|
46
|
+
elsif ast_node.selections.empty?
|
47
|
+
return_validation_error = true
|
48
|
+
legacy_invalid_empty_selection_result = nil
|
49
|
+
if !resolved_type.kind.fields?
|
50
|
+
case @schema.allow_legacy_invalid_empty_selections_on_union
|
51
|
+
when true
|
52
|
+
legacy_invalid_empty_selection_result = @schema.legacy_invalid_empty_selections_on_union(@context.query)
|
53
|
+
case legacy_invalid_empty_selection_result
|
54
|
+
when :return_validation_error
|
55
|
+
# keep `return_validation_error = true`
|
56
|
+
when String
|
57
|
+
return_validation_error = false
|
58
|
+
# the string is returned below
|
59
|
+
when nil
|
60
|
+
# No error:
|
61
|
+
return_validation_error = false
|
62
|
+
legacy_invalid_empty_selection_result = nil
|
63
|
+
else
|
64
|
+
raise GraphQL::InvariantError, "Unexpected return value from legacy_invalid_empty_selections_on_union, must be `:return_validation_error`, String, or nil (got: #{legacy_invalid_empty_selection_result.inspect})"
|
65
|
+
end
|
66
|
+
when false
|
67
|
+
# pass -- error below
|
37
68
|
else
|
38
|
-
|
69
|
+
return_validation_error = false
|
70
|
+
@context.query.logger.warn("Unions require selections but #{ast_node.alias || ast_node.name} (#{resolved_type.graphql_name}) doesn't have any. This will fail with a validation error on a future GraphQL-Ruby version. More info: https://graphql-ruby.org/api-doc/#{GraphQL::VERSION}/GraphQL/Schema.html#allow_legacy_invalid_empty_selections_on_union-class_method")
|
39
71
|
end
|
40
72
|
end
|
41
|
-
|
42
|
-
|
43
|
-
|
73
|
+
if return_validation_error
|
74
|
+
"Field must have selections (%{node_name} returns #{resolved_type.graphql_name} but has no selections. Did you mean '#{ast_node.name} { ... }'?)"
|
75
|
+
else
|
76
|
+
legacy_invalid_empty_selection_result
|
77
|
+
end
|
44
78
|
else
|
45
79
|
nil
|
46
80
|
end
|
@@ -33,26 +33,19 @@ module GraphQL
|
|
33
33
|
|
34
34
|
private
|
35
35
|
|
36
|
-
def
|
37
|
-
@
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
def arg_conflicts
|
43
|
-
@arg_conflicts ||= Hash.new do |errors, field|
|
44
|
-
errors[field] = GraphQL::StaticValidation::FieldsWillMergeError.new(kind: :argument, field_name: field)
|
36
|
+
def conflicts
|
37
|
+
@conflicts ||= Hash.new do |h, error_type|
|
38
|
+
h[error_type] = Hash.new do |h2, field_name|
|
39
|
+
h2[field_name] = GraphQL::StaticValidation::FieldsWillMergeError.new(kind: error_type, field_name: field_name)
|
40
|
+
end
|
45
41
|
end
|
46
42
|
end
|
47
43
|
|
48
44
|
def setting_errors
|
49
|
-
@
|
50
|
-
@arg_conflicts = nil
|
51
|
-
|
45
|
+
@conflicts = nil
|
52
46
|
yield
|
53
47
|
# don't initialize these if they weren't initialized in the block:
|
54
|
-
@
|
55
|
-
@arg_conflicts && @arg_conflicts.each_value { |error| add_error(error) }
|
48
|
+
@conflicts&.each_value { |error_type| error_type.each_value { |error| add_error(error) } }
|
56
49
|
end
|
57
50
|
|
58
51
|
def conflicts_within_selection_set(node, parent_type)
|
@@ -222,7 +215,7 @@ module GraphQL
|
|
222
215
|
|
223
216
|
if !are_mutually_exclusive
|
224
217
|
if node1.name != node2.name
|
225
|
-
conflict =
|
218
|
+
conflict = conflicts[:field][response_key]
|
226
219
|
|
227
220
|
conflict.add_conflict(node1, node1.name)
|
228
221
|
conflict.add_conflict(node2, node2.name)
|
@@ -231,7 +224,7 @@ module GraphQL
|
|
231
224
|
end
|
232
225
|
|
233
226
|
if !same_arguments?(node1, node2)
|
234
|
-
conflict =
|
227
|
+
conflict = conflicts[:argument][response_key]
|
235
228
|
|
236
229
|
conflict.add_conflict(node1, GraphQL::Language.serialize(serialize_field_args(node1)))
|
237
230
|
conflict.add_conflict(node2, GraphQL::Language.serialize(serialize_field_args(node2)))
|
@@ -240,6 +233,49 @@ module GraphQL
|
|
240
233
|
end
|
241
234
|
end
|
242
235
|
|
236
|
+
if !conflicts[:field].key?(response_key) &&
|
237
|
+
(t1 = field1.definition&.type) &&
|
238
|
+
(t2 = field2.definition&.type) &&
|
239
|
+
return_types_conflict?(t1, t2)
|
240
|
+
|
241
|
+
return_error = nil
|
242
|
+
message_override = nil
|
243
|
+
case @schema.allow_legacy_invalid_return_type_conflicts
|
244
|
+
when false
|
245
|
+
return_error = true
|
246
|
+
when true
|
247
|
+
legacy_handling = @schema.legacy_invalid_return_type_conflicts(@context.query, t1, t2, node1, node2)
|
248
|
+
case legacy_handling
|
249
|
+
when nil
|
250
|
+
return_error = false
|
251
|
+
when :return_validation_error
|
252
|
+
return_error = true
|
253
|
+
when String
|
254
|
+
return_error = true
|
255
|
+
message_override = legacy_handling
|
256
|
+
else
|
257
|
+
raise GraphQL::Error, "#{@schema}.legacy_invalid_scalar_conflicts returned unexpected value: #{legacy_handling.inspect}. Expected `nil`, String, or `:return_validation_error`."
|
258
|
+
end
|
259
|
+
else
|
260
|
+
return_error = false
|
261
|
+
@context.query.logger.warn <<~WARN
|
262
|
+
GraphQL-Ruby encountered mismatched types in this query: `#{t1.to_type_signature}` (at #{node1.line}:#{node1.col}) vs. `#{t2.to_type_signature}` (at #{node2.line}:#{node2.col}).
|
263
|
+
This will return an error in future GraphQL-Ruby versions, as per the GraphQL specification
|
264
|
+
Learn about migrating here: https://graphql-ruby.org/api-doc/#{GraphQL::VERSION}/GraphQL/Schema.html#allow_legacy_invalid_return_type_conflicts-class_method
|
265
|
+
WARN
|
266
|
+
end
|
267
|
+
|
268
|
+
if return_error
|
269
|
+
conflict = conflicts[:return_type][response_key]
|
270
|
+
if message_override
|
271
|
+
conflict.message = message_override
|
272
|
+
end
|
273
|
+
conflict.add_conflict(node1, "`#{t1.to_type_signature}`")
|
274
|
+
conflict.add_conflict(node2, "`#{t2.to_type_signature}`")
|
275
|
+
@conflict_count += 1
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
243
279
|
find_conflicts_between_sub_selection_sets(
|
244
280
|
field1,
|
245
281
|
field2,
|
@@ -247,6 +283,32 @@ module GraphQL
|
|
247
283
|
)
|
248
284
|
end
|
249
285
|
|
286
|
+
def return_types_conflict?(type1, type2)
|
287
|
+
if type1.list?
|
288
|
+
if type2.list?
|
289
|
+
return_types_conflict?(type1.of_type, type2.of_type)
|
290
|
+
else
|
291
|
+
true
|
292
|
+
end
|
293
|
+
elsif type2.list?
|
294
|
+
true
|
295
|
+
elsif type1.non_null?
|
296
|
+
if type2.non_null?
|
297
|
+
return_types_conflict?(type1.of_type, type2.of_type)
|
298
|
+
else
|
299
|
+
true
|
300
|
+
end
|
301
|
+
elsif type2.non_null?
|
302
|
+
true
|
303
|
+
elsif type1.kind.leaf? && type2.kind.leaf?
|
304
|
+
type1 != type2
|
305
|
+
else
|
306
|
+
# One or more of these are composite types,
|
307
|
+
# their selections will be validated later on.
|
308
|
+
false
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
250
312
|
def find_conflicts_between_sub_selection_sets(field1, field2, mutually_exclusive:)
|
251
313
|
return if field1.definition.nil? ||
|
252
314
|
field2.definition.nil? ||
|
@@ -14,9 +14,11 @@ module GraphQL
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def message
|
17
|
-
"Field '#{field_name}' has #{kind == :argument ? 'an' : 'a'} #{kind} conflict: #{conflicts}?"
|
17
|
+
@message || "Field '#{field_name}' has #{kind == :argument ? 'an' : 'a'} #{kind} conflict: #{conflicts}?"
|
18
18
|
end
|
19
19
|
|
20
|
+
attr_writer :message
|
21
|
+
|
20
22
|
def path
|
21
23
|
[]
|
22
24
|
end
|
@@ -26,7 +28,13 @@ module GraphQL
|
|
26
28
|
end
|
27
29
|
|
28
30
|
def add_conflict(node, conflict_str)
|
29
|
-
|
31
|
+
# Can't use `.include?` here because AST nodes implement `#==`
|
32
|
+
# based on string value, not including location. But sometimes,
|
33
|
+
# identical nodes conflict because of their differing return types.
|
34
|
+
if nodes.any? { |n| n == node && n.line == node.line && n.col == node.col }
|
35
|
+
# already have an error for this node
|
36
|
+
return
|
37
|
+
end
|
30
38
|
|
31
39
|
@nodes << node
|
32
40
|
@conflicts << conflict_str
|
data/lib/graphql/version.rb
CHANGED
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: graphql
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.5.
|
4
|
+
version: 2.5.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Robert Mosolgo
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-04-
|
10
|
+
date: 2025-04-14 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: base64
|