graphql 1.10.13 → 1.11.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/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
|