graphql 1.10.13 → 1.11.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/generators/graphql/templates/graphql_controller.erb +11 -9
- data/lib/graphql.rb +3 -3
- data/lib/graphql/directive.rb +4 -0
- data/lib/graphql/execution/interpreter.rb +1 -1
- data/lib/graphql/execution/interpreter/runtime.rb +6 -4
- data/lib/graphql/execution/multiplex.rb +1 -2
- data/lib/graphql/field.rb +4 -0
- data/lib/graphql/input_object_type.rb +4 -0
- data/lib/graphql/introspection/schema_type.rb +3 -3
- data/lib/graphql/invalid_null_error.rb +18 -0
- data/lib/graphql/language/nodes.rb +1 -0
- data/lib/graphql/language/visitor.rb +2 -2
- data/lib/graphql/pagination/connection.rb +18 -13
- data/lib/graphql/pagination/connections.rb +17 -4
- data/lib/graphql/query.rb +1 -2
- data/lib/graphql/schema.rb +22 -16
- data/lib/graphql/schema/build_from_definition.rb +7 -12
- data/lib/graphql/schema/build_from_definition/resolve_map.rb +3 -1
- data/lib/graphql/schema/enum_value.rb +1 -0
- data/lib/graphql/schema/field.rb +63 -77
- data/lib/graphql/schema/field/connection_extension.rb +42 -32
- data/lib/graphql/schema/loader.rb +19 -1
- data/lib/graphql/schema/member/has_arguments.rb +3 -1
- data/lib/graphql/schema/member/has_fields.rb +15 -5
- data/lib/graphql/schema/mutation.rb +4 -0
- data/lib/graphql/schema/object.rb +1 -1
- data/lib/graphql/schema/resolver.rb +20 -0
- data/lib/graphql/schema/resolver/has_payload_type.rb +2 -1
- data/lib/graphql/schema/subscription.rb +1 -1
- data/lib/graphql/schema/union.rb +29 -0
- data/lib/graphql/schema/warden.rb +0 -1
- data/lib/graphql/static_validation/literal_validator.rb +7 -7
- data/lib/graphql/static_validation/rules/arguments_are_defined.rb +1 -1
- data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +2 -2
- data/lib/graphql/static_validation/rules/required_input_object_attributes_are_present.rb +1 -2
- data/lib/graphql/static_validation/rules/variables_are_used_and_defined.rb +4 -2
- data/lib/graphql/subscriptions.rb +41 -8
- data/lib/graphql/subscriptions/action_cable_subscriptions.rb +66 -11
- data/lib/graphql/subscriptions/broadcast_analyzer.rb +84 -0
- data/lib/graphql/subscriptions/default_subscription_resolve_extension.rb +21 -0
- data/lib/graphql/subscriptions/event.rb +16 -1
- data/lib/graphql/subscriptions/serialize.rb +22 -4
- data/lib/graphql/subscriptions/subscription_root.rb +3 -1
- data/lib/graphql/tracing.rb +1 -27
- data/lib/graphql/tracing/platform_tracing.rb +25 -15
- data/lib/graphql/tracing/statsd_tracing.rb +14 -14
- data/lib/graphql/version.rb +1 -1
- metadata +4 -2
@@ -58,7 +58,8 @@ module GraphQL
|
|
58
58
|
resolver_fields.each do |name, f|
|
59
59
|
# Reattach the already-defined field here
|
60
60
|
# (The field's `.owner` will still point to the mutation, not the object type, I think)
|
61
|
-
|
61
|
+
# Don't re-warn about a method conflict. Since this type is generated, it should be fixed in the resolver instead.
|
62
|
+
add_field(f, method_conflict_warning: false)
|
62
63
|
end
|
63
64
|
end
|
64
65
|
end
|
@@ -93,11 +93,11 @@ module GraphQL
|
|
93
93
|
raise UnsubscribedError
|
94
94
|
end
|
95
95
|
|
96
|
+
READING_SCOPE = ::Object.new
|
96
97
|
# Call this method to provide a new subscription_scope; OR
|
97
98
|
# call it without an argument to get the subscription_scope
|
98
99
|
# @param new_scope [Symbol]
|
99
100
|
# @return [Symbol]
|
100
|
-
READING_SCOPE = ::Object.new
|
101
101
|
def self.subscription_scope(new_scope = READING_SCOPE)
|
102
102
|
if new_scope != READING_SCOPE
|
103
103
|
@subscription_scope = new_scope
|
data/lib/graphql/schema/union.rb
CHANGED
@@ -14,6 +14,7 @@ module GraphQL
|
|
14
14
|
def possible_types(*types, context: GraphQL::Query::NullContext, **options)
|
15
15
|
if types.any?
|
16
16
|
types.each do |t|
|
17
|
+
assert_valid_union_member(t)
|
17
18
|
type_memberships << type_membership_class.new(self, t, **options)
|
18
19
|
end
|
19
20
|
else
|
@@ -55,6 +56,34 @@ module GraphQL
|
|
55
56
|
def type_memberships
|
56
57
|
@type_memberships ||= []
|
57
58
|
end
|
59
|
+
|
60
|
+
# Update a type membership whose `.object_type` is a string or late-bound type
|
61
|
+
# so that the type membership's `.object_type` is the given `object_type`.
|
62
|
+
# (This is used for updating the union after the schema as lazily loaded the union member.)
|
63
|
+
# @api private
|
64
|
+
def assign_type_membership_object_type(object_type)
|
65
|
+
assert_valid_union_member(object_type)
|
66
|
+
type_memberships.each { |tm|
|
67
|
+
possible_type = tm.object_type
|
68
|
+
if possible_type.is_a?(String) && (possible_type == object_type.name)
|
69
|
+
# This is a match of Ruby class names, not graphql names,
|
70
|
+
# since strings are used to refer to constants.
|
71
|
+
tm.object_type = object_type
|
72
|
+
elsif possible_type.is_a?(LateBoundType) && possible_type.graphql_name == object_type.graphql_name
|
73
|
+
tm.object_type = object_type
|
74
|
+
end
|
75
|
+
}
|
76
|
+
nil
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def assert_valid_union_member(type_defn)
|
82
|
+
if type_defn.is_a?(Module) && !type_defn.is_a?(Class)
|
83
|
+
# it's an interface type, defined as a module
|
84
|
+
raise ArgumentError, "Union possible_types can only be object types (not interface types), remove #{type_defn.graphql_name} (#{type_defn.inspect})"
|
85
|
+
end
|
86
|
+
end
|
58
87
|
end
|
59
88
|
end
|
60
89
|
end
|
@@ -91,7 +91,6 @@ module GraphQL
|
|
91
91
|
|
92
92
|
# @return [GraphQL::Field, nil] The field named `field_name` on `parent_type`, if it exists
|
93
93
|
def get_field(parent_type, field_name)
|
94
|
-
|
95
94
|
@visible_parent_fields ||= read_through do |type|
|
96
95
|
read_through do |f_name|
|
97
96
|
field_defn = @schema.get_field(type, f_name)
|
@@ -95,16 +95,17 @@ module GraphQL
|
|
95
95
|
def required_input_fields_are_present(type, ast_node)
|
96
96
|
# TODO - would be nice to use these to create an error message so the caller knows
|
97
97
|
# that required fields are missing
|
98
|
-
required_field_names =
|
99
|
-
.select { |
|
98
|
+
required_field_names = type.arguments.each_value
|
99
|
+
.select { |argument| argument.type.kind.non_null? && @warden.get_argument(type, argument.name) }
|
100
100
|
.map(&:name)
|
101
|
+
|
101
102
|
present_field_names = ast_node.arguments.map(&:name)
|
102
103
|
missing_required_field_names = required_field_names - present_field_names
|
103
104
|
if @context.schema.error_bubbling
|
104
105
|
missing_required_field_names.empty? ? @valid_response : @invalid_response
|
105
106
|
else
|
106
107
|
results = missing_required_field_names.map do |name|
|
107
|
-
arg_type = @warden.
|
108
|
+
arg_type = @warden.get_argument(type, name).type
|
108
109
|
recursively_validate(GraphQL::Language::Nodes::NullValue.new(name: name), arg_type)
|
109
110
|
end
|
110
111
|
merge_results(results)
|
@@ -112,13 +113,12 @@ module GraphQL
|
|
112
113
|
end
|
113
114
|
|
114
115
|
def present_input_field_values_are_valid(type, ast_node)
|
115
|
-
field_map = @warden.arguments(type).reduce({}) { |m, f| m[f.name] = f; m}
|
116
116
|
results = ast_node.arguments.map do |value|
|
117
|
-
field =
|
117
|
+
field = @warden.get_argument(type, value.name)
|
118
118
|
# we want to call validate on an argument even if it's an invalid one
|
119
119
|
# so that our raise exception is on it instead of the entire InputObject
|
120
|
-
|
121
|
-
recursively_validate(value.value,
|
120
|
+
field_type = field && field.type
|
121
|
+
recursively_validate(value.value, field_type)
|
122
122
|
end
|
123
123
|
merge_results(results)
|
124
124
|
end
|
@@ -5,7 +5,7 @@ module GraphQL
|
|
5
5
|
def on_argument(node, parent)
|
6
6
|
parent_defn = parent_definition(parent)
|
7
7
|
|
8
|
-
if parent_defn && context.warden.
|
8
|
+
if parent_defn && context.warden.get_argument(parent_defn, node.name)
|
9
9
|
super
|
10
10
|
elsif parent_defn
|
11
11
|
kind_of_node = node_type(parent)
|
@@ -17,8 +17,8 @@ module GraphQL
|
|
17
17
|
|
18
18
|
def assert_required_args(ast_node, defn)
|
19
19
|
present_argument_names = ast_node.arguments.map(&:name)
|
20
|
-
required_argument_names =
|
21
|
-
.select { |a| a.type.kind.non_null? && !a.default_value? }
|
20
|
+
required_argument_names = defn.arguments.each_value
|
21
|
+
.select { |a| a.type.kind.non_null? && !a.default_value? && context.warden.get_argument(defn, a.name) }
|
22
22
|
.map(&:name)
|
23
23
|
|
24
24
|
missing_names = required_argument_names - present_argument_names
|
@@ -26,8 +26,7 @@ module GraphQL
|
|
26
26
|
context.field_definition
|
27
27
|
end
|
28
28
|
|
29
|
-
parent_type = context.warden.
|
30
|
-
.find{|f| f.name == parent_name(parent, defn) }
|
29
|
+
parent_type = context.warden.get_argument(defn, parent_name(parent, defn))
|
31
30
|
parent_type ? parent_type.type.unwrap : nil
|
32
31
|
end
|
33
32
|
|
@@ -126,8 +126,9 @@ module GraphQL
|
|
126
126
|
node_variables
|
127
127
|
.select { |name, usage| usage.declared? && !usage.used? }
|
128
128
|
.each { |var_name, usage|
|
129
|
+
declared_by_error_name = usage.declared_by.name || "anonymous #{usage.declared_by.operation_type}"
|
129
130
|
add_error(GraphQL::StaticValidation::VariablesAreUsedAndDefinedError.new(
|
130
|
-
"Variable $#{var_name} is declared by #{
|
131
|
+
"Variable $#{var_name} is declared by #{declared_by_error_name} but not used",
|
131
132
|
nodes: usage.declared_by,
|
132
133
|
path: usage.path,
|
133
134
|
name: var_name,
|
@@ -139,8 +140,9 @@ module GraphQL
|
|
139
140
|
node_variables
|
140
141
|
.select { |name, usage| usage.used? && !usage.declared? }
|
141
142
|
.each { |var_name, usage|
|
143
|
+
used_by_error_name = usage.used_by.name || "anonymous #{usage.used_by.operation_type}"
|
142
144
|
add_error(GraphQL::StaticValidation::VariablesAreUsedAndDefinedError.new(
|
143
|
-
"Variable $#{var_name} is used by #{
|
145
|
+
"Variable $#{var_name} is used by #{used_by_error_name} but not declared",
|
144
146
|
nodes: usage.ast_node,
|
145
147
|
path: usage.path,
|
146
148
|
name: var_name,
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require "securerandom"
|
3
|
+
require "graphql/subscriptions/broadcast_analyzer"
|
3
4
|
require "graphql/subscriptions/event"
|
4
5
|
require "graphql/subscriptions/instrumentation"
|
5
6
|
require "graphql/subscriptions/serialize"
|
@@ -7,6 +8,7 @@ if defined?(ActionCable)
|
|
7
8
|
require "graphql/subscriptions/action_cable_subscriptions"
|
8
9
|
end
|
9
10
|
require "graphql/subscriptions/subscription_root"
|
11
|
+
require "graphql/subscriptions/default_subscription_resolve_extension"
|
10
12
|
|
11
13
|
module GraphQL
|
12
14
|
class Subscriptions
|
@@ -29,14 +31,25 @@ module GraphQL
|
|
29
31
|
defn.instrument(:field, instrumentation)
|
30
32
|
options[:schema] = schema
|
31
33
|
schema.subscriptions = self.new(**options)
|
34
|
+
schema.add_subscription_extension_if_necessary
|
32
35
|
nil
|
33
36
|
end
|
34
37
|
|
35
38
|
# @param schema [Class] the GraphQL schema this manager belongs to
|
36
|
-
def initialize(schema:, **rest)
|
39
|
+
def initialize(schema:, broadcast: false, default_broadcastable: false, **rest)
|
40
|
+
if broadcast
|
41
|
+
if !schema.using_ast_analysis?
|
42
|
+
raise ArgumentError, "`broadcast: true` requires AST analysis, add `using GraphQL::Analysis::AST` to your schema or see https://graphql-ruby.org/queries/ast_analysis.html."
|
43
|
+
end
|
44
|
+
schema.query_analyzer(Subscriptions::BroadcastAnalyzer)
|
45
|
+
end
|
46
|
+
@default_broadcastable = default_broadcastable
|
37
47
|
@schema = schema
|
38
48
|
end
|
39
49
|
|
50
|
+
# @return [Boolean] Used when fields don't have `broadcastable:` explicitly set
|
51
|
+
attr_reader :default_broadcastable
|
52
|
+
|
40
53
|
# Fetch subscriptions matching this field + arguments pair
|
41
54
|
# And pass them off to the queue.
|
42
55
|
# @param event_name [String]
|
@@ -77,15 +90,13 @@ module GraphQL
|
|
77
90
|
# `event` was triggered on `object`, and `subscription_id` was subscribed,
|
78
91
|
# so it should be updated.
|
79
92
|
#
|
80
|
-
# Load `subscription_id`'s GraphQL data, re-evaluate the query
|
81
|
-
#
|
82
|
-
# This is where a queue may be inserted to push updates in the background.
|
93
|
+
# Load `subscription_id`'s GraphQL data, re-evaluate the query and return the result.
|
83
94
|
#
|
84
95
|
# @param subscription_id [String]
|
85
96
|
# @param event [GraphQL::Subscriptions::Event] The event which was triggered
|
86
97
|
# @param object [Object] The value for the subscription field
|
87
|
-
# @return [
|
88
|
-
def
|
98
|
+
# @return [GraphQL::Query::Result]
|
99
|
+
def execute_update(subscription_id, event, object)
|
89
100
|
# Lookup the saved data for this subscription
|
90
101
|
query_data = read_subscription(subscription_id)
|
91
102
|
if query_data.nil?
|
@@ -98,7 +109,7 @@ module GraphQL
|
|
98
109
|
context = query_data.fetch(:context)
|
99
110
|
operation_name = query_data.fetch(:operation_name)
|
100
111
|
# Re-evaluate the saved query
|
101
|
-
|
112
|
+
@schema.execute(
|
102
113
|
query: query_string,
|
103
114
|
context: context,
|
104
115
|
subscription_topic: event.topic,
|
@@ -106,13 +117,25 @@ module GraphQL
|
|
106
117
|
variables: variables,
|
107
118
|
root_value: object,
|
108
119
|
)
|
109
|
-
deliver(subscription_id, result)
|
110
120
|
rescue GraphQL::Schema::Subscription::NoUpdateError
|
111
121
|
# This update was skipped in user code; do nothing.
|
122
|
+
nil
|
112
123
|
rescue GraphQL::Schema::Subscription::UnsubscribedError
|
113
124
|
# `unsubscribe` was called, clean up on our side
|
114
125
|
# TODO also send `{more: false}` to client?
|
115
126
|
delete_subscription(subscription_id)
|
127
|
+
nil
|
128
|
+
end
|
129
|
+
|
130
|
+
# Run the update query for this subscription and deliver it
|
131
|
+
# @see {#execute_update}
|
132
|
+
# @see {#deliver}
|
133
|
+
# @return [void]
|
134
|
+
def execute(subscription_id, event, object)
|
135
|
+
res = execute_update(subscription_id, event, object)
|
136
|
+
if !res.nil?
|
137
|
+
deliver(subscription_id, res)
|
138
|
+
end
|
116
139
|
end
|
117
140
|
|
118
141
|
# Event `event` occurred on `object`,
|
@@ -185,6 +208,16 @@ module GraphQL
|
|
185
208
|
Schema::Member::BuildType.camelize(event_or_arg_name.to_s)
|
186
209
|
end
|
187
210
|
|
211
|
+
# @return [Boolean] if true, then a query like this one would be broadcasted
|
212
|
+
def broadcastable?(query_str, **query_options)
|
213
|
+
query = GraphQL::Query.new(@schema, query_str, **query_options)
|
214
|
+
if !query.valid?
|
215
|
+
raise "Invalid query: #{query.validation_errors.map(&:to_h).inspect}"
|
216
|
+
end
|
217
|
+
GraphQL::Analysis::AST.analyze_query(query, @schema.query_analyzers)
|
218
|
+
query.context.namespace(:subscriptions)[:subscription_broadcastable]
|
219
|
+
end
|
220
|
+
|
188
221
|
private
|
189
222
|
|
190
223
|
# Recursively normalize `args` as belonging to `arg_owner`:
|
@@ -8,7 +8,7 @@ module GraphQL
|
|
8
8
|
#
|
9
9
|
# - No queueing system; ActiveJob should be added
|
10
10
|
# - Take care to reload context when re-delivering the subscription. (see {Query#subscription_update?})
|
11
|
-
# - Avoid the async ActionCable adapter and use the redis or PostgreSQL adapters instead. Otherwise calling #trigger won't work from background jobs or the Rails console.
|
11
|
+
# - Avoid the async ActionCable adapter and use the redis or PostgreSQL adapters instead. Otherwise calling #trigger won't work from background jobs or the Rails console.
|
12
12
|
#
|
13
13
|
# @example Adding ActionCableSubscriptions to your schema
|
14
14
|
# class MySchema < GraphQL::Schema
|
@@ -90,6 +90,7 @@ module GraphQL
|
|
90
90
|
# A per-process map of subscriptions to deliver.
|
91
91
|
# This is provided by Rails, so let's use it
|
92
92
|
@subscriptions = Concurrent::Map.new
|
93
|
+
@events = Concurrent::Map.new { |h, k| h[k] = Concurrent::Map.new { |h2, k2| h2[k2] = Concurrent::Array.new } }
|
93
94
|
@serializer = serializer
|
94
95
|
super
|
95
96
|
end
|
@@ -120,27 +121,81 @@ module GraphQL
|
|
120
121
|
channel.stream_from(stream)
|
121
122
|
@subscriptions[subscription_id] = query
|
122
123
|
events.each do |event|
|
123
|
-
|
124
|
-
|
125
|
-
|
124
|
+
# Setup a new listener to run all events with this topic in this process
|
125
|
+
setup_stream(channel, event)
|
126
|
+
# Add this event to the list of events to be updated
|
127
|
+
@events[event.topic][event.fingerprint] << event
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Every subscribing channel is listening here, but only one of them takes any action.
|
132
|
+
# This is so we can reuse payloads when possible, and make one payload to send to
|
133
|
+
# all subscribers.
|
134
|
+
#
|
135
|
+
# But the problem is, any channel could close at any time, so each channel has to
|
136
|
+
# be ready to take over the primary position.
|
137
|
+
#
|
138
|
+
# To make sure there's always one-and-only-one channel building payloads,
|
139
|
+
# let the listener belonging to the first event on the list be
|
140
|
+
# the one to build and publish payloads.
|
141
|
+
#
|
142
|
+
def setup_stream(channel, initial_event)
|
143
|
+
topic = initial_event.topic
|
144
|
+
channel.stream_from(EVENT_PREFIX + topic, coder: ActiveSupport::JSON) do |message|
|
145
|
+
object = @serializer.load(message)
|
146
|
+
events_by_fingerprint = @events[topic]
|
147
|
+
events_by_fingerprint.each do |_fingerprint, events|
|
148
|
+
if events.any? && events.first == initial_event
|
149
|
+
# The fingerprint has told us that this response should be shared by all subscribers,
|
150
|
+
# so just run it once, then deliver the result to every subscriber
|
151
|
+
first_event = events.first
|
152
|
+
first_subscription_id = first_event.context.fetch(:subscription_id)
|
153
|
+
result = execute_update(first_subscription_id, first_event, object)
|
154
|
+
# Having calculated the result _once_, send the same payload to all subscribers
|
155
|
+
events.each do |event|
|
156
|
+
subscription_id = event.context.fetch(:subscription_id)
|
157
|
+
deliver(subscription_id, result)
|
158
|
+
end
|
159
|
+
end
|
126
160
|
end
|
161
|
+
nil
|
127
162
|
end
|
128
163
|
end
|
129
164
|
|
130
165
|
# Return the query from "storage" (in memory)
|
131
166
|
def read_subscription(subscription_id)
|
132
167
|
query = @subscriptions[subscription_id]
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
168
|
+
if query.nil?
|
169
|
+
# This can happen when a subscription is triggered from an unsubscribed channel,
|
170
|
+
# see https://github.com/rmosolgo/graphql-ruby/issues/2478.
|
171
|
+
# (This `nil` is handled by `#execute_update`)
|
172
|
+
nil
|
173
|
+
else
|
174
|
+
{
|
175
|
+
query_string: query.query_string,
|
176
|
+
variables: query.provided_variables,
|
177
|
+
context: query.context.to_h,
|
178
|
+
operation_name: query.operation_name,
|
179
|
+
}
|
180
|
+
end
|
139
181
|
end
|
140
182
|
|
141
183
|
# The channel was closed, forget about it.
|
142
184
|
def delete_subscription(subscription_id)
|
143
|
-
@subscriptions.delete(subscription_id)
|
185
|
+
query = @subscriptions.delete(subscription_id)
|
186
|
+
# This can be `nil` when `.trigger` happens inside an unsubscribed ActionCable channel,
|
187
|
+
# see https://github.com/rmosolgo/graphql-ruby/issues/2478
|
188
|
+
if query
|
189
|
+
events = query.context.namespace(:subscriptions)[:events]
|
190
|
+
events.each do |event|
|
191
|
+
ev_by_fingerprint = @events[event.topic]
|
192
|
+
ev_for_fingerprint = ev_by_fingerprint[event.fingerprint]
|
193
|
+
ev_for_fingerprint.delete(event)
|
194
|
+
if ev_for_fingerprint.empty?
|
195
|
+
ev_by_fingerprint.delete(event.fingerprint)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
144
199
|
end
|
145
200
|
end
|
146
201
|
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
class Subscriptions
|
5
|
+
# Detect whether the current operation:
|
6
|
+
# - Is a subscription operation
|
7
|
+
# - Is completely broadcastable
|
8
|
+
#
|
9
|
+
# Assign the result to `context.namespace(:subscriptions)[:subscription_broadcastable]`
|
10
|
+
# @api private
|
11
|
+
# @see Subscriptions#broadcastable? for a public API
|
12
|
+
class BroadcastAnalyzer < GraphQL::Analysis::AST::Analyzer
|
13
|
+
def initialize(subject)
|
14
|
+
super
|
15
|
+
@default_broadcastable = subject.schema.subscriptions.default_broadcastable
|
16
|
+
# Maybe this will get set to false while analyzing
|
17
|
+
@subscription_broadcastable = true
|
18
|
+
end
|
19
|
+
|
20
|
+
# Only analyze subscription operations
|
21
|
+
def analyze?
|
22
|
+
@query.subscription?
|
23
|
+
end
|
24
|
+
|
25
|
+
def on_enter_field(node, parent, visitor)
|
26
|
+
if (@subscription_broadcastable == false) || visitor.skipping?
|
27
|
+
return
|
28
|
+
end
|
29
|
+
|
30
|
+
current_field = visitor.field_definition
|
31
|
+
apply_broadcastable(current_field)
|
32
|
+
|
33
|
+
current_type = visitor.parent_type_definition
|
34
|
+
if current_type.kind.interface?
|
35
|
+
pt = @query.possible_types(current_type)
|
36
|
+
pt.each do |object_type|
|
37
|
+
ot_field = @query.get_field(object_type, current_field.graphql_name)
|
38
|
+
if !ot_field
|
39
|
+
binding.pry
|
40
|
+
end
|
41
|
+
# Inherited fields would be exactly the same object;
|
42
|
+
# only check fields that are overrides of the inherited one
|
43
|
+
if ot_field && ot_field != current_field
|
44
|
+
apply_broadcastable(ot_field)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Assign the result to context.
|
51
|
+
# (This method is allowed to return an error, but we don't need to)
|
52
|
+
# @return [void]
|
53
|
+
def result
|
54
|
+
query.context.namespace(:subscriptions)[:subscription_broadcastable] = @subscription_broadcastable
|
55
|
+
nil
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
# Modify `@subscription_broadcastable` based on `field_defn`'s configuration (and/or the default value)
|
61
|
+
def apply_broadcastable(field_defn)
|
62
|
+
current_field_broadcastable = field_defn.introspection? || field_defn.broadcastable?
|
63
|
+
case current_field_broadcastable
|
64
|
+
when nil
|
65
|
+
# If the value wasn't set, mix in the default value:
|
66
|
+
# - If the default is false and the current value is true, make it false
|
67
|
+
# - If the default is true and the current value is true, it stays true
|
68
|
+
# - If the default is false and the current value is false, keep it false
|
69
|
+
# - If the default is true and the current value is false, keep it false
|
70
|
+
@subscription_broadcastable = @subscription_broadcastable && @default_broadcastable
|
71
|
+
when false
|
72
|
+
# One non-broadcastable field is enough to make the whole subscription non-broadcastable
|
73
|
+
@subscription_broadcastable = false
|
74
|
+
when true
|
75
|
+
# Leave `@broadcastable_query` true if it's already true,
|
76
|
+
# but don't _set_ it to true if it was set to false by something else.
|
77
|
+
# Actually, just leave it!
|
78
|
+
else
|
79
|
+
raise ArgumentError, "Unexpected `.broadcastable?` value for #{field_defn.path}: #{current_field_broadcastable}"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|