graphql-stitching 1.4.3 → 1.5.1

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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -0
  3. data/README.md +10 -7
  4. data/docs/README.md +1 -0
  5. data/docs/client.md +6 -0
  6. data/docs/composer.md +1 -1
  7. data/docs/subscriptions.md +208 -0
  8. data/docs/{resolver.md → type_resolver.md} +3 -3
  9. data/examples/subscriptions/.gitattributes +9 -0
  10. data/examples/subscriptions/.gitignore +35 -0
  11. data/examples/subscriptions/Gemfile +65 -0
  12. data/examples/subscriptions/README.md +38 -0
  13. data/examples/subscriptions/Rakefile +6 -0
  14. data/examples/subscriptions/app/channels/graphql_channel.rb +50 -0
  15. data/examples/subscriptions/app/controllers/graphql_controller.rb +44 -0
  16. data/examples/subscriptions/app/graphql/entities_schema.rb +42 -0
  17. data/examples/subscriptions/app/graphql/stitched_schema.rb +10 -0
  18. data/examples/subscriptions/app/graphql/subscriptions_schema.rb +54 -0
  19. data/examples/subscriptions/app/models/repository.rb +39 -0
  20. data/examples/subscriptions/app/views/graphql/client.html.erb +159 -0
  21. data/examples/subscriptions/bin/bundle +109 -0
  22. data/examples/subscriptions/bin/docker-entrypoint +8 -0
  23. data/examples/subscriptions/bin/importmap +4 -0
  24. data/examples/subscriptions/bin/rails +4 -0
  25. data/examples/subscriptions/bin/rake +4 -0
  26. data/examples/subscriptions/bin/setup +33 -0
  27. data/examples/subscriptions/config/application.rb +14 -0
  28. data/examples/subscriptions/config/boot.rb +4 -0
  29. data/examples/subscriptions/config/cable.yml +10 -0
  30. data/examples/subscriptions/config/credentials.yml.enc +1 -0
  31. data/examples/subscriptions/config/database.yml +25 -0
  32. data/examples/subscriptions/config/environment.rb +5 -0
  33. data/examples/subscriptions/config/environments/development.rb +74 -0
  34. data/examples/subscriptions/config/environments/production.rb +91 -0
  35. data/examples/subscriptions/config/environments/test.rb +64 -0
  36. data/examples/subscriptions/config/initializers/content_security_policy.rb +25 -0
  37. data/examples/subscriptions/config/initializers/filter_parameter_logging.rb +8 -0
  38. data/examples/subscriptions/config/initializers/inflections.rb +16 -0
  39. data/examples/subscriptions/config/initializers/permissions_policy.rb +13 -0
  40. data/examples/subscriptions/config/locales/en.yml +31 -0
  41. data/examples/subscriptions/config/master.key +1 -0
  42. data/examples/subscriptions/config/puma.rb +35 -0
  43. data/examples/subscriptions/config/routes.rb +8 -0
  44. data/examples/subscriptions/config/storage.yml +34 -0
  45. data/examples/subscriptions/config.ru +6 -0
  46. data/examples/subscriptions/db/seeds.rb +9 -0
  47. data/examples/subscriptions/public/404.html +17 -0
  48. data/examples/subscriptions/public/422.html +17 -0
  49. data/examples/subscriptions/public/500.html +16 -0
  50. data/examples/subscriptions/public/apple-touch-icon-precomposed.png +0 -0
  51. data/examples/subscriptions/public/apple-touch-icon.png +0 -0
  52. data/examples/subscriptions/public/favicon.ico +0 -0
  53. data/examples/subscriptions/public/robots.txt +1 -0
  54. data/lib/graphql/stitching/client.rb +18 -11
  55. data/lib/graphql/stitching/composer/{resolver_config.rb → type_resolver_config.rb} +3 -3
  56. data/lib/graphql/stitching/composer/{validate_resolvers.rb → validate_type_resolvers.rb} +8 -2
  57. data/lib/graphql/stitching/composer.rb +48 -42
  58. data/lib/graphql/stitching/executor/shaper.rb +3 -3
  59. data/lib/graphql/stitching/executor/{resolver_source.rb → type_resolver_source.rb} +2 -2
  60. data/lib/graphql/stitching/executor.rb +19 -11
  61. data/lib/graphql/stitching/http_executable.rb +3 -0
  62. data/lib/graphql/stitching/plan.rb +1 -1
  63. data/lib/graphql/stitching/planner/step.rb +1 -1
  64. data/lib/graphql/stitching/planner.rb +29 -15
  65. data/lib/graphql/stitching/{skip_include.rb → request/skip_include.rb} +3 -3
  66. data/lib/graphql/stitching/request.rb +44 -6
  67. data/lib/graphql/stitching/supergraph/to_definition.rb +3 -3
  68. data/lib/graphql/stitching/supergraph.rb +6 -3
  69. data/lib/graphql/stitching/{resolver → type_resolver}/arguments.rb +7 -7
  70. data/lib/graphql/stitching/{resolver → type_resolver}/keys.rb +3 -4
  71. data/lib/graphql/stitching/{resolver.rb → type_resolver.rb} +5 -5
  72. data/lib/graphql/stitching/util.rb +1 -0
  73. data/lib/graphql/stitching/version.rb +1 -1
  74. data/lib/graphql/stitching.rb +32 -4
  75. metadata +56 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a72adc399f618790fc1aefe1b6dca07709d45f10cc2936f5c3978d75b5d3f8c2
4
- data.tar.gz: d45d7b73045755a750d6d4334e00e30f33c5124f83c849c24669c98f7912ca92
3
+ metadata.gz: d8fb3561ddb47de2d07bdcdf32e940e1a151dff731081b2b4bffe09489809021
4
+ data.tar.gz: 9163e0d0e5116e232ee0594821d02568f85405c7fdc0f80e085e713d2e4d3a71
5
5
  SHA512:
6
- metadata.gz: 5302083cb9047ad77aa5e6276d02e3bdac8256ef328edb8c72b9248efbb279c48bb3e34b8e220f131d3efa541fdaa1b682fe4622501900defe797217cbc365c8
7
- data.tar.gz: 4e238de4607764406ac52634daafc293c1b5fe672457106ff1070e77bc4d52cdc1ace02ce46ca4cacbcde7648d8a4f2bb8cc0321157dfb3c85c3ec40d5f82e43
6
+ metadata.gz: 6823d661a3f812fbf11bbf14edf66f3da27a850399f909061c63d464d3be93777fe7f9c7158ea26cc0d1889aed04749d3ad10cd42c3ca76079604447ab66e3cc
7
+ data.tar.gz: 27dee9de11b90e960fb0c17d68129332ede38d237ef37ae1eca4cdca2c83696dec4b6c9f0283ee7f508c4f5076029afc81efa4b58d60e5b74411a6a368092947
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](./docs/subscriptions.md).
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
- - File uploads via [multipart form spec](https://github.com/jaydenseric/graphql-multipart-request-spec).
13
+ - [File uploads](./docs/http_executable.md) via multipart forms.
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
 
@@ -34,7 +35,7 @@ require "graphql/stitching"
34
35
 
35
36
  ## Usage
36
37
 
37
- The quickest way to start is to use the provided [`Client`](./docs/client.md) component that wraps a stitched graph in an executable workflow (with optional query plan caching hooks):
38
+ The [`Client`](./docs/client.md) component builds a stitched graph wrapped in an executable workflow (with optional query plan caching hooks):
38
39
 
39
40
  ```ruby
40
41
  movies_schema = <<~GRAPHQL
@@ -74,7 +75,7 @@ result = client.execute(
74
75
 
75
76
  Schemas provided in [location settings](./docs/composer.md#performing-composition) may be class-based schemas with local resolvers (locally-executable schemas), or schemas built from SDL strings (schema definition language parsed using `GraphQL::Schema.from_definition`) and mapped to remote locations via [executables](#executables).
76
77
 
77
- While the `Client` constructor is an easy quick start, the library also has several discrete components that can be assembled into custom workflows:
78
+ While `Client` is sufficient for most usecases, the library offers several discrete components that can be assembled into tailored workflows:
78
79
 
79
80
  - [Composer](./docs/composer.md) - merges and validates many schemas into one supergraph.
80
81
  - [Supergraph](./docs/supergraph.md) - manages the combined schema, location routing maps, and executable resources. Can be exported, cached, and rehydrated.
@@ -87,7 +88,7 @@ While the `Client` constructor is an easy quick start, the library also has seve
87
88
 
88
89
  ![Merging types](./docs/images/merging.png)
89
90
 
90
- To facilitate this merging of types, stitching must know how to cross-reference and fetch each variant of a type from its source location using [resolver queries](#merged-type-resolver-queries). For those in an Apollo ecosystem, there's also _limited_ support for merging types though a [federation `_entities` protocol](./docs/federation_entities.md).
91
+ To facilitate this merging of types, stitching must know how to cross-reference and fetch each variant of a type from its source location using [type resolver queries](#merged-type-resolver-queries). For those in an Apollo ecosystem, there's also _limited_ support for merging types though a [federation `_entities` protocol](./docs/federation_entities.md).
91
92
 
92
93
  ### Merged type resolver queries
93
94
 
@@ -248,7 +249,7 @@ type Query {
248
249
  }
249
250
  ```
250
251
 
251
- See [resolver arguments](./docs/resolver.md#arguments) for full documentation on shaping input.
252
+ See [resolver arguments](./docs/type_resolver.md#arguments) for full documentation on shaping input.
252
253
 
253
254
  #### Composite type keys
254
255
 
@@ -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)
@@ -456,6 +458,7 @@ This repo includes working examples of stitched schemas running across small Rac
456
458
 
457
459
  - [Merged types](./examples/merged_types)
458
460
  - [File uploads](./examples/file_uploads)
461
+ - [Subscriptions](./examples/subscriptions)
459
462
 
460
463
  ## Tests
461
464
 
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/client.md CHANGED
@@ -68,6 +68,12 @@ client.on_cache_write do |request, payload|
68
68
  end
69
69
  ```
70
70
 
71
+ All request digests use SHA2 by default. You can swap in [a faster algorithm](https://github.com/Shopify/blake3-rb) and/or add base scoping by reconfiguring the stitching library:
72
+
73
+ ```ruby
74
+ GraphQL::Stitching.digest { |str| Digest::MD5.hexdigest("v2/#{str}") }
75
+ ```
76
+
71
77
  Note that inlined input data works against caching, so you should _avoid_ these input literals when possible:
72
78
 
73
79
  ```graphql
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 the 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 composed 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 out 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
+ result = super(subscription_id, event, object)
185
+ result.context[:stitch_subscription_update]&.call(result)
186
+ result
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
+ ```
@@ -1,10 +1,10 @@
1
- ## GraphQL::Stitching::Resolver
1
+ ## GraphQL::Stitching::TypeResolver
2
2
 
3
- A `Resolver` contains all information about a root query used by stitching to fetch location-specific variants of a merged type. Specifically, resolvers manage parsed keys and argument structures.
3
+ A `TypeResolver` contains all information about a root query used by stitching to fetch location-specific variants of a merged type. Specifically, resolvers manage parsed keys and argument structures.
4
4
 
5
5
  ### Arguments
6
6
 
7
- Resolvers configure arguments through a template string of [GraphQL argument literal syntax](https://spec.graphql.org/October2021/#sec-Language.Arguments). This allows sending multiple arguments that intermix stitching keys with complex object shapes and other static values.
7
+ Type resolvers configure arguments through a template string of [GraphQL argument literal syntax](https://spec.graphql.org/October2021/#sec-Language.Arguments). This allows sending multiple arguments that intermix stitching keys with complex object shapes and other static values.
8
8
 
9
9
  #### Key insertions
10
10
 
@@ -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
+ })