Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Subscriptions demo app #153

Merged
merged 1 commit into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ Gemfile.lock
.ruby-version
.DS_Store

examples/subscriptions/log/*
examples/subscriptions/tmp/*
examples/subscriptions/storage/*

## Specific to RubyMotion:
.dat*
.repl_history
Expand Down
8 changes: 5 additions & 3 deletions docs/subscriptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ class EntitiesSchema < GraphQL::Schema
end

def posts(ids:)
Post.where(id: ids)
records_by_id = Post.where(id: ids).index_by(&:id)
ids.map { |id| records_by_id[id] }
end

field :comments, [Comment, null: true] do
Expand All @@ -78,7 +79,8 @@ class EntitiesSchema < GraphQL::Schema
end

def comments(ids:)
Comment.where(id: ids)
records_by_id = Comment.where(id: ids).index_by(&:id)
ids.map { |id| records_by_id[id] }
end
end

Expand Down Expand Up @@ -185,7 +187,7 @@ class StitchedActionCableSubscriptions < GraphQL::Subscriptions::ActionCableSubs
end
end

class SubscriptionSchema
class SubscriptionSchema < GraphQL::Schema
# switch the plugin on the subscriptions schema to use the patched class...
use StitchedActionCableSubscriptions
end
Expand Down
9 changes: 9 additions & 0 deletions examples/subscriptions/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# See https://git-scm.com/docs/gitattributes for more about git attribute files.

# Mark the database schema as having been generated.
db/schema.rb linguist-generated

# Mark any vendored files as having been vendored.
vendor/* linguist-vendored
config/credentials/*.yml.enc diff=rails_credentials
config/credentials.yml.enc diff=rails_credentials
35 changes: 35 additions & 0 deletions examples/subscriptions/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# See https://help.github.com/articles/ignoring-files for more about ignoring files.
#
# If you find yourself ignoring temporary files generated by your text editor
# or operating system, you probably want to add a global ignore instead:
# git config --global core.excludesfile '~/.gitignore_global'

# Ignore bundler config.
/.bundle

# Ignore all environment files (except templates).
/.env*
!/.env*.erb

# Ignore all logfiles and tempfiles.
/log/*
/tmp/*
!/log/.keep
!/tmp/.keep

# Ignore pidfiles, but keep the directory.
/tmp/pids/*
!/tmp/pids/
!/tmp/pids/.keep

# Ignore storage (uploaded files in development and any SQLite databases).
/storage/*
!/storage/.keep
/tmp/storage/*
!/tmp/storage/
!/tmp/storage/.keep

/public/assets

# Ignore master key for decrypting credentials and more.
/config/master.key
65 changes: 65 additions & 0 deletions examples/subscriptions/Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
source "https://rubygems.org"

ruby "3.1.1"

# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem "rails", "~> 7.1.3", ">= 7.1.3.4"

# Use sqlite3 as the database for Active Record
gem "sqlite3", "~> 1.4"

# Use the Puma web server [https://github.com/puma/puma]
gem "puma", ">= 5.0"

# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]
gem "importmap-rails"

# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]
gem "turbo-rails"

# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]
gem "stimulus-rails"

# Build JSON APIs with ease [https://github.com/rails/jbuilder]
gem "jbuilder"

# Use Redis adapter to run Action Cable in production
gem "redis", ">= 4.0.1"

# Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis]
# gem "kredis"

# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
# gem "bcrypt", "~> 3.1.7"

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "tzinfo-data", platforms: %i[ mswin mswin64 mingw x64_mingw jruby ]

# Reduces boot times through caching; required in config/boot.rb
gem "bootsnap", require: false

gem "graphql", "~> 2.3.0"

group :development, :test do
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
gem "debug", platforms: %i[ mri mswin mswin64 mingw x64_mingw ]
end

group :development do
# Use console on exceptions pages [https://github.com/rails/web-console]
gem "web-console"

# Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler]
# gem "rack-mini-profiler"

# Speed up commands on slow machines / big apps [https://github.com/rails/spring]
# gem "spring"

gem "error_highlight", ">= 0.4.0", platforms: [:ruby]
end

group :test do
# Use system testing [https://guides.rubyonrails.org/testing.html#system-testing]
gem "capybara"
gem "selenium-webdriver"
end
38 changes: 38 additions & 0 deletions examples/subscriptions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Subscriptions example

This example demonstrates stitching subscriptions in a small Rails application. No database required, just bundle-install and try running it:

```shell
cd examples/subscriptions
bundle install
bin/rails s
```

Then visit the GraphiQL client running at [`http://localhost:3000`](http://localhost:3000) and try subscribing:

```graphql
subscription SubscribeToComments {
commentAddedToPost(postId: "1") {
post {
id
title
comments {
id
message
}
}
comment {
id
message
}
}
}
```

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:

```
http://localhost:3000/graphql/event
```

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.
6 changes: 6 additions & 0 deletions examples/subscriptions/Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.

require_relative "config/application"

Rails.application.load_tasks
50 changes: 50 additions & 0 deletions examples/subscriptions/app/channels/graphql_channel.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
class GraphqlChannel < ActionCable::Channel::Base
def subscribed
@subscription_ids = []
end

def execute(data)
result = StitchedSchema.execute(
data["query"],
context: { channel: self },
variables: ensure_hash(data["variables"]),
operation_name: data["operationName"],
)

payload = {
result: result.to_h,
more: result.subscription?,
}

if result.context[:subscription_id]
@subscription_ids << result.context[:subscription_id]
end

transmit(payload)
end

def unsubscribed
@subscription_ids.each { |sid|
SubscriptionsSchema.subscriptions.delete_subscription(sid)
}
end

private

def ensure_hash(ambiguous_param)
case ambiguous_param
when String
if ambiguous_param.present?
ensure_hash(JSON.parse(ambiguous_param))
else
{}
end
when Hash, ActionController::Parameters
ambiguous_param
when nil
{}
else
raise ArgumentError, "Unexpected parameter: #{ambiguous_param}"
end
end
end
44 changes: 44 additions & 0 deletions examples/subscriptions/app/controllers/graphql_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
class GraphqlController < ActionController::Base
skip_before_action :verify_authenticity_token
layout false

def client
end

def execute
result = StitchedSchema.execute(
params[:query],
variables: ensure_hash(params[:variables]),
context: {},
operation_name: params[:operationName],
)

render json: result
end

COMMENTS = ["Great", "Meh", "Terrible"].freeze

def event
comment = Repository.add_comment("1", COMMENTS.sample)
render json: comment
end

private

def ensure_hash(ambiguous_param)
case ambiguous_param
when String
if ambiguous_param.present?
ensure_hash(JSON.parse(ambiguous_param))
else
{}
end
when Hash, ActionController::Parameters
ambiguous_param
when nil
{}
else
raise ArgumentError, "Unexpected parameter: #{ambiguous_param}"
end
end
end
42 changes: 42 additions & 0 deletions examples/subscriptions/app/graphql/entities_schema.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
class EntitiesSchema < GraphQL::Schema
class StitchingResolver < GraphQL::Schema::Directive
graphql_name "stitch"
locations FIELD_DEFINITION
argument :key, String, required: true
argument :arguments, String, required: false
repeatable true
end

class Comment < GraphQL::Schema::Object
field :id, ID, null: false
field :message, String, null: false
end

class Post < GraphQL::Schema::Object
field :id, ID, null: false
field :title, String, null: false
field :comments, [Comment, null: false], null: false
end

class QueryType < GraphQL::Schema::Object
field :posts, [Post, null: true] do
directive StitchingResolver, key: "id"
argument :ids, [ID], required: true
end

def posts(ids:)
ids.map { Repository.post(_1) }
end

field :comments, [Comment, null: true] do
directive StitchingResolver, key: "id"
argument :ids, [ID], required: true
end

def comments(ids:)
ids.map { Repository.comment(_1) }
end
end

query QueryType
end
10 changes: 10 additions & 0 deletions examples/subscriptions/app/graphql/stitched_schema.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
require_relative "../../../../lib/graphql/stitching"

StitchedSchema = GraphQL::Stitching::Client.new(locations: {
entities: {
schema: EntitiesSchema,
},
subscriptions: {
schema: SubscriptionsSchema,
},
})
Loading
Loading