graphql-stitching 1.4.2 → 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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -0
  3. data/README.md +4 -2
  4. data/docs/README.md +1 -0
  5. data/docs/composer.md +1 -1
  6. data/docs/subscriptions.md +208 -0
  7. data/examples/subscriptions/.gitattributes +9 -0
  8. data/examples/subscriptions/.gitignore +35 -0
  9. data/examples/subscriptions/Gemfile +65 -0
  10. data/examples/subscriptions/README.md +38 -0
  11. data/examples/subscriptions/Rakefile +6 -0
  12. data/examples/subscriptions/app/channels/graphql_channel.rb +50 -0
  13. data/examples/subscriptions/app/controllers/graphql_controller.rb +44 -0
  14. data/examples/subscriptions/app/graphql/entities_schema.rb +42 -0
  15. data/examples/subscriptions/app/graphql/stitched_schema.rb +10 -0
  16. data/examples/subscriptions/app/graphql/subscriptions_schema.rb +54 -0
  17. data/examples/subscriptions/app/models/repository.rb +39 -0
  18. data/examples/subscriptions/app/views/graphql/client.html.erb +159 -0
  19. data/examples/subscriptions/bin/bundle +109 -0
  20. data/examples/subscriptions/bin/docker-entrypoint +8 -0
  21. data/examples/subscriptions/bin/importmap +4 -0
  22. data/examples/subscriptions/bin/rails +4 -0
  23. data/examples/subscriptions/bin/rake +4 -0
  24. data/examples/subscriptions/bin/setup +33 -0
  25. data/examples/subscriptions/config/application.rb +14 -0
  26. data/examples/subscriptions/config/boot.rb +4 -0
  27. data/examples/subscriptions/config/cable.yml +10 -0
  28. data/examples/subscriptions/config/credentials.yml.enc +1 -0
  29. data/examples/subscriptions/config/database.yml +25 -0
  30. data/examples/subscriptions/config/environment.rb +5 -0
  31. data/examples/subscriptions/config/environments/development.rb +74 -0
  32. data/examples/subscriptions/config/environments/production.rb +91 -0
  33. data/examples/subscriptions/config/environments/test.rb +64 -0
  34. data/examples/subscriptions/config/initializers/content_security_policy.rb +25 -0
  35. data/examples/subscriptions/config/initializers/filter_parameter_logging.rb +8 -0
  36. data/examples/subscriptions/config/initializers/inflections.rb +16 -0
  37. data/examples/subscriptions/config/initializers/permissions_policy.rb +13 -0
  38. data/examples/subscriptions/config/locales/en.yml +31 -0
  39. data/examples/subscriptions/config/master.key +1 -0
  40. data/examples/subscriptions/config/puma.rb +35 -0
  41. data/examples/subscriptions/config/routes.rb +8 -0
  42. data/examples/subscriptions/config/storage.yml +34 -0
  43. data/examples/subscriptions/config.ru +6 -0
  44. data/examples/subscriptions/db/seeds.rb +9 -0
  45. data/examples/subscriptions/public/404.html +17 -0
  46. data/examples/subscriptions/public/422.html +17 -0
  47. data/examples/subscriptions/public/500.html +16 -0
  48. data/examples/subscriptions/public/apple-touch-icon-precomposed.png +0 -0
  49. data/examples/subscriptions/public/apple-touch-icon.png +0 -0
  50. data/examples/subscriptions/public/favicon.ico +0 -0
  51. data/examples/subscriptions/public/robots.txt +1 -0
  52. data/lib/graphql/stitching/client.rb +18 -11
  53. data/lib/graphql/stitching/composer/resolver_config.rb +1 -1
  54. data/lib/graphql/stitching/composer/validate_resolvers.rb +7 -1
  55. data/lib/graphql/stitching/composer.rb +30 -27
  56. data/lib/graphql/stitching/{shaper.rb → executor/shaper.rb} +3 -3
  57. data/lib/graphql/stitching/executor.rb +20 -11
  58. data/lib/graphql/stitching/http_executable.rb +3 -0
  59. data/lib/graphql/stitching/plan.rb +1 -1
  60. data/lib/graphql/stitching/{planner_step.rb → planner/step.rb} +3 -3
  61. data/lib/graphql/stitching/planner.rb +27 -7
  62. data/lib/graphql/stitching/{skip_include.rb → request/skip_include.rb} +2 -2
  63. data/lib/graphql/stitching/request.rb +42 -4
  64. data/lib/graphql/stitching/resolver/arguments.rb +2 -2
  65. data/lib/graphql/stitching/resolver/keys.rb +2 -3
  66. data/lib/graphql/stitching/resolver.rb +4 -4
  67. data/lib/graphql/stitching/supergraph.rb +5 -2
  68. data/lib/graphql/stitching/util.rb +1 -0
  69. data/lib/graphql/stitching/version.rb +1 -1
  70. data/lib/graphql/stitching.rb +18 -4
  71. metadata +51 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b81b29cdab4287ba09d6081595b83b4edcc96f0921790483f44d53ff946fb441
4
- data.tar.gz: 39c3351dbb4a712aa4f43006017660a961a2056b82b96a5a14b3110b1ecfad44
3
+ metadata.gz: 7b71b1d9e65a84356466a0ff822e177ef5faccce1e2a0cbd91d930b06ea50923
4
+ data.tar.gz: 8c73c1bcc78d23ea3b0c3f09da2659d84b7877000ccba86f79fa9f83f5801c60
5
5
  SHA512:
6
- metadata.gz: d376c15fa17088797be0b2f7ab42a94cf24050f68b37640aac88cc13a526d56f0ee366290b246e4cc6838a60050b9d9073610366cb7ad5bdb0a9a3435e688dd4
7
- data.tar.gz: 6a4fda5651bdd8794f59497e32bb9421c1d3f931a706483b55088597efdc9fe0bb1166dca16a66832c4ded42a21c89de2a7a8f2dc36cfb51bc44cfd40df9c4da
6
+ metadata.gz: 0e46b25856e61dc033ba0e2640d30133463c550af0071ac0e8bb3836b2fa8f4d0690018359b5b3b086861a907b5c66ea55eece9d4bfebe85296e723aedd96415
7
+ data.tar.gz: 79180fc8944adde3ee0abf79a6fefa3487724cfbf9fb55e00adf29aa42d637c1c53331aad34a0c9c32ed7e3120fdaa6dd26fba9644df86b0feb774a84a8876b2
data/.gitignore CHANGED
@@ -21,6 +21,10 @@ Gemfile.lock
21
21
  .ruby-version
22
22
  .DS_Store
23
23
 
24
+ examples/subscriptions/log/*
25
+ examples/subscriptions/tmp/*
26
+ examples/subscriptions/storage/*
27
+
24
28
  ## Specific to RubyMotion:
25
29
  .dat*
26
30
  .repl_history
data/README.md CHANGED
@@ -5,16 +5,17 @@ GraphQL stitching composes a single schema from multiple underlying GraphQL reso
5
5
  ![Stitched graph](./docs/images/stitching.png)
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
- - Subscriptions, defer/stream.
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:Schema` instances for various graph locations and _composes_ them into a combined [`Supergraph`](./supergraph.md) that is validated for integrity.
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,6 @@
1
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
2
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
+
4
+ require_relative "config/application"
5
+
6
+ Rails.application.load_tasks
@@ -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,10 @@
1
+ require_relative "../../../../lib/graphql/stitching"
2
+
3
+ StitchedSchema = GraphQL::Stitching::Client.new(locations: {
4
+ entities: {
5
+ schema: EntitiesSchema,
6
+ },
7
+ subscriptions: {
8
+ schema: SubscriptionsSchema,
9
+ },
10
+ })
@@ -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")