Skip to content

Commit 0b0e182

Browse files
authored
Merge pull request #997 from code0-tech/357-setup-actioncable-for-graphql-subscriptions
Setup ActionCable for GraphQL subscriptions
2 parents 6d319d7 + 4c208eb commit 0b0e182

18 files changed

Lines changed: 369 additions & 8 deletions

File tree

app/channels/application_cable/channel.rb

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,45 @@
22

33
module ApplicationCable
44
class Channel < ActionCable::Channel::Base
5+
def subscribe_to_channel
6+
with_context { super }
7+
end
8+
9+
def perform_action(data)
10+
with_context { super }
11+
end
12+
13+
protected
14+
15+
def find_authentication(authorization)
16+
return Sagittarius::Authentication.new(:none, nil) if authorization.blank?
17+
18+
token_type, token = authorization.split(' ', 2)
19+
20+
create_authentication(token_type, token)
21+
end
22+
23+
def create_authentication(token_type, token)
24+
case token_type
25+
when 'Session'
26+
Sagittarius::Authentication.new(:session, UserSession.find_by(token: token, active: true))
27+
else
28+
Sagittarius::Authentication.new(:invalid, nil)
29+
end
30+
end
31+
32+
def with_context(&block)
33+
Code0::ZeroTrack::Context.with_context(
34+
application: 'cable',
35+
ip_address: request_ip,
36+
&block
37+
)
38+
end
39+
40+
def request_ip
41+
return unless connection.respond_to?(:env)
42+
43+
::Rack::Request.new(connection.env).ip
44+
end
545
end
646
end

app/channels/graphql_channel.rb

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# frozen_string_literal: true
2+
3+
class GraphqlChannel < ApplicationCable::Channel
4+
periodically :verify_authentication, every: 30.seconds
5+
6+
def subscribed
7+
@token = params[:token]
8+
@subscription_ids = []
9+
10+
verify_authentication
11+
end
12+
13+
def execute(data)
14+
result = SagittariusSchema.execute(
15+
query: data['query'],
16+
context: {
17+
current_authentication: find_authentication(@token),
18+
visibility_profile: :execution,
19+
channel: self,
20+
},
21+
variables: data['variables'],
22+
operation_name: data['operationName']
23+
)
24+
25+
@subscription_ids << result.context[:subscription_id] if result.context[:subscription_id]
26+
27+
transmit({ result: result.to_h, more: result.subscription? })
28+
end
29+
30+
def unsubscribed
31+
@subscription_ids.each do |sid|
32+
SagittariusSchema.subscriptions.delete_subscription(sid)
33+
end
34+
end
35+
36+
private
37+
38+
def verify_authentication
39+
with_context do
40+
authentication = find_authentication(@token)
41+
return unless authentication.invalid? || authentication.none?
42+
43+
@subscription_ids.each { |sid| SagittariusSchema.subscriptions.delete_subscription(sid) }
44+
reject
45+
end
46+
end
47+
end

app/graphql/sagittarius_schema.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,18 @@
44
class SagittariusSchema < GraphQL::Schema
55
mutation(Types::MutationType)
66
query(Types::QueryType)
7+
subscription(Types::SubscriptionType)
78

89
default_max_page_size 50
910
max_depth 20
1011
connections.add(ActiveRecord::Relation, Sagittarius::Graphql::StableConnection)
1112

13+
use GraphQL::Subscriptions::ActionCableSubscriptions
14+
1215
# For batch-loading (see https://graphql-ruby.org/dataloader/overview.html)
1316
use GraphQL::Dataloader
1417

15-
use GraphQL::Schema::Visibility, profiles: {
18+
use GraphQL::Schema::Visibility, preload: true, profiles: {
1619
execution: {},
1720
types: {},
1821
docs: {},
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# frozen_string_literal: true
2+
3+
module Subscriptions
4+
class BaseSubscription < GraphQL::Schema::Subscription
5+
argument_class Types::BaseArgument
6+
field_class Types::BaseField
7+
object_class Types::BaseObject
8+
9+
def current_authentication
10+
context[:current_authentication]
11+
end
12+
13+
def self.generate_payload_type
14+
result = super
15+
result.graphql_name("#{graphql_name}SubscriptionPayload")
16+
result
17+
end
18+
end
19+
end

app/graphql/subscriptions/echo.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# frozen_string_literal: true
2+
3+
module Subscriptions
4+
class Echo < BaseSubscription
5+
description <<~DOC
6+
A subscription that does not perform any real updates.
7+
8+
This is expected to be used for testing of endpoints, to verify
9+
that a user has subscription access.
10+
DOC
11+
12+
argument :message,
13+
type: GraphQL::Types::String,
14+
required: false,
15+
description: 'Message to return to the user.'
16+
17+
field :message,
18+
type: GraphQL::Types::String,
19+
null: true,
20+
description: 'Message returned to the user.'
21+
22+
def subscribe(message: nil)
23+
{ message: message }
24+
end
25+
26+
def update(*)
27+
{ message: object[:message] }
28+
end
29+
end
30+
end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# frozen_string_literal: true
2+
3+
module Types
4+
class SubscriptionType < Types::BaseObject
5+
description 'Root subscription type'
6+
7+
include Sagittarius::Graphql::MountSubscription
8+
9+
mount_subscription Subscriptions::Echo
10+
end
11+
end

config.cable.ru

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# frozen_string_literal: true
2+
3+
require_relative 'config/environment'
4+
Rails.application.eager_load!
5+
6+
run ActionCable.server

config/cable.yml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
development:
2-
adapter: async
2+
adapter: postgresql
33

44
test:
55
adapter: test
66

77
production:
8-
adapter: redis
9-
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
10-
channel_prefix: sagittarius_production
8+
adapter: postgresql
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# frozen_string_literal: true
2+
3+
Rails.application.configure do
4+
config.action_cable.disable_request_forgery_protection = true
5+
end

config/puma.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
# are invoked here are part of Puma's configuration DSL. For more information
55
# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html.
66

7+
require_relative 'environment'
8+
79
# Puma can serve each request in a thread from an internal thread pool.
810
# The `threads` method setting takes two numbers: a minimum and maximum.
911
# Any libraries that use thread pools should be configured to match

0 commit comments

Comments
 (0)