graphql-stitching 1.4.3 → 1.5.0
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/.gitignore +4 -0
- data/README.md +4 -2
- data/docs/README.md +1 -0
- data/docs/composer.md +1 -1
- data/docs/subscriptions.md +208 -0
- data/examples/subscriptions/.gitattributes +9 -0
- data/examples/subscriptions/.gitignore +35 -0
- data/examples/subscriptions/Gemfile +65 -0
- data/examples/subscriptions/README.md +38 -0
- data/examples/subscriptions/Rakefile +6 -0
- data/examples/subscriptions/app/channels/graphql_channel.rb +50 -0
- data/examples/subscriptions/app/controllers/graphql_controller.rb +44 -0
- data/examples/subscriptions/app/graphql/entities_schema.rb +42 -0
- data/examples/subscriptions/app/graphql/stitched_schema.rb +10 -0
- data/examples/subscriptions/app/graphql/subscriptions_schema.rb +54 -0
- data/examples/subscriptions/app/models/repository.rb +39 -0
- data/examples/subscriptions/app/views/graphql/client.html.erb +159 -0
- data/examples/subscriptions/bin/bundle +109 -0
- data/examples/subscriptions/bin/docker-entrypoint +8 -0
- data/examples/subscriptions/bin/importmap +4 -0
- data/examples/subscriptions/bin/rails +4 -0
- data/examples/subscriptions/bin/rake +4 -0
- data/examples/subscriptions/bin/setup +33 -0
- data/examples/subscriptions/config/application.rb +14 -0
- data/examples/subscriptions/config/boot.rb +4 -0
- data/examples/subscriptions/config/cable.yml +10 -0
- data/examples/subscriptions/config/credentials.yml.enc +1 -0
- data/examples/subscriptions/config/database.yml +25 -0
- data/examples/subscriptions/config/environment.rb +5 -0
- data/examples/subscriptions/config/environments/development.rb +74 -0
- data/examples/subscriptions/config/environments/production.rb +91 -0
- data/examples/subscriptions/config/environments/test.rb +64 -0
- data/examples/subscriptions/config/initializers/content_security_policy.rb +25 -0
- data/examples/subscriptions/config/initializers/filter_parameter_logging.rb +8 -0
- data/examples/subscriptions/config/initializers/inflections.rb +16 -0
- data/examples/subscriptions/config/initializers/permissions_policy.rb +13 -0
- data/examples/subscriptions/config/locales/en.yml +31 -0
- data/examples/subscriptions/config/master.key +1 -0
- data/examples/subscriptions/config/puma.rb +35 -0
- data/examples/subscriptions/config/routes.rb +8 -0
- data/examples/subscriptions/config/storage.yml +34 -0
- data/examples/subscriptions/config.ru +6 -0
- data/examples/subscriptions/db/seeds.rb +9 -0
- data/examples/subscriptions/public/404.html +17 -0
- data/examples/subscriptions/public/422.html +17 -0
- data/examples/subscriptions/public/500.html +16 -0
- data/examples/subscriptions/public/apple-touch-icon-precomposed.png +0 -0
- data/examples/subscriptions/public/apple-touch-icon.png +0 -0
- data/examples/subscriptions/public/favicon.ico +0 -0
- data/examples/subscriptions/public/robots.txt +1 -0
- data/lib/graphql/stitching/client.rb +18 -11
- data/lib/graphql/stitching/composer/resolver_config.rb +1 -1
- data/lib/graphql/stitching/composer/validate_resolvers.rb +7 -1
- data/lib/graphql/stitching/composer.rb +30 -27
- data/lib/graphql/stitching/executor/shaper.rb +1 -1
- data/lib/graphql/stitching/executor.rb +19 -11
- data/lib/graphql/stitching/http_executable.rb +3 -0
- data/lib/graphql/stitching/plan.rb +1 -1
- data/lib/graphql/stitching/planner.rb +21 -5
- data/lib/graphql/stitching/{skip_include.rb → request/skip_include.rb} +2 -2
- data/lib/graphql/stitching/request.rb +42 -4
- data/lib/graphql/stitching/resolver/arguments.rb +2 -2
- data/lib/graphql/stitching/resolver/keys.rb +2 -3
- data/lib/graphql/stitching/resolver.rb +3 -3
- data/lib/graphql/stitching/supergraph.rb +5 -2
- data/lib/graphql/stitching/util.rb +1 -0
- data/lib/graphql/stitching/version.rb +1 -1
- data/lib/graphql/stitching.rb +17 -1
- metadata +49 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7b71b1d9e65a84356466a0ff822e177ef5faccce1e2a0cbd91d930b06ea50923
|
4
|
+
data.tar.gz: 8c73c1bcc78d23ea3b0c3f09da2659d84b7877000ccba86f79fa9f83f5801c60
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0e46b25856e61dc033ba0e2640d30133463c550af0071ac0e8bb3836b2fa8f4d0690018359b5b3b086861a907b5c66ea55eece9d4bfebe85296e723aedd96415
|
7
|
+
data.tar.gz: 79180fc8944adde3ee0abf79a6fefa3487724cfbf9fb55e00adf29aa42d637c1c53331aad34a0c9c32ed7e3120fdaa6dd26fba9644df86b0feb774a84a8876b2
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -5,16 +5,17 @@ GraphQL stitching composes a single schema from multiple underlying GraphQL reso
|
|
5
5
|

|
6
6
|
|
7
7
|
**Supports:**
|
8
|
+
- All operation types: query, mutation, and subscription.
|
8
9
|
- Merged object and abstract types.
|
9
|
-
- Multiple and composite keys per merged type.
|
10
10
|
- Shared objects, fields, enums, and inputs across locations.
|
11
|
+
- Multiple and composite type keys.
|
11
12
|
- Combining local and remote schemas.
|
12
13
|
- File uploads via [multipart form spec](https://github.com/jaydenseric/graphql-multipart-request-spec).
|
13
14
|
- Tested with all minor versions of `graphql-ruby`.
|
14
15
|
|
15
16
|
**NOT Supported:**
|
16
17
|
- Computed fields (ie: federation-style `@requires`).
|
17
|
-
-
|
18
|
+
- Defer/stream.
|
18
19
|
|
19
20
|
This Ruby implementation is a sibling to [GraphQL Tools](https://the-guild.dev/graphql/stitching) (JS) and [Bramble](https://movio.github.io/bramble/) (Go), and its capabilities fall somewhere in between them. GraphQL stitching is similar in concept to [Apollo Federation](https://www.apollographql.com/docs/federation/), though more generic. The opportunity here is for a Ruby application to stitch its local schemas together or onto remote sources without requiring an additional proxy service running in another language. If your goal is to build a purely high-throughput federated reverse proxy, consider not using Ruby.
|
20
21
|
|
@@ -445,6 +446,7 @@ The [Executor](./docs/executor.md) component builds atop the Ruby fiber-based im
|
|
445
446
|
- [Modeling foreign keys for stitching](./docs/mechanics.md##modeling-foreign-keys-for-stitching)
|
446
447
|
- [Deploying a stitched schema](./docs/mechanics.md#deploying-a-stitched-schema)
|
447
448
|
- [Schema composition merge patterns](./docs/composer.md#merge-patterns)
|
449
|
+
- [Subscriptions tutorial](./docs/subscriptions.md)
|
448
450
|
- [Field selection routing](./docs/mechanics.md#field-selection-routing)
|
449
451
|
- [Root selection routing](./docs/mechanics.md#root-selection-routing)
|
450
452
|
- [Stitched errors](./docs/mechanics.md#stitched-errors)
|
data/docs/README.md
CHANGED
@@ -15,4 +15,5 @@ Major components include:
|
|
15
15
|
Additional topics:
|
16
16
|
|
17
17
|
- [Stitching mechanics](./mechanics.md) - more about building for stitching and how it operates.
|
18
|
+
- [Subscriptions](./subscriptions.md) - explore how to stitch realtime event subscriptions.
|
18
19
|
- [Federation entities](./federation_entities.md) - more about Apollo Federation compatibility.
|
data/docs/composer.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
## GraphQL::Stitching::Composer
|
2
2
|
|
3
|
-
A `Composer` receives many individual `GraphQL
|
3
|
+
A `Composer` receives many individual `GraphQL::Schema` instances representing various graph locations and _composes_ them into one combined [`Supergraph`](./supergraph.md) that is validated for integrity.
|
4
4
|
|
5
5
|
### Configuring composition
|
6
6
|
|
@@ -0,0 +1,208 @@
|
|
1
|
+
## Stitching subscriptions
|
2
|
+
|
3
|
+
Stitching is an interesting prospect for subscriptions because socket-based interactions can be isolated to their own schema/server with very little implementation beyond resolving entity keys. Then, entity data can be stitched onto subscription payloads from other locations.
|
4
|
+
|
5
|
+
### Composing a subscriptions schema
|
6
|
+
|
7
|
+
For simplicity, subscription resolvers should exist together in a single schema (multiple schemas with subscriptions probably aren't worth the confusion). This subscriptions schema may provide basic entity types that will merge with other locations. For example, here's a bare-bones subscriptions schema:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
class SubscriptionSchema < GraphQL::Schema
|
11
|
+
class Post < GraphQL::Schema::Object
|
12
|
+
field :id, ID, null: false
|
13
|
+
end
|
14
|
+
|
15
|
+
class Comment < GraphQL::Schema::Object
|
16
|
+
field :id, ID, null: false
|
17
|
+
end
|
18
|
+
|
19
|
+
class CommentAddedToPost < GraphQL::Schema::Subscription
|
20
|
+
argument :post_id, ID, required: true
|
21
|
+
field :post, Post, null: false
|
22
|
+
field :comment, Comment, null: true
|
23
|
+
|
24
|
+
def subscribe(post_id:)
|
25
|
+
{ post: { id: post_id }, comment: nil }
|
26
|
+
end
|
27
|
+
|
28
|
+
def update(post_id:)
|
29
|
+
{ post: { id: post_id }, comment: object }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class SubscriptionType < GraphQL::Schema::Object
|
34
|
+
field :comment_added_to_post, subscription: CommentAddedToPost
|
35
|
+
end
|
36
|
+
|
37
|
+
use GraphQL::Subscriptions::ActionCableSubscriptions
|
38
|
+
subscription SubscriptionType
|
39
|
+
end
|
40
|
+
```
|
41
|
+
|
42
|
+
The above subscriptions schema can compose with other locations, such as the following that provides full entity types:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
class EntitiesSchema < GraphQL::Schema
|
46
|
+
class StitchingResolver < GraphQL::Schema::Directive
|
47
|
+
graphql_name "stitch"
|
48
|
+
locations FIELD_DEFINITION
|
49
|
+
argument :key, String, required: true
|
50
|
+
argument :arguments, String, required: false
|
51
|
+
repeatable true
|
52
|
+
end
|
53
|
+
|
54
|
+
class Comment < GraphQL::Schema::Object
|
55
|
+
field :id, ID, null: false
|
56
|
+
field :message, String, null: false
|
57
|
+
end
|
58
|
+
|
59
|
+
class Post < GraphQL::Schema::Object
|
60
|
+
field :id, ID, null: false
|
61
|
+
field :title, String, null: false
|
62
|
+
field :comments, [Comment, null: false], null: false
|
63
|
+
end
|
64
|
+
|
65
|
+
class QueryType < GraphQL::Schema::Object
|
66
|
+
field :posts, [Post, null: true] do
|
67
|
+
directive StitchingResolver, key: "id"
|
68
|
+
argument :ids, [ID], required: true
|
69
|
+
end
|
70
|
+
|
71
|
+
def posts(ids:)
|
72
|
+
records_by_id = Post.where(id: ids).index_by(&:id)
|
73
|
+
ids.map { |id| records_by_id[id] }
|
74
|
+
end
|
75
|
+
|
76
|
+
field :comments, [Comment, null: true] do
|
77
|
+
directive StitchingResolver, key: "id"
|
78
|
+
argument :ids, [ID], required: true
|
79
|
+
end
|
80
|
+
|
81
|
+
def comments(ids:)
|
82
|
+
records_by_id = Comment.where(id: ids).index_by(&:id)
|
83
|
+
ids.map { |id| records_by_id[id] }
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
query QueryType
|
88
|
+
end
|
89
|
+
```
|
90
|
+
|
91
|
+
These schemas can be composed as normal into a stitching client. The subscriptions schema must be locally-executable while other entity schema(s) may be served from anywhere:
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
StitchedSchema = GraphQL::Stitching::Client.new(locations: {
|
95
|
+
subscriptions: {
|
96
|
+
schema: SubscriptionSchema, # << locally executable!
|
97
|
+
},
|
98
|
+
entities: {
|
99
|
+
schema: GraphQL::Schema.from_definition(entities_schema_sdl),
|
100
|
+
executable: GraphQL::Stitching::HttpExecutable.new("http://localhost:3001"),
|
101
|
+
},
|
102
|
+
})
|
103
|
+
```
|
104
|
+
|
105
|
+
### Serving stitched subscriptions
|
106
|
+
|
107
|
+
Once you've stitched a schema with subscriptions, it gets called as part of three workflows:
|
108
|
+
|
109
|
+
1. Controller - handles normal query and mutation requests recieved via HTTP.
|
110
|
+
2. Channel - handles subscription-create requests recieved through a socket connection.
|
111
|
+
3. Plugin – handles subscription-update events pushed to the socket connection.
|
112
|
+
|
113
|
+
#### Controller
|
114
|
+
|
115
|
+
A controller will recieve basic query and mutation requests sent over HTTP, including introspection requests. Fulfill these using the stitched schema client.
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
class GraphqlController < ApplicationController
|
119
|
+
skip_before_action :verify_authenticity_token
|
120
|
+
layout false
|
121
|
+
|
122
|
+
def execute
|
123
|
+
result = StitchedSchema.execute(
|
124
|
+
params[:query],
|
125
|
+
context: {},
|
126
|
+
variables: params[:variables],
|
127
|
+
operation_name: params[:operationName],
|
128
|
+
)
|
129
|
+
|
130
|
+
render json: result
|
131
|
+
end
|
132
|
+
end
|
133
|
+
```
|
134
|
+
|
135
|
+
#### Channel
|
136
|
+
|
137
|
+
A channel handles subscription requests initiated via websocket connection. This mostly follows the [GraphQL Ruby documentation example](https://graphql-ruby.org/api-doc/2.3.9/GraphQL/Subscriptions/ActionCableSubscriptions), except that `execute` uses the stitched schema client while `unsubscribed` uses the subscriptions subschema directly:
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
class GraphqlChannel < ApplicationCable::Channel
|
141
|
+
def subscribed
|
142
|
+
@subscription_ids = []
|
143
|
+
end
|
144
|
+
|
145
|
+
def execute(params)
|
146
|
+
result = StitchedSchema.execute(
|
147
|
+
params["query"],
|
148
|
+
context: { channel: self },
|
149
|
+
variables: params["variables"],
|
150
|
+
operation_name: params["operationName"]
|
151
|
+
)
|
152
|
+
|
153
|
+
payload = {
|
154
|
+
result: result.to_h,
|
155
|
+
more: result.subscription?,
|
156
|
+
}
|
157
|
+
|
158
|
+
if result.context[:subscription_id]
|
159
|
+
@subscription_ids << result.context[:subscription_id]
|
160
|
+
end
|
161
|
+
|
162
|
+
transmit(payload)
|
163
|
+
end
|
164
|
+
|
165
|
+
def unsubscribed
|
166
|
+
@subscription_ids.each { |sid|
|
167
|
+
# Go directly through the subscriptions subschema
|
168
|
+
# when managing/triggering subscriptions:
|
169
|
+
SubscriptionSchema.subscriptions.delete_subscription(sid)
|
170
|
+
}
|
171
|
+
end
|
172
|
+
end
|
173
|
+
```
|
174
|
+
|
175
|
+
What happens behind the scenes here is that stitching filters the `execute` request down to just subscription selections, and passes those through to the subscriptions subschema where they register an event binding. The subscriber response gets stitched while passing back up through the stitching client.
|
176
|
+
|
177
|
+
#### Plugin
|
178
|
+
|
179
|
+
Lastly, update events trigger with the filtered subscriptions selection, so must get stitched before transmitting. The stitching client adds an update handler into request context for this purpose. A small patch to the subscriptions plugin class can call this handler on update event payloads before transmitting them:
|
180
|
+
|
181
|
+
```ruby
|
182
|
+
class StitchedActionCableSubscriptions < GraphQL::Subscriptions::ActionCableSubscriptions
|
183
|
+
def execute_update(subscription_id, event, object)
|
184
|
+
super(subscription_id, event, object).tap do |result|
|
185
|
+
result.context[:stitch_subscription_update]&.call(result)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
class SubscriptionSchema < GraphQL::Schema
|
191
|
+
# switch the plugin on the subscriptions schema to use the patched class...
|
192
|
+
use StitchedActionCableSubscriptions
|
193
|
+
end
|
194
|
+
```
|
195
|
+
|
196
|
+
### Triggering subscriptions
|
197
|
+
|
198
|
+
Subscription update events are triggered as normal directly through the subscriptions subschema:
|
199
|
+
|
200
|
+
```ruby
|
201
|
+
class Comment < ApplicationRecord
|
202
|
+
after_create :trigger_subscriptions
|
203
|
+
|
204
|
+
def trigger_subscriptions
|
205
|
+
SubscriptionsSchema.subscriptions.trigger(:comment_added_to_post, { post_id: post_id }, self)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
```
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# See https://git-scm.com/docs/gitattributes for more about git attribute files.
|
2
|
+
|
3
|
+
# Mark the database schema as having been generated.
|
4
|
+
db/schema.rb linguist-generated
|
5
|
+
|
6
|
+
# Mark any vendored files as having been vendored.
|
7
|
+
vendor/* linguist-vendored
|
8
|
+
config/credentials/*.yml.enc diff=rails_credentials
|
9
|
+
config/credentials.yml.enc diff=rails_credentials
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# See https://help.github.com/articles/ignoring-files for more about ignoring files.
|
2
|
+
#
|
3
|
+
# If you find yourself ignoring temporary files generated by your text editor
|
4
|
+
# or operating system, you probably want to add a global ignore instead:
|
5
|
+
# git config --global core.excludesfile '~/.gitignore_global'
|
6
|
+
|
7
|
+
# Ignore bundler config.
|
8
|
+
/.bundle
|
9
|
+
|
10
|
+
# Ignore all environment files (except templates).
|
11
|
+
/.env*
|
12
|
+
!/.env*.erb
|
13
|
+
|
14
|
+
# Ignore all logfiles and tempfiles.
|
15
|
+
/log/*
|
16
|
+
/tmp/*
|
17
|
+
!/log/.keep
|
18
|
+
!/tmp/.keep
|
19
|
+
|
20
|
+
# Ignore pidfiles, but keep the directory.
|
21
|
+
/tmp/pids/*
|
22
|
+
!/tmp/pids/
|
23
|
+
!/tmp/pids/.keep
|
24
|
+
|
25
|
+
# Ignore storage (uploaded files in development and any SQLite databases).
|
26
|
+
/storage/*
|
27
|
+
!/storage/.keep
|
28
|
+
/tmp/storage/*
|
29
|
+
!/tmp/storage/
|
30
|
+
!/tmp/storage/.keep
|
31
|
+
|
32
|
+
/public/assets
|
33
|
+
|
34
|
+
# Ignore master key for decrypting credentials and more.
|
35
|
+
/config/master.key
|
@@ -0,0 +1,65 @@
|
|
1
|
+
source "https://rubygems.org"
|
2
|
+
|
3
|
+
ruby "3.1.1"
|
4
|
+
|
5
|
+
# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
|
6
|
+
gem "rails", "~> 7.1.3", ">= 7.1.3.4"
|
7
|
+
|
8
|
+
# Use sqlite3 as the database for Active Record
|
9
|
+
gem "sqlite3", "~> 1.4"
|
10
|
+
|
11
|
+
# Use the Puma web server [https://github.com/puma/puma]
|
12
|
+
gem "puma", ">= 5.0"
|
13
|
+
|
14
|
+
# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]
|
15
|
+
gem "importmap-rails"
|
16
|
+
|
17
|
+
# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]
|
18
|
+
gem "turbo-rails"
|
19
|
+
|
20
|
+
# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]
|
21
|
+
gem "stimulus-rails"
|
22
|
+
|
23
|
+
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
|
24
|
+
gem "jbuilder"
|
25
|
+
|
26
|
+
# Use Redis adapter to run Action Cable in production
|
27
|
+
gem "redis", ">= 4.0.1"
|
28
|
+
|
29
|
+
# Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis]
|
30
|
+
# gem "kredis"
|
31
|
+
|
32
|
+
# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
|
33
|
+
# gem "bcrypt", "~> 3.1.7"
|
34
|
+
|
35
|
+
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
|
36
|
+
gem "tzinfo-data", platforms: %i[ mswin mswin64 mingw x64_mingw jruby ]
|
37
|
+
|
38
|
+
# Reduces boot times through caching; required in config/boot.rb
|
39
|
+
gem "bootsnap", require: false
|
40
|
+
|
41
|
+
gem "graphql", "~> 2.3.0"
|
42
|
+
|
43
|
+
group :development, :test do
|
44
|
+
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
|
45
|
+
gem "debug", platforms: %i[ mri mswin mswin64 mingw x64_mingw ]
|
46
|
+
end
|
47
|
+
|
48
|
+
group :development do
|
49
|
+
# Use console on exceptions pages [https://github.com/rails/web-console]
|
50
|
+
gem "web-console"
|
51
|
+
|
52
|
+
# Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler]
|
53
|
+
# gem "rack-mini-profiler"
|
54
|
+
|
55
|
+
# Speed up commands on slow machines / big apps [https://github.com/rails/spring]
|
56
|
+
# gem "spring"
|
57
|
+
|
58
|
+
gem "error_highlight", ">= 0.4.0", platforms: [:ruby]
|
59
|
+
end
|
60
|
+
|
61
|
+
group :test do
|
62
|
+
# Use system testing [https://guides.rubyonrails.org/testing.html#system-testing]
|
63
|
+
gem "capybara"
|
64
|
+
gem "selenium-webdriver"
|
65
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# Subscriptions example
|
2
|
+
|
3
|
+
This example demonstrates stitching subscriptions in a small Rails application. No database required, just bundle-install and try running it:
|
4
|
+
|
5
|
+
```shell
|
6
|
+
cd examples/subscriptions
|
7
|
+
bundle install
|
8
|
+
bin/rails s
|
9
|
+
```
|
10
|
+
|
11
|
+
Then visit the GraphiQL client running at [`http://localhost:3000`](http://localhost:3000) and try subscribing:
|
12
|
+
|
13
|
+
```graphql
|
14
|
+
subscription SubscribeToComments {
|
15
|
+
commentAddedToPost(postId: "1") {
|
16
|
+
post {
|
17
|
+
id
|
18
|
+
title
|
19
|
+
comments {
|
20
|
+
id
|
21
|
+
message
|
22
|
+
}
|
23
|
+
}
|
24
|
+
comment {
|
25
|
+
id
|
26
|
+
message
|
27
|
+
}
|
28
|
+
}
|
29
|
+
}
|
30
|
+
```
|
31
|
+
|
32
|
+
Upon running that subscription, you'll recieve an initial payload for the subscribe event that stitches post data from another schema. Now try triggering events by hitting this URL in another browser window:
|
33
|
+
|
34
|
+
```
|
35
|
+
http://localhost:3000/graphql/event
|
36
|
+
```
|
37
|
+
|
38
|
+
Each refresh of the above URL will add a comment and trigger a subscription event. Assuming you're subscribed, you should see comment activity appear in the GraphiQL output. Again, these update events are stitched to enrich the basic subscription payload with additional data from another schema.
|
@@ -0,0 +1,50 @@
|
|
1
|
+
class GraphqlChannel < ActionCable::Channel::Base
|
2
|
+
def subscribed
|
3
|
+
@subscription_ids = []
|
4
|
+
end
|
5
|
+
|
6
|
+
def execute(data)
|
7
|
+
result = StitchedSchema.execute(
|
8
|
+
data["query"],
|
9
|
+
context: { channel: self },
|
10
|
+
variables: ensure_hash(data["variables"]),
|
11
|
+
operation_name: data["operationName"],
|
12
|
+
)
|
13
|
+
|
14
|
+
payload = {
|
15
|
+
result: result.to_h,
|
16
|
+
more: result.subscription?,
|
17
|
+
}
|
18
|
+
|
19
|
+
if result.context[:subscription_id]
|
20
|
+
@subscription_ids << result.context[:subscription_id]
|
21
|
+
end
|
22
|
+
|
23
|
+
transmit(payload)
|
24
|
+
end
|
25
|
+
|
26
|
+
def unsubscribed
|
27
|
+
@subscription_ids.each { |sid|
|
28
|
+
SubscriptionsSchema.subscriptions.delete_subscription(sid)
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def ensure_hash(ambiguous_param)
|
35
|
+
case ambiguous_param
|
36
|
+
when String
|
37
|
+
if ambiguous_param.present?
|
38
|
+
ensure_hash(JSON.parse(ambiguous_param))
|
39
|
+
else
|
40
|
+
{}
|
41
|
+
end
|
42
|
+
when Hash, ActionController::Parameters
|
43
|
+
ambiguous_param
|
44
|
+
when nil
|
45
|
+
{}
|
46
|
+
else
|
47
|
+
raise ArgumentError, "Unexpected parameter: #{ambiguous_param}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
class GraphqlController < ActionController::Base
|
2
|
+
skip_before_action :verify_authenticity_token
|
3
|
+
layout false
|
4
|
+
|
5
|
+
def client
|
6
|
+
end
|
7
|
+
|
8
|
+
def execute
|
9
|
+
result = StitchedSchema.execute(
|
10
|
+
params[:query],
|
11
|
+
variables: ensure_hash(params[:variables]),
|
12
|
+
context: {},
|
13
|
+
operation_name: params[:operationName],
|
14
|
+
)
|
15
|
+
|
16
|
+
render json: result
|
17
|
+
end
|
18
|
+
|
19
|
+
COMMENTS = ["Great", "Meh", "Terrible"].freeze
|
20
|
+
|
21
|
+
def event
|
22
|
+
comment = Repository.add_comment("1", COMMENTS.sample)
|
23
|
+
render json: comment
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def ensure_hash(ambiguous_param)
|
29
|
+
case ambiguous_param
|
30
|
+
when String
|
31
|
+
if ambiguous_param.present?
|
32
|
+
ensure_hash(JSON.parse(ambiguous_param))
|
33
|
+
else
|
34
|
+
{}
|
35
|
+
end
|
36
|
+
when Hash, ActionController::Parameters
|
37
|
+
ambiguous_param
|
38
|
+
when nil
|
39
|
+
{}
|
40
|
+
else
|
41
|
+
raise ArgumentError, "Unexpected parameter: #{ambiguous_param}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
class EntitiesSchema < GraphQL::Schema
|
2
|
+
class StitchingResolver < GraphQL::Schema::Directive
|
3
|
+
graphql_name "stitch"
|
4
|
+
locations FIELD_DEFINITION
|
5
|
+
argument :key, String, required: true
|
6
|
+
argument :arguments, String, required: false
|
7
|
+
repeatable true
|
8
|
+
end
|
9
|
+
|
10
|
+
class Comment < GraphQL::Schema::Object
|
11
|
+
field :id, ID, null: false
|
12
|
+
field :message, String, null: false
|
13
|
+
end
|
14
|
+
|
15
|
+
class Post < GraphQL::Schema::Object
|
16
|
+
field :id, ID, null: false
|
17
|
+
field :title, String, null: false
|
18
|
+
field :comments, [Comment, null: false], null: false
|
19
|
+
end
|
20
|
+
|
21
|
+
class QueryType < GraphQL::Schema::Object
|
22
|
+
field :posts, [Post, null: true] do
|
23
|
+
directive StitchingResolver, key: "id"
|
24
|
+
argument :ids, [ID], required: true
|
25
|
+
end
|
26
|
+
|
27
|
+
def posts(ids:)
|
28
|
+
ids.map { Repository.post(_1) }
|
29
|
+
end
|
30
|
+
|
31
|
+
field :comments, [Comment, null: true] do
|
32
|
+
directive StitchingResolver, key: "id"
|
33
|
+
argument :ids, [ID], required: true
|
34
|
+
end
|
35
|
+
|
36
|
+
def comments(ids:)
|
37
|
+
ids.map { Repository.comment(_1) }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
query QueryType
|
42
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
class SubscriptionsSchema < GraphQL::Schema
|
2
|
+
class StitchedActionCableSubscriptions < GraphQL::Subscriptions::ActionCableSubscriptions
|
3
|
+
def execute_update(subscription_id, event, object)
|
4
|
+
super(subscription_id, event, object).tap do |result|
|
5
|
+
result.context[:stitch_subscription_update]&.call(result)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class Post < GraphQL::Schema::Object
|
11
|
+
field :id, ID, null: false
|
12
|
+
end
|
13
|
+
|
14
|
+
class Comment < GraphQL::Schema::Object
|
15
|
+
field :id, ID, null: false
|
16
|
+
end
|
17
|
+
|
18
|
+
class CommentAddedToPost < GraphQL::Schema::Subscription
|
19
|
+
argument :post_id, ID, required: true
|
20
|
+
field :post, Post, null: false
|
21
|
+
field :comment, Comment, null: true
|
22
|
+
|
23
|
+
def subscribe(post_id:)
|
24
|
+
{
|
25
|
+
post: { id: post_id },
|
26
|
+
comment: nil,
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
def update(post_id:)
|
31
|
+
{
|
32
|
+
post: { id: post_id },
|
33
|
+
comment: object,
|
34
|
+
}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class SubscriptionType < GraphQL::Schema::Object
|
39
|
+
field :comment_added_to_post, subscription: CommentAddedToPost
|
40
|
+
end
|
41
|
+
|
42
|
+
class QueryType < GraphQL::Schema::Object
|
43
|
+
field :ping, String
|
44
|
+
|
45
|
+
def ping
|
46
|
+
"PONG"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
use StitchedActionCableSubscriptions
|
51
|
+
|
52
|
+
subscription SubscriptionType
|
53
|
+
query QueryType
|
54
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
class Repository
|
2
|
+
POSTS = {}
|
3
|
+
COMMENTS = {}
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def post(id)
|
7
|
+
POSTS.fetch(id)
|
8
|
+
end
|
9
|
+
|
10
|
+
def comment(id)
|
11
|
+
COMMENTS.fetch(id)
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_post(title, id = Time.zone.now.to_i.to_s)
|
15
|
+
post = {
|
16
|
+
id: id,
|
17
|
+
title: title,
|
18
|
+
comments: [],
|
19
|
+
}
|
20
|
+
POSTS[post[:id]] = post
|
21
|
+
post
|
22
|
+
end
|
23
|
+
|
24
|
+
def add_comment(post_id, message)
|
25
|
+
comment = {
|
26
|
+
id: Time.zone.now.to_i.to_s,
|
27
|
+
message: message,
|
28
|
+
}
|
29
|
+
parent = post(post_id)
|
30
|
+
parent[:comments] << comment
|
31
|
+
COMMENTS[comment[:id]] = comment
|
32
|
+
|
33
|
+
SubscriptionsSchema.subscriptions.trigger(:comment_added_to_post, { post_id: parent[:id] }, comment)
|
34
|
+
comment
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
Repository.add_post("How to walk, talk, and chew gum", "1")
|