Skip to content

Commit 79b8c0b

Browse files
author
Robert Mosolgo
authored
Merge pull request #2986 from rmosolgo/1.11-dev
Put 1.11-dev on master
2 parents e291b70 + 59ab7c1 commit 79b8c0b

45 files changed

Lines changed: 1092 additions & 334 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

guides/queries/interpreter.md

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,6 @@ class MySchema < GraphQL::Schema
4242
end
4343
```
4444

45-
If you have a subscription root type, it will also need an update. Extend this new module:
46-
47-
```ruby
48-
class Types::Subscription < Types::BaseObject
49-
# Extend this module to support subscription root fields with Interpreter
50-
extend GraphQL::Subscriptions::SubscriptionRoot
51-
end
52-
```
53-
5445
Some Relay configurations must be updated too. For example:
5546

5647
```diff

guides/subscriptions/broadcast.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
---
2+
layout: guide
3+
doc_stub: false
4+
search: true
5+
section: Subscriptions
6+
title: Broadcasts
7+
desc: Delivering the same GraphQL result to multiple subscribers
8+
index: 3
9+
---
10+
11+
GraphQL-Ruby 1.11+ introduced a new algorithm for tracking subscriptions and delivering updates, _broadcasts_.
12+
13+
A broadcast is a subscription update which is executed _once_, then delivered to _any number_ of subscribers. This reduces the time your server spends running GraphQL queries, since it doesn't have to re-run the query for every subscriber.
14+
15+
But, __take care__: this approach risks leaking information to subscribers who shouldn't receive it.
16+
17+
## Setup
18+
19+
To enable broadcasts, add `broadcast: true` to your subscription setup:
20+
21+
```ruby
22+
class MyAppSchema < GraphQL::Schema
23+
# ...
24+
use GraphQL::Execution::Interpreter
25+
use GraphQL::Analysis::AST
26+
use SomeSubscriptionImplementation,
27+
broadcast: true # <----
28+
end
29+
```
30+
31+
Then, any broadcastable field can be configured with `broadcastable: true`:
32+
33+
```ruby
34+
field :name, String, null: false,
35+
broadcastable: true
36+
```
37+
38+
When a subscription comes in where _all_ of its fields are `broadcastable: true`, then it will be handled as a broadcast.
39+
40+
Additionally, you can set `default_broadcastable: true`:
41+
42+
```ruby
43+
class MyAppSchema < GraphQL::Schema
44+
# ...
45+
use GraphQL::Execution::Interpreter
46+
use GraphQL::Analysis::AST
47+
use SomeSubscriptionImplementation,
48+
broadcast: true,
49+
default_broadcastable: true # <----
50+
end
51+
```
52+
53+
With this setting, fields are broadcastable by default. Only a field with `broadcastable: false` in its configuration will cause a subscription to be handled on a subscriber-by-subscriber basis.
54+
55+
## What fields are broadcastable?
56+
57+
GraphQL-Ruby can't infer whether a field is broadcastable or not. You must configure it explicitly with `broadcastable: true` or `broadcastable: false`. (The subscription plugin also accepts `default_broadcastable: true|false`.)
58+
59+
A field is broadcastable if _all clients who request the field will see the same value_. For example:
60+
61+
- General facts: celebrity names, laws of physics, historical dates
62+
- Public information: object names, document updated-at timestamps, boilerplate info
63+
64+
For fields like this, you can add `broadcastable: true`.
65+
66+
A field is __not broadcastable__ if its value is different for different clients. For example:
67+
68+
- __Viewer-specific information:__ if a field is specifically viewer-based, then it can't be broadcasted to other viewers. For example, `discussion { viewerCanModerate }` might be true for a moderator, but it shouldn't be broadcasted to other viewers.
69+
- __Context-specific information:__ if a field's value takes the request context into consideration, it shouldn't be broadcasted. For example, IP addresses or HTTP header values probably can't be broadcasted. If a field reflects the viewer's timezone, it can't be broadcasted.
70+
- __Restricted information:__ if some viewers see one value, while other viewers see a different value, then it's not broadcastable. Broadcasting this data might leak private information to unauthorized clients. (This includes filtered lists: if the filtering is viewer-by-viewer, it's not broadcastable.)
71+
- __Fields with side effects:__ if the system requires a side effect (eg, logging a metric, updating a database, incrementing a counter) whenever a resolver is executed, it's not a good candidate for broadcasting because some executions will be optimized away.
72+
73+
These fields can be tagged with `broadcastable: false` so that GraphQL-Ruby will handle them on a subscriber-by-subscriber basis.
74+
75+
If you want to use subscriptions but have a lot of non-broadcastable fields in your schema, consider building a new set of subscription fields with limited access to other schema objects. Instead, optimize those subscriptions for broacastability.
76+
77+
## Under the Hood
78+
79+
GraphQL-Ruby determines which subscribers can receive a broadcast by inspecting:
80+
81+
- __Query string__. Only exactly-matching query strings will receive the same broadcast.
82+
- __Variables__. Only exactly-matching variable values will receive the same broadcast.
83+
- __Field and Arguments__ given to `.trigger`. They must match the ones initially sent when subscribing. (Subscriptions always worked this way.)
84+
- __Subscription scope__. Only clients with exactly-matching subscription scope can receive the same broadcasts.
85+
86+
So, take care to {% internal_link "set subscription_scope", "subscriptions/subscription_classes#scope" %} whenever a subscription should be implicitly scoped!
87+
88+
(See {{ "GraphQL::Subscriptions::Event#fingerprint" | api_doc }} for the implementation of broadcast fingerprints.)

guides/subscriptions/implementation.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ The {{ "GraphQL::Subscriptions" | api_doc }} plugin is a base class for implemen
1212

1313
Each method corresponds to a step in the subscription lifecycle. See the API docs for method-by-method documentation: {{ "GraphQL::Subscriptions" | api_doc }}.
1414

15-
Also, see the {% internal_link "Pusher implementation guide", "subscriptions/pusher_implementation" %}, the {% internal_link "ActionCable implementation guide", "subscriptions/action_cable_implementation" %} or {{ "GraphQL::Subscriptions::ActionCableSubscriptions" | api_doc }} docs for an example implementation.
15+
Also, see the {% internal_link "Pusher implementation guide", "subscriptions/pusher_implementation" %}, the {% internal_link "Ably implementation guide", "subscriptions/ably_implementation" %}, the {% internal_link "ActionCable implementation guide", "subscriptions/action_cable_implementation" %} or {{ "GraphQL::Subscriptions::ActionCableSubscriptions" | api_doc }} docs for an example implementation.
1616

1717
## Considerations
1818

@@ -22,3 +22,7 @@ Every Ruby application is different, so consider these points when implementing
2222
- What components of your application can be used for persistence and message passing?
2323
- How will you deliver push updates to subscribed clients? (For example, websockets, ActionCable, Pusher, webhooks, or something else?)
2424
- How will you handle [thundering herd](https://en.wikipedia.org/wiki/Thundering_herd_problem)s? When an event is triggered, how will you manage database access to update clients without swamping your system?
25+
26+
## Broadcasts
27+
28+
_Broadcasting_ updates to multiple subscribers is supported by GraphQL-Ruby, but requires implementation-specific work, see more in the {% internal_link "Broadcast guide", "subscriptions/broadcast" %}.

guides/subscriptions/overview.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,23 @@ index: 0
1111
_Subscriptions_ allow GraphQL clients to observe specific events and receive updates from the server when those events occur. This supports live updates, such as websocket pushes. Subscriptions introduce several new concepts:
1212

1313
- The __Subscription type__ is the entry point for subscription queries
14+
- __Subscription classes__ are resolvers for handing initial subscription requests and subsequent updates
1415
- __Triggers__ begin the update process
1516
- The __Implementation__ provides application-specific methods for executing & delivering updates.
17+
- __Broadcasts__ can send the same GraphQL result to any number of subscribers.
1618

1719
### Subscription Type
1820

1921
`subscription` is an entry point to your GraphQL schema, like `query` or `mutation`. It is defined by your `SubscriptionType`, a root-level `GraphQL::Schema::Object`.
2022

2123
Read more in the {% internal_link "Subscription Type guide", "subscriptions/subscription_type" %}.
2224

25+
### Subscription Classes
26+
27+
{{ "GraphQL::Schema::Subscription" | api_doc }} is a resolver class with subscription-specific behaviors. Each subscription field should be implemented by a subscription class.
28+
29+
Read more in the {% internal_link "Subscription Classes guide", "subscriptions/subscription_classes" %}
30+
2331
### Triggers
2432

2533
After an event occurs in our application, _triggers_ begin the update process by sending a name and payload to GraphQL.
@@ -34,4 +42,8 @@ Besides the GraphQL component, your application must provide some subscription-r
3442
- __transport__: How does your application deliver payloads to clients?
3543
- __queueing__: How does your application distribute the work of re-running subscription queries?
3644

37-
Read more in the {% internal_link "Implementation guide", "subscriptions/implementation" %} or check out the {% internal_link "ActionCable implementation", "subscriptions/action_cable_implementation" %} or {% internal_link "Pusher implementation", "subscriptions/pusher_implementation" %}.
45+
Read more in the {% internal_link "Implementation guide", "subscriptions/implementation" %} or check out the {% internal_link "ActionCable implementation", "subscriptions/action_cable_implementation" %}, {% internal_link "Pusher implementation", "subscriptions/pusher_implementation" %} or {% internal_link "Ably implementation", "subscriptions/ably_implementation" %}.
46+
47+
### Broadcasts
48+
49+
By default, the subscription implementations listed above handle each subscription in total isolation. However, this behavior can be optimized by setting up broadcasts. Read more in the {% internal_link "Broadcast guide", "subscriptions/broadcast" %}.

guides/subscriptions/subscription_classes.md

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,13 @@ You can extend {{ "GraphQL::Schema::Subscription" | api_doc }} to create fields
1212

1313
These classes support several behaviors:
1414

15-
- Authorizing (or rejecting) initial subscription requests and subsequent updates
16-
- Returning values for initial subscription requests
17-
- Unsubscribing from the server
18-
- Skipping updates for certain clients (eg, don't send updates to the person who triggered the event)
15+
- [Authorizing](#check-permissions-with-authorized) (or rejecting) initial subscription requests and subsequent updates
16+
- Returning values for [initial subscription requests](#initial-subscription-with-subscribe)
17+
- [Unsubscribing](#terminating-the-subscription-with-unsubscribe) from the server
18+
- Implicitly [scoping updates](#scope), to direct data to the right subscriber
19+
- [Skipping updates](#subsequent-updates-with-update) for certain clients (eg, don't send updates to the person who triggered the event)
20+
21+
Continue reading to set up subscription classes.
1922

2023
## Add a base class
2124

@@ -143,6 +146,53 @@ payload_type Types::MessageType
143146

144147
(In that case, don't return a hash from `#subscribe` or `#update`, return a `message` object instead.)
145148

149+
## Scope
150+
151+
Usually, GraphQL-Ruby uses explicitly-passed arguments to determine when a {% internal_link "trigger", "subscriptions/triggers" %} applies to an active subscription. But, you can use `subscription_scope` to configure _implicit_ conditions on updates. When `subscription_scope` is configured, only triggers with a matching `scope:` value will cause clients to receive updates.
152+
153+
`subscription_scope` accepts a symbol and the given symbol will be looked up in `context` to find a scope value.
154+
155+
For example, this subscription will use `context[:current_organization_id]` as a scope:
156+
157+
```ruby
158+
class Subscriptions::EmployeeHired < Subscriptions::BaseSubscription
159+
# ...
160+
subscription_scope :current_organization_id
161+
end
162+
```
163+
164+
Clients subscribe _without_ any arguments:
165+
166+
```graphql
167+
subscription {
168+
employeeHired {
169+
hireDate
170+
employee {
171+
name
172+
department
173+
}
174+
}
175+
}
176+
```
177+
178+
But `.trigger`s are routed using `scope:`. So, if the subscriber's context includes `current_organization_id: 100`, then the trigger must include the same `scope:` value:
179+
180+
```ruby
181+
MyAppSchema.subscriptions.trigger(
182+
# Field name
183+
:employee_hired,
184+
# Arguments
185+
{},
186+
# Object
187+
{ hire_date: Time.now, employee: new_employee },
188+
# This corresponds to `context[:current_organization_id]`
189+
# in the original subscription:
190+
scope: 100
191+
)
192+
```
193+
194+
Scope is also used for determining whether subscribers can receive the same {% internal_link "broadcast", "subscriptions/implementation#broadcast" %}.
195+
146196
## Check Permissions with #authorized?
147197

148198
Suppose a client is subscribing to messages in a chat room:

guides/subscriptions/subscription_type.md

Lines changed: 2 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,7 @@ class Types::SubscriptionType < GraphQL::Schema::Object
4343
# If you're using the interpreter, also add:
4444
extend GraphQL::Subscriptions::SubscriptionRoot
4545

46-
field :post_was_published, Types::PostType, null: false,
47-
description: "A post was published to the blog"
46+
field :post_was_published, subscription: Subscriptions::PostWasPublished
4847
# ...
4948
end
5049
```
@@ -63,37 +62,4 @@ end
6362

6463
See {% internal_link "Implementing Subscriptions","subscriptions/implementation" %} for more about actually delivering updates.
6564

66-
## Authorizing Subscriptions
67-
68-
When a client first sends a `subscription` operation, the root fields are resolved, so their corresponding methods are called, for example:
69-
70-
```ruby
71-
class Types::SubscriptionType < GraphQL::Schema::Object
72-
extend GraphQL::Subscriptions::SubscriptionRoot
73-
74-
field :post_was_published, Types::PostType, null: false,
75-
description: "A post was published to the blog" do
76-
argument :topic, Types::PostTopic, required: true
77-
end
78-
79-
def post_was_published(topic:)
80-
# This will be called on the initial request
81-
end
82-
end
83-
```
84-
85-
During that method, you can raise an error to _prevent_ establishing the subscription. For example:
86-
87-
```ruby
88-
def post_was_published(topic:)
89-
if context[:viewer].can_subscribe_to?(topic)
90-
# Allow the request
91-
else
92-
raise GraphQL::ExecutionError.new("Can't subscribe to this topic: #{topic}")
93-
end
94-
end
95-
```
96-
97-
If the error is raised, it will be added to the response's `"errors"` key and the subscription won't be created.
98-
99-
The return value of the method is not used; only the raised error affects the behavior of the subscription.
65+
See {% internal_link "Subscription Classes", "subscriptions/subscription_classes" %} for more about implementing subscription root fields.

guides/subscriptions/triggers.md

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ Events are triggered _by name_, and the name must match fields on your {% intern
1414

1515
```ruby
1616
# Update the system with the new blog post:
17-
MySchema.subscriptions.trigger("postAdded", {}, new_post)
17+
MySchema.subscriptions.trigger(:post_added, {}, new_post)
1818
```
1919

2020
The arguments are:
2121

2222
- `name`, which corresponds to the field on subscription type
23-
- `arguments`, which corresponds to the arguments on subscription type (for example, if you subscribe to comments on a certain post, the arguments would be `{postId: comment.post_id}`.)
23+
- `arguments`, which corresponds to the arguments on subscription type (for example, if you subscribe to comments on a certain post, the arguments would be `{post_id: comment.post_id}`.)
2424
- `object`, which will be the root object of the subscription update
2525
- `scope:` (shown below) for implicitly scoping the clients who will receive updates.
2626

@@ -30,17 +30,20 @@ To send updates to _certain clients only_, you can use `scope:` to narrow the tr
3030

3131
Scopes are based on query context: a value in `context:` is used as the scope; an equivalent value must be passed with `.trigger(... scope:)` to update that client. (The value is serialized with {{ "GraphQL::Subscriptions::Serialize" | api_doc }})
3232

33-
To specify that a topic is scoped, edit the field definition on your root `Subscription` type. Use the `subscription_scope:` option to name a `context:` key, for example:
33+
To specify that a topic is scoped, add a `subscription_scope` option to the Subscription class:
3434

3535
```ruby
36-
# For a given viewer, this will be triggered
37-
# whenever one of their posts gets a new comment
38-
field :comment_added, CommentType,
39-
null: false,
40-
description: "A comment was added to one of the viewer's posts"
41-
subscription_scope: :current_user_id
36+
class Subscriptions::CommentAdded < Subscription::BaseSubscription
37+
description "A comment was added to one of the viewer's posts"
38+
# For a given viewer, this will be triggered
39+
# whenever one of their posts gets a new comment
40+
subscription_scope :current_user_id
41+
# ...
42+
end
4243
```
4344

45+
(Read more in the {% internal_link "Subscription Classes guide", "subscriptions/subscription_classes#scope" %}.)
46+
4447
Then, subscription operations should have a `context: { current_user_id: ... }` value, for example:
4548

4649
```ruby
@@ -55,7 +58,7 @@ Finally, when events happen in your app, you should provide the scoping value as
5558
comment = post.comments.create!(attrs)
5659
# notify the author
5760
author_id = post.author.id
58-
MySchema.subscriptions.trigger("commentAdded", {}, comment, scope: author_id)
61+
MySchema.subscriptions.trigger(:comment_added, {}, comment, scope: author_id)
5962
```
6063

6164
Since this trigger has a `scope:`, only subscribers with a matching scope value will be updated.

lib/graphql/execution/interpreter.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def self.use(schema_class)
2626
schema_class.query_execution_strategy(GraphQL::Execution::Interpreter)
2727
schema_class.mutation_execution_strategy(GraphQL::Execution::Interpreter)
2828
schema_class.subscription_execution_strategy(GraphQL::Execution::Interpreter)
29-
29+
schema_class.add_subscription_extension_if_necessary
3030
GraphQL::Schema::Object.include(HandlesRawValue)
3131
end
3232

lib/graphql/execution/multiplex.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,7 @@ def initialize(schema:, queries:, context:, max_complexity:)
3434
@schema = schema
3535
@queries = queries
3636
@context = context
37-
# TODO remove support for global tracers
38-
@tracers = schema.tracers + GraphQL::Tracing.tracers + (context[:tracers] || [])
37+
@tracers = schema.tracers + (context[:tracers] || [])
3938
# Support `context: {backtrace: true}`
4039
if context[:backtrace] && !@tracers.include?(GraphQL::Backtrace::Tracer)
4140
@tracers << GraphQL::Backtrace::Tracer

lib/graphql/introspection/schema_type.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ class SchemaType < Introspection::BaseObject
99
"query, mutation, and subscription operations."
1010

1111
field :types, [GraphQL::Schema::LateBoundType.new("__Type")], "A list of all types supported by this server.", null: false
12-
field :queryType, GraphQL::Schema::LateBoundType.new("__Type"), "The type that query operations will be rooted at.", null: false
13-
field :mutationType, GraphQL::Schema::LateBoundType.new("__Type"), "If this server supports mutation, the type that mutation operations will be rooted at.", null: true
14-
field :subscriptionType, GraphQL::Schema::LateBoundType.new("__Type"), "If this server support subscription, the type that subscription operations will be rooted at.", null: true
12+
field :query_type, GraphQL::Schema::LateBoundType.new("__Type"), "The type that query operations will be rooted at.", null: false
13+
field :mutation_type, GraphQL::Schema::LateBoundType.new("__Type"), "If this server supports mutation, the type that mutation operations will be rooted at.", null: true
14+
field :subscription_type, GraphQL::Schema::LateBoundType.new("__Type"), "If this server support subscription, the type that subscription operations will be rooted at.", null: true
1515
field :directives, [GraphQL::Schema::LateBoundType.new("__Directive")], "A list of all directives supported by this server.", null: false
1616

1717
def types

0 commit comments

Comments
 (0)