graphql 2.5.11 → 2.6.10
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/generators/graphql/detailed_trace_generator.rb +77 -0
- data/lib/generators/graphql/field_extractor.rb +17 -1
- data/lib/generators/graphql/templates/create_graphql_detailed_traces.erb +10 -0
- data/lib/generators/graphql/templates/schema.erb +2 -1
- data/lib/graphql/analysis/query_complexity.rb +31 -15
- data/lib/graphql/analysis.rb +20 -13
- data/lib/graphql/backtrace/table.rb +10 -1
- data/lib/graphql/current.rb +7 -1
- data/lib/graphql/dashboard/application_controller.rb +45 -0
- data/lib/graphql/dashboard/landings_controller.rb +9 -0
- data/lib/graphql/dashboard/statics_controller.rb +31 -0
- data/lib/graphql/dashboard/subscriptions.rb +2 -1
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/_form.html.erb +1 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/edit.html.erb +2 -2
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/index.html.erb +1 -1
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/new.html.erb +1 -1
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/index.html.erb +1 -1
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/show.html.erb +1 -1
- data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/show.html.erb +1 -1
- data/lib/graphql/dashboard/views/layouts/graphql/dashboard/application.html.erb +7 -7
- data/lib/graphql/dashboard.rb +14 -73
- data/lib/graphql/dataloader/async_dataloader.rb +336 -60
- data/lib/graphql/dataloader/null_dataloader.rb +48 -10
- data/lib/graphql/dataloader/source.rb +32 -19
- data/lib/graphql/dataloader.rb +107 -39
- data/lib/graphql/date_encoding_error.rb +1 -1
- data/lib/graphql/execution/directive_checks.rb +2 -0
- data/lib/graphql/execution/field_resolve_step.rb +804 -0
- data/lib/graphql/execution/finalize.rb +229 -0
- data/lib/graphql/execution/input_values.rb +333 -0
- data/lib/graphql/execution/interpreter/arguments_cache.rb +3 -0
- data/lib/graphql/execution/interpreter/handles_raw_value.rb +6 -0
- data/lib/graphql/execution/interpreter/resolve.rb +7 -13
- data/lib/graphql/execution/interpreter/runtime/graphql_result.rb +13 -0
- data/lib/graphql/execution/interpreter/runtime.rb +60 -40
- data/lib/graphql/execution/interpreter.rb +17 -23
- data/lib/graphql/execution/lazy.rb +1 -1
- data/lib/graphql/execution/load_argument_step.rb +102 -0
- data/lib/graphql/execution/multiplex.rb +1 -1
- data/lib/graphql/execution/next.rb +98 -0
- data/lib/graphql/execution/prepare_object_step.rb +151 -0
- data/lib/graphql/execution/resolve_type_step.rb +27 -0
- data/lib/graphql/execution/runner.rb +457 -0
- data/lib/graphql/execution/selections_step.rb +95 -0
- data/lib/graphql/execution.rb +15 -4
- data/lib/graphql/execution_error.rb +17 -12
- data/lib/graphql/float_decoding_error.rb +13 -0
- data/lib/graphql/float_encoding_error.rb +28 -0
- data/lib/graphql/introspection/directive_type.rb +7 -3
- data/lib/graphql/introspection/dynamic_fields.rb +5 -1
- data/lib/graphql/introspection/entry_points.rb +11 -3
- data/lib/graphql/introspection/enum_value_type.rb +5 -5
- data/lib/graphql/introspection/field_type.rb +13 -5
- data/lib/graphql/introspection/input_value_type.rb +21 -13
- data/lib/graphql/introspection/schema_type.rb +6 -2
- data/lib/graphql/introspection/type_type.rb +64 -28
- data/lib/graphql/invalid_null_error.rb +11 -5
- data/lib/graphql/language/block_string.rb +6 -11
- data/lib/graphql/language/cache.rb +84 -14
- data/lib/graphql/language/document_from_schema_definition.rb +2 -1
- data/lib/graphql/language/lexer.rb +27 -17
- data/lib/graphql/language/nodes.rb +2 -1
- data/lib/graphql/language/parser.rb +1 -1
- data/lib/graphql/language.rb +32 -17
- data/lib/graphql/pagination/connection.rb +2 -0
- data/lib/graphql/pagination/connections.rb +30 -0
- data/lib/graphql/pagination/relation_connection.rb +10 -1
- data/lib/graphql/query/context.rb +10 -3
- data/lib/graphql/query/null_context.rb +9 -3
- data/lib/graphql/query/partial.rb +18 -3
- data/lib/graphql/query/variable_validation_error.rb +16 -1
- data/lib/graphql/query/variables.rb +1 -1
- data/lib/graphql/query.rb +12 -3
- data/lib/graphql/railtie.rb +2 -1
- data/lib/graphql/runtime_error.rb +6 -0
- data/lib/graphql/schema/argument.rb +13 -1
- data/lib/graphql/schema/build_from_definition.rb +28 -1
- data/lib/graphql/schema/directive/feature.rb +4 -0
- data/lib/graphql/schema/directive/transform.rb +20 -0
- data/lib/graphql/schema/directive.rb +48 -13
- data/lib/graphql/schema/field/connection_extension.rb +4 -37
- data/lib/graphql/schema/field/scope_extension.rb +18 -13
- data/lib/graphql/schema/field.rb +79 -48
- data/lib/graphql/schema/field_extension.rb +11 -8
- data/lib/graphql/schema/has_single_input_argument.rb +24 -13
- data/lib/graphql/schema/input_object.rb +6 -2
- data/lib/graphql/schema/interface.rb +26 -0
- data/lib/graphql/schema/introspection_system.rb +6 -21
- data/lib/graphql/schema/list.rb +5 -1
- data/lib/graphql/schema/member/base_dsl_methods.rb +1 -11
- data/lib/graphql/schema/member/has_arguments.rb +43 -14
- data/lib/graphql/schema/member/has_authorization.rb +35 -0
- data/lib/graphql/schema/member/has_dataloader.rb +37 -0
- data/lib/graphql/schema/member/has_fields.rb +86 -5
- data/lib/graphql/schema/member.rb +5 -0
- data/lib/graphql/schema/non_null.rb +1 -1
- data/lib/graphql/schema/object.rb +1 -0
- data/lib/graphql/schema/printer.rb +1 -1
- data/lib/graphql/schema/ractor_shareable.rb +1 -0
- data/lib/graphql/schema/relay_classic_mutation.rb +16 -2
- data/lib/graphql/schema/resolver.rb +76 -1
- data/lib/graphql/schema/subscription.rb +53 -10
- data/lib/graphql/schema/timeout.rb +2 -2
- data/lib/graphql/schema/validator/allow_blank_validator.rb +3 -3
- data/lib/graphql/schema/validator/allow_null_validator.rb +3 -3
- data/lib/graphql/schema/validator/exclusion_validator.rb +2 -2
- data/lib/graphql/schema/validator/format_validator.rb +3 -3
- data/lib/graphql/schema/validator/inclusion_validator.rb +2 -2
- data/lib/graphql/schema/validator/length_validator.rb +6 -6
- data/lib/graphql/schema/validator/numericality_validator.rb +19 -19
- data/lib/graphql/schema/validator/required_validator.rb +38 -5
- data/lib/graphql/schema/validator.rb +9 -0
- data/lib/graphql/schema/visibility/profile.rb +74 -53
- data/lib/graphql/schema/visibility/visit.rb +1 -1
- data/lib/graphql/schema/visibility.rb +32 -24
- data/lib/graphql/schema/wrapper.rb +11 -1
- data/lib/graphql/schema.rb +93 -27
- 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 +14 -4
- data/lib/graphql/static_validation/rules/fields_will_merge.rb +351 -248
- 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/action_cable_subscriptions.rb +1 -0
- data/lib/graphql/subscriptions/default_subscription_resolve_extension.rb +40 -10
- data/lib/graphql/subscriptions/serialize.rb +10 -4
- data/lib/graphql/subscriptions.rb +36 -1
- data/lib/graphql/testing/helpers.rb +12 -9
- data/lib/graphql/testing/mock_action_cable.rb +111 -0
- data/lib/graphql/testing.rb +1 -0
- data/lib/graphql/tracing/detailed_trace/active_record_backend.rb +74 -0
- data/lib/graphql/tracing/detailed_trace.rb +70 -7
- data/lib/graphql/tracing/perfetto_trace.rb +213 -81
- data/lib/graphql/tracing/sentry_trace.rb +3 -1
- data/lib/graphql/tracing/trace.rb +6 -0
- data/lib/graphql/types/float.rb +18 -4
- data/lib/graphql/types/relay/connection_behaviors.rb +8 -6
- data/lib/graphql/types/relay/edge_behaviors.rb +4 -3
- data/lib/graphql/types/relay/has_node_field.rb +13 -8
- data/lib/graphql/types/relay/has_nodes_field.rb +13 -8
- data/lib/graphql/types/relay/node_behaviors.rb +13 -2
- data/lib/graphql/unauthorized_error.rb +9 -1
- data/lib/graphql/version.rb +1 -1
- data/lib/graphql.rb +8 -2
- metadata +23 -59
|
@@ -8,31 +8,112 @@ 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
|
|
18
|
+
EXCLUSIVE_COMPARISON = 1
|
|
19
|
+
NONEXCLUSIVE_COMPARISON = 2
|
|
20
|
+
|
|
21
|
+
class Field
|
|
22
|
+
attr_reader :node, :definition, :owner_type, :parents
|
|
23
|
+
|
|
24
|
+
def initialize(node, definition, owner_type, parents)
|
|
25
|
+
@node = node
|
|
26
|
+
@definition = definition
|
|
27
|
+
@owner_type = owner_type
|
|
28
|
+
@parents = parents
|
|
29
|
+
end
|
|
13
30
|
|
|
14
|
-
|
|
15
|
-
|
|
31
|
+
def return_type
|
|
32
|
+
@return_type ||= @definition&.type
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def unwrapped_return_type
|
|
36
|
+
@unwrapped_return_type ||= return_type&.unwrap
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def comparison_key
|
|
40
|
+
@comparison_key ||= [@node, @parents]
|
|
41
|
+
end
|
|
42
|
+
end
|
|
16
43
|
|
|
17
44
|
def initialize(*)
|
|
18
45
|
super
|
|
19
|
-
@visited_fragments = {}
|
|
20
|
-
@compared_fragments = {}
|
|
21
46
|
@conflict_count = 0
|
|
47
|
+
@max_errors = context.max_errors
|
|
48
|
+
@fragments = context.fragments
|
|
49
|
+
# Track which sub-selection node pairs have been compared to prevent
|
|
50
|
+
# infinite recursion with cyclic fragments
|
|
51
|
+
@compared_sub_selections = {}.compare_by_identity
|
|
52
|
+
@compared_field_groups = {}
|
|
53
|
+
@field_group_signatures = {}.compare_by_identity
|
|
54
|
+
@field_selection_signatures = {}.compare_by_identity
|
|
55
|
+
# Cache mutually_exclusive? results for type pairs
|
|
56
|
+
@mutually_exclusive_cache = {}.compare_by_identity
|
|
57
|
+
# Cache collect_fields results for sub-selection comparison
|
|
58
|
+
@sub_fields_cache = {}.compare_by_identity
|
|
22
59
|
end
|
|
23
60
|
|
|
24
61
|
def on_operation_definition(node, _parent)
|
|
25
|
-
|
|
62
|
+
@conflicts = nil
|
|
63
|
+
conflicts_within_selection_set(node, type_definition)
|
|
64
|
+
@conflicts&.each_value { |error_type| error_type.each_value { |error| add_error(error) } }
|
|
26
65
|
super
|
|
27
66
|
end
|
|
28
67
|
|
|
29
68
|
def on_field(node, _parent)
|
|
30
|
-
|
|
69
|
+
if !node.selections.empty? && selections_may_conflict?(node.selections)
|
|
70
|
+
@conflicts = nil
|
|
71
|
+
conflicts_within_selection_set(node, type_definition)
|
|
72
|
+
@conflicts&.each_value { |error_type| error_type.each_value { |error| add_error(error) } }
|
|
73
|
+
end
|
|
31
74
|
super
|
|
32
75
|
end
|
|
33
76
|
|
|
34
77
|
private
|
|
35
78
|
|
|
79
|
+
# Quick check: can the direct children of this selection set possibly conflict?
|
|
80
|
+
# If all direct selections are Fields with unique names and no aliases,
|
|
81
|
+
# and there are no fragments, then no response key can have >1 field,
|
|
82
|
+
# so there are no merge conflicts to check at this level.
|
|
83
|
+
def selections_may_conflict?(selections)
|
|
84
|
+
i = 0
|
|
85
|
+
len = selections.size
|
|
86
|
+
while i < len
|
|
87
|
+
sel = selections[i]
|
|
88
|
+
# Fragment spread or inline fragment — needs full check
|
|
89
|
+
return true unless sel.is_a?(GraphQL::Language::Nodes::Field)
|
|
90
|
+
|
|
91
|
+
# Aliased field — could create duplicate response key
|
|
92
|
+
return true if sel.alias
|
|
93
|
+
|
|
94
|
+
i += 1
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# All are unaliased fields — check for duplicate names
|
|
98
|
+
# For small sets, O(n²) is cheaper than hash allocation
|
|
99
|
+
if len <= 8
|
|
100
|
+
i = 0
|
|
101
|
+
while i < len
|
|
102
|
+
j = i + 1
|
|
103
|
+
name_i = selections[i].name
|
|
104
|
+
while j < len
|
|
105
|
+
return true if selections[j].name == name_i
|
|
106
|
+
j += 1
|
|
107
|
+
end
|
|
108
|
+
i += 1
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
false
|
|
112
|
+
else
|
|
113
|
+
true # Assume potential conflicts for larger sets
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
36
117
|
def conflicts
|
|
37
118
|
@conflicts ||= Hash.new do |h, error_type|
|
|
38
119
|
h[error_type] = Hash.new do |h2, field_name|
|
|
@@ -41,177 +122,198 @@ module GraphQL
|
|
|
41
122
|
end
|
|
42
123
|
end
|
|
43
124
|
|
|
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
|
-
|
|
125
|
+
# Core algorithm: collect ALL fields (expanding fragments inline) into a flat
|
|
126
|
+
# map keyed by response key, then compare within each group.
|
|
51
127
|
def conflicts_within_selection_set(node, parent_type)
|
|
52
128
|
return if parent_type.nil?
|
|
129
|
+
return if node.selections.empty?
|
|
53
130
|
|
|
54
|
-
fields
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
131
|
+
# Collect all fields from this selection set, expanding fragments transitively
|
|
132
|
+
response_keys = collect_fields(node.selections, owner_type: parent_type, parents: [])
|
|
133
|
+
|
|
134
|
+
# Find conflicts within each response key group
|
|
135
|
+
find_conflicts_within(response_keys)
|
|
90
136
|
end
|
|
91
137
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
138
|
+
# Collect all fields from selections, expanding fragment spreads inline.
|
|
139
|
+
# Returns a Hash of { response_key => Field | [Field, ...] }
|
|
140
|
+
def collect_fields(selections, owner_type:, parents:)
|
|
141
|
+
response_keys = {}
|
|
142
|
+
collect_fields_inner(selections, owner_type: owner_type, parents: parents, response_keys: response_keys, visited_fragments: nil)
|
|
143
|
+
response_keys
|
|
144
|
+
end
|
|
96
145
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
)
|
|
102
|
-
if @compared_fragments.key?(cache_key)
|
|
103
|
-
return
|
|
104
|
-
else
|
|
105
|
-
@compared_fragments[cache_key] = true
|
|
106
|
-
end
|
|
146
|
+
def collect_fields_inner(selections, owner_type:, parents:, response_keys:, visited_fragments:)
|
|
147
|
+
deferred_spreads = nil
|
|
148
|
+
sel_idx = 0
|
|
149
|
+
sel_len = selections.size
|
|
107
150
|
|
|
108
|
-
|
|
109
|
-
|
|
151
|
+
while sel_idx < sel_len
|
|
152
|
+
sel = selections[sel_idx]
|
|
110
153
|
|
|
111
|
-
|
|
154
|
+
case sel
|
|
155
|
+
when GraphQL::Language::Nodes::Field
|
|
156
|
+
definition = @types.field(owner_type, sel.name)
|
|
157
|
+
key = sel.alias || sel.name
|
|
158
|
+
field = Field.new(sel, definition, owner_type, parents)
|
|
159
|
+
existing = response_keys[key]
|
|
160
|
+
|
|
161
|
+
if existing.nil?
|
|
162
|
+
response_keys[key] = field
|
|
163
|
+
elsif existing.is_a?(Field)
|
|
164
|
+
response_keys[key] = [existing, field]
|
|
165
|
+
else
|
|
166
|
+
existing << field
|
|
167
|
+
end
|
|
168
|
+
when GraphQL::Language::Nodes::InlineFragment
|
|
169
|
+
frag_type = sel.type ? @types.type(sel.type.name) : owner_type
|
|
170
|
+
|
|
171
|
+
if frag_type
|
|
172
|
+
new_parents = parents.dup
|
|
173
|
+
new_parents << frag_type
|
|
174
|
+
collect_fields_inner(sel.selections, owner_type: frag_type, parents: new_parents, response_keys: response_keys, visited_fragments: visited_fragments)
|
|
175
|
+
end
|
|
176
|
+
when GraphQL::Language::Nodes::FragmentSpread
|
|
177
|
+
(deferred_spreads ||= []) << sel
|
|
178
|
+
end
|
|
112
179
|
|
|
113
|
-
|
|
114
|
-
|
|
180
|
+
sel_idx += 1
|
|
181
|
+
end
|
|
115
182
|
|
|
116
|
-
|
|
183
|
+
if deferred_spreads
|
|
184
|
+
visited_fragments ||= {}
|
|
185
|
+
sel_idx = 0
|
|
186
|
+
sel_len = deferred_spreads.size
|
|
117
187
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
)
|
|
123
|
-
fragment_fields2, fragment_spreads2 = fields_and_fragments_from_selection(
|
|
124
|
-
fragment2,
|
|
125
|
-
owner_type: fragment_type1,
|
|
126
|
-
parents: [*fragment_spread2.parents, fragment_type2]
|
|
127
|
-
)
|
|
188
|
+
while sel_idx < sel_len
|
|
189
|
+
sel = deferred_spreads[sel_idx]
|
|
190
|
+
sel_idx += 1
|
|
191
|
+
next if visited_fragments.key?(sel.name)
|
|
128
192
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
fragment_fields1,
|
|
133
|
-
fragment_fields2,
|
|
134
|
-
mutually_exclusive: mutually_exclusive,
|
|
135
|
-
)
|
|
193
|
+
visited_fragments[sel.name] = true
|
|
194
|
+
frag = @fragments[sel.name]
|
|
195
|
+
next unless frag
|
|
136
196
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
fragment_spreads2.each do |fragment_spread|
|
|
140
|
-
find_conflicts_between_fragments(
|
|
141
|
-
fragment_spread1,
|
|
142
|
-
fragment_spread,
|
|
143
|
-
mutually_exclusive: mutually_exclusive,
|
|
144
|
-
)
|
|
145
|
-
end
|
|
197
|
+
frag_type = @types.type(frag.type.name)
|
|
198
|
+
next unless frag_type
|
|
146
199
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
fragment_spread2,
|
|
152
|
-
fragment_spread,
|
|
153
|
-
mutually_exclusive: mutually_exclusive,
|
|
154
|
-
)
|
|
200
|
+
new_parents = parents.dup
|
|
201
|
+
new_parents << frag_type
|
|
202
|
+
collect_fields_inner(frag.selections, owner_type: frag_type, parents: new_parents, response_keys: response_keys, visited_fragments: visited_fragments)
|
|
203
|
+
end
|
|
155
204
|
end
|
|
156
205
|
end
|
|
157
206
|
|
|
158
|
-
def
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
207
|
+
def find_conflicts_within(response_keys)
|
|
208
|
+
response_keys.each do |key, fields|
|
|
209
|
+
next unless fields.is_a?(Array)
|
|
210
|
+
|
|
211
|
+
# Optimization: group fields by signature (name + definition + arguments).
|
|
212
|
+
# Fields with the same signature can only conflict on sub-selections,
|
|
213
|
+
# so we only need to compare one pair within each group.
|
|
214
|
+
if fields.size > 4
|
|
215
|
+
f0 = fields[0]
|
|
216
|
+
all_same = true
|
|
217
|
+
i = 1
|
|
218
|
+
while i < fields.size
|
|
219
|
+
unless fields_same_signature?(f0, fields[i])
|
|
220
|
+
all_same = false
|
|
221
|
+
break
|
|
222
|
+
end
|
|
223
|
+
i += 1
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
if all_same
|
|
227
|
+
find_conflicts_between_selection_groups(key, fields)
|
|
228
|
+
else
|
|
229
|
+
groups = fields.group_by { |f| field_signature(f) }
|
|
230
|
+
unique_groups = groups.values
|
|
231
|
+
|
|
232
|
+
# Compare representatives across different groups
|
|
233
|
+
gi = 0
|
|
234
|
+
while gi < unique_groups.size
|
|
235
|
+
gj = gi + 1
|
|
236
|
+
while gj < unique_groups.size
|
|
237
|
+
find_conflict(key, unique_groups[gi][0], unique_groups[gj][0])
|
|
238
|
+
gj += 1
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Within the same group, fields can only conflict on sub-selections.
|
|
242
|
+
group = unique_groups[gi]
|
|
243
|
+
if group.size >= 2
|
|
244
|
+
find_conflicts_between_selection_groups(key, group)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
gi += 1
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
else
|
|
251
|
+
# Small number of fields — original O(n²) is fine
|
|
252
|
+
i = 0
|
|
253
|
+
while i < fields.size
|
|
254
|
+
j = i + 1
|
|
255
|
+
while j < fields.size
|
|
256
|
+
find_conflict(key, fields[i], fields[j])
|
|
257
|
+
j += 1
|
|
258
|
+
end
|
|
259
|
+
i += 1
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
162
264
|
|
|
163
|
-
|
|
164
|
-
|
|
265
|
+
def find_conflicts_between_selection_groups(response_key, fields)
|
|
266
|
+
fields_by_selection = {}
|
|
267
|
+
fields.each do |field|
|
|
268
|
+
fields_by_selection[field_selection_signature(field)] ||= field
|
|
269
|
+
end
|
|
165
270
|
|
|
166
|
-
|
|
167
|
-
|
|
271
|
+
representatives = fields_by_selection.values
|
|
272
|
+
i = 0
|
|
273
|
+
while i < representatives.size
|
|
274
|
+
j = i + 1
|
|
275
|
+
while j < representatives.size
|
|
276
|
+
find_conflict(response_key, representatives[i], representatives[j])
|
|
277
|
+
j += 1
|
|
278
|
+
end
|
|
279
|
+
i += 1
|
|
280
|
+
end
|
|
281
|
+
end
|
|
168
282
|
|
|
169
|
-
|
|
283
|
+
def field_selection_signature(field)
|
|
284
|
+
node = field.node
|
|
285
|
+
@field_selection_signatures[node] ||= node.selections.map(&:to_query_string)
|
|
286
|
+
end
|
|
170
287
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
fields,
|
|
175
|
-
fragment_fields,
|
|
176
|
-
mutually_exclusive: mutually_exclusive,
|
|
177
|
-
)
|
|
288
|
+
def fields_same_signature?(f1, f2)
|
|
289
|
+
n1 = f1.node
|
|
290
|
+
n2 = f2.node
|
|
178
291
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
find_conflicts_between_fields_and_fragment(
|
|
183
|
-
fragment_spread,
|
|
184
|
-
fields,
|
|
185
|
-
mutually_exclusive: mutually_exclusive,
|
|
186
|
-
)
|
|
187
|
-
end
|
|
292
|
+
f1.definition.equal?(f2.definition) &&
|
|
293
|
+
n1.name == n2.name &&
|
|
294
|
+
same_arguments?(n1, n2)
|
|
188
295
|
end
|
|
189
296
|
|
|
190
|
-
def
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
j += 1
|
|
200
|
-
end
|
|
201
|
-
i += 1
|
|
202
|
-
end
|
|
297
|
+
def field_signature(field)
|
|
298
|
+
node = field.node
|
|
299
|
+
defn = field.definition
|
|
300
|
+
args = node.arguments
|
|
301
|
+
|
|
302
|
+
if args.empty?
|
|
303
|
+
[node.name, defn.object_id]
|
|
304
|
+
else
|
|
305
|
+
[node.name, defn.object_id, args.map { |a| [a.name, serialize_arg(a.value)] }]
|
|
203
306
|
end
|
|
204
307
|
end
|
|
205
308
|
|
|
206
309
|
def find_conflict(response_key, field1, field2, mutually_exclusive: false)
|
|
207
|
-
return if @conflict_count >=
|
|
310
|
+
return if @conflict_count >= @max_errors
|
|
208
311
|
return if field1.definition.nil? || field2.definition.nil?
|
|
209
312
|
|
|
210
313
|
node1 = field1.node
|
|
211
314
|
node2 = field2.node
|
|
212
315
|
|
|
213
|
-
are_mutually_exclusive = mutually_exclusive ||
|
|
214
|
-
mutually_exclusive?(field1.parents, field2.parents)
|
|
316
|
+
are_mutually_exclusive = mutually_exclusive || mutually_exclusive?(field1.parents, field2.parents)
|
|
215
317
|
|
|
216
318
|
if !are_mutually_exclusive
|
|
217
319
|
if node1.name != node2.name
|
|
@@ -234,17 +336,20 @@ module GraphQL
|
|
|
234
336
|
end
|
|
235
337
|
|
|
236
338
|
if !conflicts[:field].key?(response_key) &&
|
|
237
|
-
|
|
238
|
-
(
|
|
339
|
+
!field1.definition.equal?(field2.definition) &&
|
|
340
|
+
(t1 = field1.return_type) &&
|
|
341
|
+
(t2 = field2.return_type) &&
|
|
239
342
|
return_types_conflict?(t1, t2)
|
|
240
343
|
|
|
241
344
|
return_error = nil
|
|
242
345
|
message_override = nil
|
|
346
|
+
|
|
243
347
|
case @schema.allow_legacy_invalid_return_type_conflicts
|
|
244
348
|
when false
|
|
245
349
|
return_error = true
|
|
246
350
|
when true
|
|
247
351
|
legacy_handling = @schema.legacy_invalid_return_type_conflicts(@context.query, t1, t2, node1, node2)
|
|
352
|
+
|
|
248
353
|
case legacy_handling
|
|
249
354
|
when nil
|
|
250
355
|
return_error = false
|
|
@@ -267,9 +372,11 @@ module GraphQL
|
|
|
267
372
|
|
|
268
373
|
if return_error
|
|
269
374
|
conflict = conflicts[:return_type][response_key]
|
|
375
|
+
|
|
270
376
|
if message_override
|
|
271
377
|
conflict.message = message_override
|
|
272
378
|
end
|
|
379
|
+
|
|
273
380
|
conflict.add_conflict(node1, "`#{t1.to_type_signature}`")
|
|
274
381
|
conflict.add_conflict(node2, "`#{t2.to_type_signature}`")
|
|
275
382
|
@conflict_count += 1
|
|
@@ -303,121 +410,109 @@ module GraphQL
|
|
|
303
410
|
elsif type1.kind.leaf? && type2.kind.leaf?
|
|
304
411
|
type1 != type2
|
|
305
412
|
else
|
|
306
|
-
# One or more of these are composite types,
|
|
307
|
-
# their selections will be validated later on.
|
|
308
413
|
false
|
|
309
414
|
end
|
|
310
415
|
end
|
|
311
416
|
|
|
417
|
+
# When two fields with the same response key both have sub-selections,
|
|
418
|
+
# we need to check those sub-selections against each other.
|
|
312
419
|
def find_conflicts_between_sub_selection_sets(field1, field2, mutually_exclusive:)
|
|
313
420
|
return if field1.definition.nil? ||
|
|
314
421
|
field2.definition.nil? ||
|
|
315
422
|
(field1.node.selections.empty? && field2.node.selections.empty?)
|
|
316
423
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
parents1 = [return_type1]
|
|
320
|
-
parents2 = [return_type2]
|
|
321
|
-
|
|
322
|
-
fields, fragment_spreads = fields_and_fragments_from_selection(
|
|
323
|
-
field1.node,
|
|
324
|
-
owner_type: return_type1,
|
|
325
|
-
parents: parents1
|
|
326
|
-
)
|
|
424
|
+
node1 = field1.node
|
|
425
|
+
node2 = field2.node
|
|
327
426
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
owner_type: return_type2,
|
|
331
|
-
parents: parents2
|
|
332
|
-
)
|
|
427
|
+
# Prevent infinite recursion from cyclic fragments
|
|
428
|
+
return if node1.equal?(node2)
|
|
333
429
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
fields,
|
|
343
|
-
mutually_exclusive: mutually_exclusive,
|
|
344
|
-
)
|
|
430
|
+
inner = @compared_sub_selections[node1]
|
|
431
|
+
if inner
|
|
432
|
+
return if inner.key?(node2)
|
|
433
|
+
inner[node2] = true
|
|
434
|
+
else
|
|
435
|
+
inner = {}.compare_by_identity
|
|
436
|
+
inner[node2] = true
|
|
437
|
+
@compared_sub_selections[node1] = inner
|
|
345
438
|
end
|
|
346
439
|
|
|
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
|
|
440
|
+
return_type1 = field1.unwrapped_return_type
|
|
441
|
+
return_type2 = field2.unwrapped_return_type
|
|
356
442
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
443
|
+
response_keys1 = cached_sub_fields(node1, return_type1)
|
|
444
|
+
response_keys2 = cached_sub_fields(node2, return_type2)
|
|
445
|
+
|
|
446
|
+
find_conflicts_between(response_keys1, response_keys2, mutually_exclusive: mutually_exclusive)
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def cached_sub_fields(node, return_type)
|
|
450
|
+
inner = @sub_fields_cache[node]
|
|
451
|
+
|
|
452
|
+
if inner && inner.key?(return_type)
|
|
453
|
+
inner[return_type]
|
|
454
|
+
else
|
|
455
|
+
result = collect_fields(node.selections, owner_type: return_type, parents: [return_type])
|
|
456
|
+
inner ||= {}.compare_by_identity
|
|
457
|
+
inner[return_type] = result
|
|
458
|
+
@sub_fields_cache[node] = inner
|
|
459
|
+
result
|
|
368
460
|
end
|
|
369
461
|
end
|
|
370
462
|
|
|
371
463
|
def find_conflicts_between(response_keys, response_keys2, mutually_exclusive:)
|
|
372
464
|
response_keys.each do |key, fields|
|
|
373
465
|
fields2 = response_keys2[key]
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
466
|
+
next unless fields2
|
|
467
|
+
next if field_groups_already_compared?(fields, fields2, mutually_exclusive)
|
|
468
|
+
|
|
469
|
+
fields_arr = fields.is_a?(Field) ? [fields] : fields
|
|
470
|
+
fields2_arr = fields2.is_a?(Field) ? [fields2] : fields2
|
|
471
|
+
|
|
472
|
+
fields_arr.each do |field|
|
|
473
|
+
fields2_arr.each do |field2|
|
|
474
|
+
find_conflict(
|
|
475
|
+
key,
|
|
476
|
+
field,
|
|
477
|
+
field2,
|
|
478
|
+
mutually_exclusive: mutually_exclusive,
|
|
479
|
+
)
|
|
384
480
|
end
|
|
385
481
|
end
|
|
386
482
|
end
|
|
387
483
|
end
|
|
388
484
|
|
|
389
|
-
|
|
485
|
+
def field_groups_already_compared?(fields, fields2, mutually_exclusive)
|
|
486
|
+
signature1 = field_group_signature(fields)
|
|
487
|
+
signature2 = field_group_signature(fields2)
|
|
488
|
+
previous_comparisons = @compared_field_groups[signature1]
|
|
489
|
+
comparison_state = previous_comparisons && previous_comparisons[signature2]
|
|
390
490
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
491
|
+
if mutually_exclusive
|
|
492
|
+
return true if comparison_state
|
|
493
|
+
new_state = EXCLUSIVE_COMPARISON
|
|
394
494
|
else
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
response_keys = fields.group_by { |f| f.node.alias || f.node.name }
|
|
398
|
-
[response_keys, fragment_spreads]
|
|
495
|
+
return true if comparison_state == NONEXCLUSIVE_COMPARISON
|
|
496
|
+
new_state = NONEXCLUSIVE_COMPARISON
|
|
399
497
|
end
|
|
498
|
+
|
|
499
|
+
previous_comparisons ||= (@compared_field_groups[signature1] = {})
|
|
500
|
+
previous_comparisons[signature2] = new_state
|
|
501
|
+
|
|
502
|
+
reverse_comparisons = @compared_field_groups[signature2] ||= {}
|
|
503
|
+
reverse_comparisons[signature1] = new_state
|
|
504
|
+
false
|
|
400
505
|
end
|
|
401
506
|
|
|
402
|
-
def
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
fields << Field.new(node, definition, owner_type, parents)
|
|
408
|
-
when GraphQL::Language::Nodes::InlineFragment
|
|
409
|
-
fragment_type = node.type ? @types.type(node.type.name) : owner_type
|
|
410
|
-
find_fields_and_fragments(node.selections, parents: [*parents, fragment_type], owner_type: fragment_type, fields: fields, fragment_spreads: fragment_spreads) if fragment_type
|
|
411
|
-
when GraphQL::Language::Nodes::FragmentSpread
|
|
412
|
-
fragment_spreads << FragmentSpread.new(node.name, parents)
|
|
413
|
-
end
|
|
507
|
+
def field_group_signature(fields)
|
|
508
|
+
if fields.is_a?(Field)
|
|
509
|
+
fields.comparison_key
|
|
510
|
+
else
|
|
511
|
+
@field_group_signatures[fields] ||= fields.map(&:comparison_key)
|
|
414
512
|
end
|
|
415
|
-
|
|
416
|
-
[fields, fragment_spreads]
|
|
417
513
|
end
|
|
418
514
|
|
|
419
515
|
def same_arguments?(field1, field2)
|
|
420
|
-
# Check for incompatible / non-identical arguments on this node:
|
|
421
516
|
arguments1 = field1.arguments
|
|
422
517
|
arguments2 = field2.arguments
|
|
423
518
|
|
|
@@ -450,39 +545,47 @@ module GraphQL
|
|
|
450
545
|
serialized_args
|
|
451
546
|
end
|
|
452
547
|
|
|
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
548
|
# 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
549
|
def mutually_exclusive?(parents1, parents2)
|
|
465
550
|
if parents1.empty? || parents2.empty?
|
|
466
551
|
false
|
|
467
552
|
elsif parents1.length == parents2.length
|
|
468
|
-
|
|
553
|
+
i = 0
|
|
554
|
+
len = parents1.length
|
|
555
|
+
|
|
556
|
+
while i < len
|
|
469
557
|
type1 = parents1[i - 1]
|
|
470
558
|
type2 = parents2[i - 1]
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
559
|
+
unless type1.equal?(type2)
|
|
560
|
+
inner = @mutually_exclusive_cache[type1]
|
|
561
|
+
if inner
|
|
562
|
+
cached = inner[type2]
|
|
563
|
+
if cached.nil?
|
|
564
|
+
cached = types_mutually_exclusive?(type1, type2)
|
|
565
|
+
inner[type2] = cached
|
|
566
|
+
end
|
|
567
|
+
else
|
|
568
|
+
cached = types_mutually_exclusive?(type1, type2)
|
|
569
|
+
inner = {}.compare_by_identity
|
|
570
|
+
inner[type2] = cached
|
|
571
|
+
@mutually_exclusive_cache[type1] = inner
|
|
572
|
+
end
|
|
573
|
+
return true if cached
|
|
480
574
|
end
|
|
575
|
+
i += 1
|
|
481
576
|
end
|
|
577
|
+
|
|
578
|
+
false
|
|
482
579
|
else
|
|
483
580
|
true
|
|
484
581
|
end
|
|
485
582
|
end
|
|
583
|
+
|
|
584
|
+
def types_mutually_exclusive?(type1, type2)
|
|
585
|
+
possible_right_types = @types.possible_types(type1)
|
|
586
|
+
possible_left_types = @types.possible_types(type2)
|
|
587
|
+
(possible_right_types & possible_left_types).empty?
|
|
588
|
+
end
|
|
486
589
|
end
|
|
487
590
|
end
|
|
488
591
|
end
|