Skip to content

Instantly share code, notes, and snippets.

@abhinaykumar
Last active May 31, 2021 05:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save abhinaykumar/48b7e037e766883fd84304611ce74780 to your computer and use it in GitHub Desktop.
Save abhinaykumar/48b7e037e766883fd84304611ce74780 to your computer and use it in GitHub Desktop.
Setup Graphql subscription with ActionCable Rails
# app/graphql/mutations/add_item_mutation.rb
module Mutations
class AddItemMutation < Mutations::BaseMutation
argument :attributes, Types::ItemAttributes, required: true
field :item, Types::ItemType, null: true
field :errors, [String], null: false
def resolve(attributes:)
check_authentication!
item = Item.new(attributes.merge(user: context[:current_user]))
if item.save
MartianLibrarySchema.subscriptions.trigger("itemAdded", {}, item, scope: context[:current_user].id)
{ item: item }
else
{ errors: item.errors.full_messages }
end
end
end
end
# config/cable.yml
development:
adapter: redis
url: redis://localhost:6379/1
test:
adapter: async
production:
adapter: redis
url: redis://localhost:6379/1
channel_prefix: xyz_production
# app/channels/application_cable/connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
private
def find_verified_user
token = request.params[:token].to_s
email = Base64.decode64(token)
current_user = User.find_by(email: email)
return current_user if current_user
reject_unauthorized_connection
end
end
end
# config/environments/development.rb
# config/environments/production.rb
Rails.application.configure do
:
:
config.action_cable.allowed_request_origins = [ENV['UI_HOST_DOMAIN']] # <- ActionCable needs to know the domain or a pattern of domain to allow the connection
end
{ Ref: https://evilmartians.com/chronicles/graphql-on-rails-3-on-the-way-to-perfection }
(Rails - 5, Graphql-ruby - 1.8.x)
# Gemfile
gem 'redis'
# app/channels/graphql_channel.rb
class GraphqlChannel < ApplicationCable::Channel
def subscribed
@subscription_ids = []
end
def execute(data)
result = execute_query(data)
payload = {
result: result.subscription? ? { data: nil } : result.to_h,
more: result.subscription?
}
# Track the subscription here so we can remove it
# on unsubscribe.
@subscription_ids << context[:subscription_id] if result.context[:subscription_id]
transmit(payload)
end
def unsubscribed
@subscription_ids.each { |sid|
Schema.subscriptions.delete_subscription(sid)
}
end
private
def execute_query(data)
Schema.execute(
query: data['query'],
context: context,
variables: ensure_hash(data['variables']),
operation_name: data['operationName']
)
end
def context
{
current_user_id: current_user.id, # <- `current_user_id` will be used as scope to broadcast message only to this user.
current_user: current_user,
channel: self
}
end
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
# app/graphql/martian_library_schema.rb
class MartianLibrarySchema < GraphQL::Schema
use GraphQL::Subscriptions::ActionCableSubscriptions, redis: Redis.new
mutation(Types::MutationType)
query(Types::QueryType)
subscription(Types::SubscriptionType)
end
# app/graphql/types/subscription_type.rb
# Ref: https://github.com/rmosolgo/graphql-ruby/commit/a329ff3cb84cc83da15b6283cf80dcdd68f49286#diff-d206d5f8891deaca408fcccd141e6193R63
Types::SubscriptionType = GraphQL::ObjectType.define do
field :item_added, Types::ItemType, null: false, description: "An item was added" do
subscription_scope :current_user_id
end
# The return value of the method is not used;
# only the raised error affects the behavior of the subscription.
# If the error is raised, it will be added to the response's `"errors"` key and
# the subscription won't be created.
# TODO: write a policy to authorize. something like
# context[:current_account].can_subscribe_to?(account_id)
def account_status
raise GraphQL::ExecutionError, 'Can not subscribe to this topic' if context[:current_user].blank?
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment