graphql 2.5.22 → 2.5.23
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/execution/interpreter/runtime.rb +3 -2
- data/lib/graphql/execution/interpreter.rb +6 -9
- data/lib/graphql/execution/lazy.rb +1 -1
- data/lib/graphql/execution/next/field_resolve_step.rb +93 -61
- data/lib/graphql/execution/next/load_argument_step.rb +5 -1
- data/lib/graphql/execution/next/prepare_object_step.rb +2 -2
- data/lib/graphql/execution/next/runner.rb +48 -26
- data/lib/graphql/execution/next.rb +3 -1
- data/lib/graphql/execution.rb +7 -4
- data/lib/graphql/execution_error.rb +5 -1
- data/lib/graphql/query/context.rb +1 -1
- data/lib/graphql/schema/field.rb +3 -4
- data/lib/graphql/schema/list.rb +1 -1
- data/lib/graphql/schema/member/has_fields.rb +5 -1
- data/lib/graphql/schema/non_null.rb +1 -1
- data/lib/graphql/schema/resolver.rb +18 -3
- data/lib/graphql/schema/subscription.rb +0 -2
- data/lib/graphql/schema/visibility/profile.rb +68 -49
- data/lib/graphql/schema/wrapper.rb +7 -1
- data/lib/graphql/static_validation/base_visitor.rb +90 -66
- data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +1 -1
- data/lib/graphql/static_validation/rules/argument_names_are_unique.rb +18 -6
- data/lib/graphql/static_validation/rules/arguments_are_defined.rb +5 -2
- data/lib/graphql/static_validation/rules/directives_are_defined.rb +5 -2
- data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +4 -3
- data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +12 -2
- data/lib/graphql/static_validation/rules/fields_will_merge.rb +322 -256
- data/lib/graphql/static_validation/rules/fields_will_merge_error.rb +4 -4
- data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +3 -3
- data/lib/graphql/static_validation/rules/fragment_types_exist.rb +10 -7
- data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +27 -7
- data/lib/graphql/static_validation/rules/variables_are_input_types.rb +12 -9
- data/lib/graphql/static_validation/validation_context.rb +1 -1
- data/lib/graphql/subscriptions/default_subscription_resolve_extension.rb +25 -1
- data/lib/graphql/subscriptions/event.rb +1 -0
- data/lib/graphql/subscriptions.rb +20 -0
- data/lib/graphql/tracing/perfetto_trace.rb +2 -2
- data/lib/graphql/unauthorized_error.rb +4 -0
- data/lib/graphql/version.rb +1 -1
- metadata +3 -3
|
@@ -8,31 +8,103 @@ module GraphQL
|
|
|
8
8
|
# fragments) either correspond to distinct response names or can be merged
|
|
9
9
|
# without ambiguity.
|
|
10
10
|
#
|
|
11
|
-
#
|
|
11
|
+
# Optimized algorithm based on:
|
|
12
|
+
# https://tech.new-work.se/graphql-overlapping-fields-can-be-merged-fast-ea6e92e0a01
|
|
13
|
+
#
|
|
14
|
+
# Instead of comparing fields, fields-vs-fragments, and fragments-vs-fragments
|
|
15
|
+
# separately (which leads to exponential recursion through nested fragments),
|
|
16
|
+
# we flatten all fragment spreads into a single field map and compare within it.
|
|
12
17
|
NO_ARGS = GraphQL::EmptyObjects::EMPTY_HASH
|
|
13
18
|
|
|
14
|
-
Field
|
|
15
|
-
|
|
19
|
+
class Field
|
|
20
|
+
attr_reader :node, :definition, :owner_type, :parents
|
|
21
|
+
|
|
22
|
+
def initialize(node, definition, owner_type, parents)
|
|
23
|
+
@node = node
|
|
24
|
+
@definition = definition
|
|
25
|
+
@owner_type = owner_type
|
|
26
|
+
@parents = parents
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def return_type
|
|
30
|
+
@return_type ||= @definition&.type
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def unwrapped_return_type
|
|
34
|
+
@unwrapped_return_type ||= return_type&.unwrap
|
|
35
|
+
end
|
|
36
|
+
end
|
|
16
37
|
|
|
17
38
|
def initialize(*)
|
|
18
39
|
super
|
|
19
|
-
@visited_fragments = {}
|
|
20
|
-
@compared_fragments = {}
|
|
21
40
|
@conflict_count = 0
|
|
41
|
+
@max_errors = context.max_errors
|
|
42
|
+
@fragments = context.fragments
|
|
43
|
+
# Track which sub-selection node pairs have been compared to prevent
|
|
44
|
+
# infinite recursion with cyclic fragments
|
|
45
|
+
@compared_sub_selections = {}.compare_by_identity
|
|
46
|
+
# Cache mutually_exclusive? results for type pairs
|
|
47
|
+
@mutually_exclusive_cache = {}.compare_by_identity
|
|
48
|
+
# Cache collect_fields results for sub-selection comparison
|
|
49
|
+
@sub_fields_cache = {}.compare_by_identity
|
|
22
50
|
end
|
|
23
51
|
|
|
24
52
|
def on_operation_definition(node, _parent)
|
|
25
|
-
|
|
53
|
+
@conflicts = nil
|
|
54
|
+
conflicts_within_selection_set(node, type_definition)
|
|
55
|
+
@conflicts&.each_value { |error_type| error_type.each_value { |error| add_error(error) } }
|
|
26
56
|
super
|
|
27
57
|
end
|
|
28
58
|
|
|
29
59
|
def on_field(node, _parent)
|
|
30
|
-
|
|
60
|
+
if !node.selections.empty? && selections_may_conflict?(node.selections)
|
|
61
|
+
@conflicts = nil
|
|
62
|
+
conflicts_within_selection_set(node, type_definition)
|
|
63
|
+
@conflicts&.each_value { |error_type| error_type.each_value { |error| add_error(error) } }
|
|
64
|
+
end
|
|
31
65
|
super
|
|
32
66
|
end
|
|
33
67
|
|
|
34
68
|
private
|
|
35
69
|
|
|
70
|
+
# Quick check: can the direct children of this selection set possibly conflict?
|
|
71
|
+
# If all direct selections are Fields with unique names and no aliases,
|
|
72
|
+
# and there are no fragments, then no response key can have >1 field,
|
|
73
|
+
# so there are no merge conflicts to check at this level.
|
|
74
|
+
def selections_may_conflict?(selections)
|
|
75
|
+
i = 0
|
|
76
|
+
len = selections.size
|
|
77
|
+
while i < len
|
|
78
|
+
sel = selections[i]
|
|
79
|
+
# Fragment spread or inline fragment — needs full check
|
|
80
|
+
return true unless sel.is_a?(GraphQL::Language::Nodes::Field)
|
|
81
|
+
|
|
82
|
+
# Aliased field — could create duplicate response key
|
|
83
|
+
return true if sel.alias
|
|
84
|
+
|
|
85
|
+
i += 1
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# All are unaliased fields — check for duplicate names
|
|
89
|
+
# For small sets, O(n²) is cheaper than hash allocation
|
|
90
|
+
if len <= 8
|
|
91
|
+
i = 0
|
|
92
|
+
while i < len
|
|
93
|
+
j = i + 1
|
|
94
|
+
name_i = selections[i].name
|
|
95
|
+
while j < len
|
|
96
|
+
return true if selections[j].name == name_i
|
|
97
|
+
j += 1
|
|
98
|
+
end
|
|
99
|
+
i += 1
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
false
|
|
103
|
+
else
|
|
104
|
+
true # Assume potential conflicts for larger sets
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
36
108
|
def conflicts
|
|
37
109
|
@conflicts ||= Hash.new do |h, error_type|
|
|
38
110
|
h[error_type] = Hash.new do |h2, field_name|
|
|
@@ -41,177 +113,201 @@ module GraphQL
|
|
|
41
113
|
end
|
|
42
114
|
end
|
|
43
115
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
yield
|
|
47
|
-
# don't initialize these if they weren't initialized in the block:
|
|
48
|
-
@conflicts&.each_value { |error_type| error_type.each_value { |error| add_error(error) } }
|
|
49
|
-
end
|
|
50
|
-
|
|
116
|
+
# Core algorithm: collect ALL fields (expanding fragments inline) into a flat
|
|
117
|
+
# map keyed by response key, then compare within each group.
|
|
51
118
|
def conflicts_within_selection_set(node, parent_type)
|
|
52
119
|
return if parent_type.nil?
|
|
120
|
+
return if node.selections.empty?
|
|
53
121
|
|
|
54
|
-
fields
|
|
55
|
-
|
|
56
|
-
# (A) Find find all conflicts "within" the fields of this selection set.
|
|
57
|
-
find_conflicts_within(fields)
|
|
58
|
-
|
|
59
|
-
fragment_spreads.each_with_index do |fragment_spread, i|
|
|
60
|
-
are_mutually_exclusive = mutually_exclusive?(
|
|
61
|
-
fragment_spread.parents,
|
|
62
|
-
[parent_type]
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
# (B) Then find conflicts between these fields and those represented by
|
|
66
|
-
# each spread fragment name found.
|
|
67
|
-
find_conflicts_between_fields_and_fragment(
|
|
68
|
-
fragment_spread,
|
|
69
|
-
fields,
|
|
70
|
-
mutually_exclusive: are_mutually_exclusive,
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
# (C) Then compare this fragment with all other fragments found in this
|
|
74
|
-
# selection set to collect conflicts between fragments spread together.
|
|
75
|
-
# This compares each item in the list of fragment names to every other
|
|
76
|
-
# item in that same list (except for itself).
|
|
77
|
-
fragment_spreads[i + 1..-1].each do |fragment_spread2|
|
|
78
|
-
are_mutually_exclusive = mutually_exclusive?(
|
|
79
|
-
fragment_spread.parents,
|
|
80
|
-
fragment_spread2.parents
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
find_conflicts_between_fragments(
|
|
84
|
-
fragment_spread,
|
|
85
|
-
fragment_spread2,
|
|
86
|
-
mutually_exclusive: are_mutually_exclusive,
|
|
87
|
-
)
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def find_conflicts_between_fragments(fragment_spread1, fragment_spread2, mutually_exclusive:)
|
|
93
|
-
fragment_name1 = fragment_spread1.name
|
|
94
|
-
fragment_name2 = fragment_spread2.name
|
|
95
|
-
return if fragment_name1 == fragment_name2
|
|
96
|
-
|
|
97
|
-
cache_key = compared_fragments_key(
|
|
98
|
-
fragment_name1,
|
|
99
|
-
fragment_name2,
|
|
100
|
-
mutually_exclusive,
|
|
101
|
-
)
|
|
102
|
-
if @compared_fragments.key?(cache_key)
|
|
103
|
-
return
|
|
104
|
-
else
|
|
105
|
-
@compared_fragments[cache_key] = true
|
|
106
|
-
end
|
|
122
|
+
# Collect all fields from this selection set, expanding fragments transitively
|
|
123
|
+
response_keys = collect_fields(node.selections, owner_type: parent_type, parents: [])
|
|
107
124
|
|
|
108
|
-
|
|
109
|
-
|
|
125
|
+
# Find conflicts within each response key group
|
|
126
|
+
find_conflicts_within(response_keys)
|
|
127
|
+
end
|
|
110
128
|
|
|
111
|
-
|
|
129
|
+
# Collect all fields from selections, expanding fragment spreads inline.
|
|
130
|
+
# Returns a Hash of { response_key => Field | [Field, ...] }
|
|
131
|
+
def collect_fields(selections, owner_type:, parents:)
|
|
132
|
+
response_keys = {}
|
|
133
|
+
collect_fields_inner(selections, owner_type: owner_type, parents: parents, response_keys: response_keys, visited_fragments: nil)
|
|
134
|
+
response_keys
|
|
135
|
+
end
|
|
112
136
|
|
|
113
|
-
|
|
114
|
-
|
|
137
|
+
def collect_fields_inner(selections, owner_type:, parents:, response_keys:, visited_fragments:)
|
|
138
|
+
deferred_spreads = nil
|
|
139
|
+
sel_idx = 0
|
|
140
|
+
sel_len = selections.size
|
|
115
141
|
|
|
116
|
-
|
|
142
|
+
while sel_idx < sel_len
|
|
143
|
+
sel = selections[sel_idx]
|
|
117
144
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
mutually_exclusive: mutually_exclusive,
|
|
135
|
-
)
|
|
145
|
+
case sel
|
|
146
|
+
when GraphQL::Language::Nodes::Field
|
|
147
|
+
definition = @types.field(owner_type, sel.name)
|
|
148
|
+
key = sel.alias || sel.name
|
|
149
|
+
field = Field.new(sel, definition, owner_type, parents)
|
|
150
|
+
existing = response_keys[key]
|
|
151
|
+
|
|
152
|
+
if existing.nil?
|
|
153
|
+
response_keys[key] = field
|
|
154
|
+
elsif existing.is_a?(Field)
|
|
155
|
+
response_keys[key] = [existing, field]
|
|
156
|
+
else
|
|
157
|
+
existing << field
|
|
158
|
+
end
|
|
159
|
+
when GraphQL::Language::Nodes::InlineFragment
|
|
160
|
+
frag_type = sel.type ? @types.type(sel.type.name) : owner_type
|
|
136
161
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
end
|
|
162
|
+
if frag_type
|
|
163
|
+
new_parents = parents.dup
|
|
164
|
+
new_parents << frag_type
|
|
165
|
+
collect_fields_inner(sel.selections, owner_type: frag_type, parents: new_parents, response_keys: response_keys, visited_fragments: visited_fragments)
|
|
166
|
+
end
|
|
167
|
+
when GraphQL::Language::Nodes::FragmentSpread
|
|
168
|
+
(deferred_spreads ||= []) << sel
|
|
169
|
+
end
|
|
146
170
|
|
|
147
|
-
|
|
148
|
-
# fragments spread in the second fragment.
|
|
149
|
-
fragment_spreads1.each do |fragment_spread|
|
|
150
|
-
find_conflicts_between_fragments(
|
|
151
|
-
fragment_spread2,
|
|
152
|
-
fragment_spread,
|
|
153
|
-
mutually_exclusive: mutually_exclusive,
|
|
154
|
-
)
|
|
171
|
+
sel_idx += 1
|
|
155
172
|
end
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def find_conflicts_between_fields_and_fragment(fragment_spread, fields, mutually_exclusive:)
|
|
159
|
-
fragment_name = fragment_spread.name
|
|
160
|
-
return if @visited_fragments.key?(fragment_name)
|
|
161
|
-
@visited_fragments[fragment_name] = true
|
|
162
173
|
|
|
163
|
-
|
|
164
|
-
|
|
174
|
+
if deferred_spreads
|
|
175
|
+
visited_fragments ||= {}
|
|
176
|
+
sel_idx = 0
|
|
177
|
+
sel_len = deferred_spreads.size
|
|
165
178
|
|
|
166
|
-
|
|
167
|
-
|
|
179
|
+
while sel_idx < sel_len
|
|
180
|
+
sel = deferred_spreads[sel_idx]
|
|
181
|
+
sel_idx += 1
|
|
182
|
+
next if visited_fragments.key?(sel.name)
|
|
168
183
|
|
|
169
|
-
|
|
184
|
+
visited_fragments[sel.name] = true
|
|
185
|
+
frag = @fragments[sel.name]
|
|
186
|
+
next unless frag
|
|
170
187
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
find_conflicts_between(
|
|
174
|
-
fields,
|
|
175
|
-
fragment_fields,
|
|
176
|
-
mutually_exclusive: mutually_exclusive,
|
|
177
|
-
)
|
|
188
|
+
frag_type = @types.type(frag.type.name)
|
|
189
|
+
next unless frag_type
|
|
178
190
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
fragment_spread,
|
|
184
|
-
fields,
|
|
185
|
-
mutually_exclusive: mutually_exclusive,
|
|
186
|
-
)
|
|
191
|
+
new_parents = parents.dup
|
|
192
|
+
new_parents << frag_type
|
|
193
|
+
collect_fields_inner(frag.selections, owner_type: frag_type, parents: new_parents, response_keys: response_keys, visited_fragments: visited_fragments)
|
|
194
|
+
end
|
|
187
195
|
end
|
|
188
196
|
end
|
|
189
197
|
|
|
190
198
|
def find_conflicts_within(response_keys)
|
|
191
199
|
response_keys.each do |key, fields|
|
|
192
|
-
next
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
+
next unless fields.is_a?(Array)
|
|
201
|
+
|
|
202
|
+
# Optimization: group fields by signature (name + definition + arguments).
|
|
203
|
+
# Fields with the same signature can only conflict on sub-selections,
|
|
204
|
+
# so we only need to compare one pair within each group.
|
|
205
|
+
if fields.size > 4
|
|
206
|
+
f0 = fields[0]
|
|
207
|
+
all_same = true
|
|
208
|
+
i = 1
|
|
209
|
+
while i < fields.size
|
|
210
|
+
unless fields_same_signature?(f0, fields[i])
|
|
211
|
+
all_same = false
|
|
212
|
+
break
|
|
213
|
+
end
|
|
214
|
+
i += 1
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
if all_same
|
|
218
|
+
# All fields share a signature, so they can only conflict on
|
|
219
|
+
# sub-selections. Deduplicate by AST node identity — fields from
|
|
220
|
+
# the same node always have identical sub-selections.
|
|
221
|
+
unique_nodes = fields.uniq { |f| f.node.object_id }
|
|
222
|
+
i = 0
|
|
223
|
+
while i < unique_nodes.size
|
|
224
|
+
j = i + 1
|
|
225
|
+
while j < unique_nodes.size
|
|
226
|
+
if unique_nodes[i].node.selections.size > 0 || unique_nodes[j].node.selections.size > 0
|
|
227
|
+
find_conflict(key, unique_nodes[i], unique_nodes[j])
|
|
228
|
+
end
|
|
229
|
+
j += 1
|
|
230
|
+
end
|
|
231
|
+
i += 1
|
|
232
|
+
end
|
|
233
|
+
else
|
|
234
|
+
groups = fields.group_by { |f| field_signature(f) }
|
|
235
|
+
unique_groups = groups.values
|
|
236
|
+
|
|
237
|
+
# Compare representatives across different groups
|
|
238
|
+
gi = 0
|
|
239
|
+
while gi < unique_groups.size
|
|
240
|
+
gj = gi + 1
|
|
241
|
+
while gj < unique_groups.size
|
|
242
|
+
find_conflict(key, unique_groups[gi][0], unique_groups[gj][0])
|
|
243
|
+
gj += 1
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Within same group, deduplicate by AST node and compare all
|
|
247
|
+
# pairs for sub-selection conflicts
|
|
248
|
+
group = unique_groups[gi]
|
|
249
|
+
if group.size >= 2
|
|
250
|
+
unique_in_group = group.uniq { |f| f.node.object_id }
|
|
251
|
+
ui = 0
|
|
252
|
+
while ui < unique_in_group.size
|
|
253
|
+
uj = ui + 1
|
|
254
|
+
while uj < unique_in_group.size
|
|
255
|
+
if unique_in_group[ui].node.selections.size > 0 || unique_in_group[uj].node.selections.size > 0
|
|
256
|
+
find_conflict(key, unique_in_group[ui], unique_in_group[uj])
|
|
257
|
+
end
|
|
258
|
+
uj += 1
|
|
259
|
+
end
|
|
260
|
+
ui += 1
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
gi += 1
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
else
|
|
268
|
+
# Small number of fields — original O(n²) is fine
|
|
269
|
+
i = 0
|
|
270
|
+
while i < fields.size
|
|
271
|
+
j = i + 1
|
|
272
|
+
while j < fields.size
|
|
273
|
+
find_conflict(key, fields[i], fields[j])
|
|
274
|
+
j += 1
|
|
275
|
+
end
|
|
276
|
+
i += 1
|
|
200
277
|
end
|
|
201
|
-
i += 1
|
|
202
278
|
end
|
|
203
279
|
end
|
|
204
280
|
end
|
|
205
281
|
|
|
282
|
+
def fields_same_signature?(f1, f2)
|
|
283
|
+
n1 = f1.node
|
|
284
|
+
n2 = f2.node
|
|
285
|
+
|
|
286
|
+
f1.definition.equal?(f2.definition) &&
|
|
287
|
+
n1.name == n2.name &&
|
|
288
|
+
same_arguments?(n1, n2)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def field_signature(field)
|
|
292
|
+
node = field.node
|
|
293
|
+
defn = field.definition
|
|
294
|
+
args = node.arguments
|
|
295
|
+
|
|
296
|
+
if args.empty?
|
|
297
|
+
[node.name, defn.object_id]
|
|
298
|
+
else
|
|
299
|
+
[node.name, defn.object_id, args.map { |a| [a.name, serialize_arg(a.value)] }]
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
206
303
|
def find_conflict(response_key, field1, field2, mutually_exclusive: false)
|
|
207
|
-
return if @conflict_count >=
|
|
304
|
+
return if @conflict_count >= @max_errors
|
|
208
305
|
return if field1.definition.nil? || field2.definition.nil?
|
|
209
306
|
|
|
210
307
|
node1 = field1.node
|
|
211
308
|
node2 = field2.node
|
|
212
309
|
|
|
213
|
-
are_mutually_exclusive = mutually_exclusive ||
|
|
214
|
-
mutually_exclusive?(field1.parents, field2.parents)
|
|
310
|
+
are_mutually_exclusive = mutually_exclusive || mutually_exclusive?(field1.parents, field2.parents)
|
|
215
311
|
|
|
216
312
|
if !are_mutually_exclusive
|
|
217
313
|
if node1.name != node2.name
|
|
@@ -234,17 +330,20 @@ module GraphQL
|
|
|
234
330
|
end
|
|
235
331
|
|
|
236
332
|
if !conflicts[:field].key?(response_key) &&
|
|
237
|
-
|
|
238
|
-
(
|
|
333
|
+
!field1.definition.equal?(field2.definition) &&
|
|
334
|
+
(t1 = field1.return_type) &&
|
|
335
|
+
(t2 = field2.return_type) &&
|
|
239
336
|
return_types_conflict?(t1, t2)
|
|
240
337
|
|
|
241
338
|
return_error = nil
|
|
242
339
|
message_override = nil
|
|
340
|
+
|
|
243
341
|
case @schema.allow_legacy_invalid_return_type_conflicts
|
|
244
342
|
when false
|
|
245
343
|
return_error = true
|
|
246
344
|
when true
|
|
247
345
|
legacy_handling = @schema.legacy_invalid_return_type_conflicts(@context.query, t1, t2, node1, node2)
|
|
346
|
+
|
|
248
347
|
case legacy_handling
|
|
249
348
|
when nil
|
|
250
349
|
return_error = false
|
|
@@ -267,9 +366,11 @@ module GraphQL
|
|
|
267
366
|
|
|
268
367
|
if return_error
|
|
269
368
|
conflict = conflicts[:return_type][response_key]
|
|
369
|
+
|
|
270
370
|
if message_override
|
|
271
371
|
conflict.message = message_override
|
|
272
372
|
end
|
|
373
|
+
|
|
273
374
|
conflict.add_conflict(node1, "`#{t1.to_type_signature}`")
|
|
274
375
|
conflict.add_conflict(node2, "`#{t2.to_type_signature}`")
|
|
275
376
|
@conflict_count += 1
|
|
@@ -303,121 +404,78 @@ module GraphQL
|
|
|
303
404
|
elsif type1.kind.leaf? && type2.kind.leaf?
|
|
304
405
|
type1 != type2
|
|
305
406
|
else
|
|
306
|
-
# One or more of these are composite types,
|
|
307
|
-
# their selections will be validated later on.
|
|
308
407
|
false
|
|
309
408
|
end
|
|
310
409
|
end
|
|
311
410
|
|
|
411
|
+
# When two fields with the same response key both have sub-selections,
|
|
412
|
+
# we need to check those sub-selections against each other.
|
|
312
413
|
def find_conflicts_between_sub_selection_sets(field1, field2, mutually_exclusive:)
|
|
313
414
|
return if field1.definition.nil? ||
|
|
314
415
|
field2.definition.nil? ||
|
|
315
416
|
(field1.node.selections.empty? && field2.node.selections.empty?)
|
|
316
417
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
parents1 = [return_type1]
|
|
320
|
-
parents2 = [return_type2]
|
|
418
|
+
node1 = field1.node
|
|
419
|
+
node2 = field2.node
|
|
321
420
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
owner_type: return_type1,
|
|
325
|
-
parents: parents1
|
|
326
|
-
)
|
|
421
|
+
# Prevent infinite recursion from cyclic fragments
|
|
422
|
+
return if node1.equal?(node2)
|
|
327
423
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
# (I) Then collect conflicts between the first collection of fields and
|
|
338
|
-
# those referenced by each fragment name associated with the second.
|
|
339
|
-
fragment_spreads2.each do |fragment_spread|
|
|
340
|
-
find_conflicts_between_fields_and_fragment(
|
|
341
|
-
fragment_spread,
|
|
342
|
-
fields,
|
|
343
|
-
mutually_exclusive: mutually_exclusive,
|
|
344
|
-
)
|
|
424
|
+
inner = @compared_sub_selections[node1]
|
|
425
|
+
if inner
|
|
426
|
+
return if inner.key?(node2)
|
|
427
|
+
inner[node2] = true
|
|
428
|
+
else
|
|
429
|
+
inner = {}.compare_by_identity
|
|
430
|
+
inner[node2] = true
|
|
431
|
+
@compared_sub_selections[node1] = inner
|
|
345
432
|
end
|
|
346
433
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
fragment_spreads.each do |fragment_spread|
|
|
350
|
-
find_conflicts_between_fields_and_fragment(
|
|
351
|
-
fragment_spread,
|
|
352
|
-
fields2,
|
|
353
|
-
mutually_exclusive: mutually_exclusive,
|
|
354
|
-
)
|
|
355
|
-
end
|
|
434
|
+
return_type1 = field1.unwrapped_return_type
|
|
435
|
+
return_type2 = field2.unwrapped_return_type
|
|
356
436
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
# names to each item in the second set of names.
|
|
360
|
-
fragment_spreads.each do |frag1|
|
|
361
|
-
fragment_spreads2.each do |frag2|
|
|
362
|
-
find_conflicts_between_fragments(
|
|
363
|
-
frag1,
|
|
364
|
-
frag2,
|
|
365
|
-
mutually_exclusive: mutually_exclusive
|
|
366
|
-
)
|
|
367
|
-
end
|
|
368
|
-
end
|
|
369
|
-
end
|
|
437
|
+
response_keys1 = cached_sub_fields(node1, return_type1)
|
|
438
|
+
response_keys2 = cached_sub_fields(node2, return_type2)
|
|
370
439
|
|
|
371
|
-
|
|
372
|
-
response_keys.each do |key, fields|
|
|
373
|
-
fields2 = response_keys2[key]
|
|
374
|
-
if fields2
|
|
375
|
-
fields.each do |field|
|
|
376
|
-
fields2.each do |field2|
|
|
377
|
-
find_conflict(
|
|
378
|
-
key,
|
|
379
|
-
field,
|
|
380
|
-
field2,
|
|
381
|
-
mutually_exclusive: mutually_exclusive,
|
|
382
|
-
)
|
|
383
|
-
end
|
|
384
|
-
end
|
|
385
|
-
end
|
|
386
|
-
end
|
|
440
|
+
find_conflicts_between(response_keys1, response_keys2, mutually_exclusive: mutually_exclusive)
|
|
387
441
|
end
|
|
388
442
|
|
|
389
|
-
|
|
443
|
+
def cached_sub_fields(node, return_type)
|
|
444
|
+
inner = @sub_fields_cache[node]
|
|
390
445
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
NO_SELECTIONS
|
|
446
|
+
if inner && inner.key?(return_type)
|
|
447
|
+
inner[return_type]
|
|
394
448
|
else
|
|
395
|
-
parents
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
[
|
|
449
|
+
result = collect_fields(node.selections, owner_type: return_type, parents: [return_type])
|
|
450
|
+
inner ||= {}.compare_by_identity
|
|
451
|
+
inner[return_type] = result
|
|
452
|
+
@sub_fields_cache[node] = inner
|
|
453
|
+
result
|
|
399
454
|
end
|
|
400
455
|
end
|
|
401
456
|
|
|
402
|
-
def
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
457
|
+
def find_conflicts_between(response_keys, response_keys2, mutually_exclusive:)
|
|
458
|
+
response_keys.each do |key, fields|
|
|
459
|
+
fields2 = response_keys2[key]
|
|
460
|
+
next unless fields2
|
|
461
|
+
|
|
462
|
+
fields_arr = fields.is_a?(Field) ? [fields] : fields
|
|
463
|
+
fields2_arr = fields2.is_a?(Field) ? [fields2] : fields2
|
|
464
|
+
|
|
465
|
+
fields_arr.each do |field|
|
|
466
|
+
fields2_arr.each do |field2|
|
|
467
|
+
find_conflict(
|
|
468
|
+
key,
|
|
469
|
+
field,
|
|
470
|
+
field2,
|
|
471
|
+
mutually_exclusive: mutually_exclusive,
|
|
472
|
+
)
|
|
473
|
+
end
|
|
413
474
|
end
|
|
414
475
|
end
|
|
415
|
-
|
|
416
|
-
[fields, fragment_spreads]
|
|
417
476
|
end
|
|
418
477
|
|
|
419
478
|
def same_arguments?(field1, field2)
|
|
420
|
-
# Check for incompatible / non-identical arguments on this node:
|
|
421
479
|
arguments1 = field1.arguments
|
|
422
480
|
arguments2 = field2.arguments
|
|
423
481
|
|
|
@@ -450,39 +508,47 @@ module GraphQL
|
|
|
450
508
|
serialized_args
|
|
451
509
|
end
|
|
452
510
|
|
|
453
|
-
def compared_fragments_key(frag1, frag2, exclusive)
|
|
454
|
-
# Cache key to not compare two fragments more than once.
|
|
455
|
-
# The key includes both fragment names sorted (this way we
|
|
456
|
-
# avoid computing "A vs B" and "B vs A"). It also includes
|
|
457
|
-
# "exclusive" since the result may change depending on the parent_type
|
|
458
|
-
"#{[frag1, frag2].sort.join('-')}-#{exclusive}"
|
|
459
|
-
end
|
|
460
|
-
|
|
461
511
|
# Given two list of parents, find out if they are mutually exclusive
|
|
462
|
-
# In this context, `parents` represents the "self scope" of the field,
|
|
463
|
-
# what types may be found at this point in the query.
|
|
464
512
|
def mutually_exclusive?(parents1, parents2)
|
|
465
513
|
if parents1.empty? || parents2.empty?
|
|
466
514
|
false
|
|
467
515
|
elsif parents1.length == parents2.length
|
|
468
|
-
|
|
516
|
+
i = 0
|
|
517
|
+
len = parents1.length
|
|
518
|
+
|
|
519
|
+
while i < len
|
|
469
520
|
type1 = parents1[i - 1]
|
|
470
521
|
type2 = parents2[i - 1]
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
522
|
+
unless type1.equal?(type2)
|
|
523
|
+
inner = @mutually_exclusive_cache[type1]
|
|
524
|
+
if inner
|
|
525
|
+
cached = inner[type2]
|
|
526
|
+
if cached.nil?
|
|
527
|
+
cached = types_mutually_exclusive?(type1, type2)
|
|
528
|
+
inner[type2] = cached
|
|
529
|
+
end
|
|
530
|
+
else
|
|
531
|
+
cached = types_mutually_exclusive?(type1, type2)
|
|
532
|
+
inner = {}.compare_by_identity
|
|
533
|
+
inner[type2] = cached
|
|
534
|
+
@mutually_exclusive_cache[type1] = inner
|
|
535
|
+
end
|
|
536
|
+
return true if cached
|
|
480
537
|
end
|
|
538
|
+
i += 1
|
|
481
539
|
end
|
|
540
|
+
|
|
541
|
+
false
|
|
482
542
|
else
|
|
483
543
|
true
|
|
484
544
|
end
|
|
485
545
|
end
|
|
546
|
+
|
|
547
|
+
def types_mutually_exclusive?(type1, type2)
|
|
548
|
+
possible_right_types = @types.possible_types(type1)
|
|
549
|
+
possible_left_types = @types.possible_types(type2)
|
|
550
|
+
(possible_right_types & possible_left_types).empty?
|
|
551
|
+
end
|
|
486
552
|
end
|
|
487
553
|
end
|
|
488
554
|
end
|