Skip to content

Instantly share code, notes, and snippets.

@RStankov
Last active June 28, 2020 12:18
Show Gist options
  • Save RStankov/48070003a31d71a66f57a237e27d5865 to your computer and use it in GitHub Desktop.
Save RStankov/48070003a31d71a66f57a237e27d5865 to your computer and use it in GitHub Desktop.
# Note(rstankov):
#
# Preload associations
#
# **Don't use for `connection`s, only for `field` attributes**
#
# supported definitions:
# - [x] :assoc
# - [x] { assoc: :assoc }
# - [x] { assoc: { assoc: :assoc } }
# - [x] [:assoc, :assoc]
# - [x] { assoc: [ :assoc, :assoc ] }
#
# not supported: (because their response is not obvious)
# - [ ] [ :assoc, { assoc: :assoc } }
# - [ ] [ { assoc: :assoc }, { assoc: :assoc } }
class Graph::Resolvers::AssociationResolver < GraphQL::Function
def initialize(preload, &block)
@preload = normalize_preloads(preload)
@handler = block || DefaultHandler
end
def call(obj, _args = nil, _ctx = nil)
return unless obj.present?
next_step preload.dup, obj, obj
end
private
def next_step(items, obj, previous_assoc)
preload_associations(previous_assoc, items.shift).then do |assoc|
if items.empty?
handle obj, assoc
else
next_step items, obj, assoc
end
end
end
def preload_associations(model, associations)
if associations.is_a? Array
Promise.all(associations.map { |name| preload_association(model, name) })
else
preload_association(model, associations)
end
end
def preload_association(model, association_name)
AssociationLoader.for(model.class, association_name).load(model)
end
def normalize_preloads(preloads)
if preloads.is_a? Hash
keys = preloads.keys
raise NotSupported, 'only one nested association supported currently' unless keys.size == 1
first_key = keys.first
[first_key] + normalize_preloads(preloads[first_key])
else
[preloads]
end
end
def handle(obj, assoc)
if handler.arity == 2
handler.call assoc, obj
else
handler.call assoc
end
end
attr_reader :preload, :handler
class NotSupported < StandardError; end
module DefaultHandler
extend self
def arity
1
end
def call(assoc)
assoc
end
end
class AssociationLoader < GraphQL::Batch::Loader
def initialize(model, association)
@model = model
@association = association
end
def load(record)
raise TypeError, "#{ @model } loader can't load association for #{ record.class }" unless record.is_a?(@model)
return Promise.resolve(read_association(record)) if association_loaded?(record)
super
end
# We want to load the associations on all records, even if they have the same id
def cache_key(record)
record.object_id
end
def perform(records)
::ActiveRecord::Associations::Preloader.new.preload(records, association)
records.each do |record|
fulfill(record, read_association(record))
end
end
private
attr_reader :model, :association
def read_association(record)
record.public_send(association)
end
def association_loaded?(record)
record.association(association).loaded?
end
end
end
Graph::Types::PostType = GraphQL::ObjectType.define do
# ... other stuff
# belogns to
field :user, Graph::Types::UserType, function: Graph::Resolvers::AssociationResolver.new(:user)
# has many
field :topics, Graph::Types::UpcomingPageType, function: Graph::Resolvers::AssociationResolver.new(:topics)
# belongs_to -> belongs_to -> custom resolver
field :shortcode, !types.String, function: Graph::Resolvers::AssociationResolver.new(product: :primary_link) do |link|
ShortcodeExtract.call(link)
end
# this can be:
# Graph::Resolvers::AssociationResolver.new({ product: :primary_link}, Shortcode)
# Unfortunally, `AssociationResolver` is not usable with scopes and Relay `connection` interface
connection :alternatives, Graph::Types::PostType.connection_type do
resolve ->(post, _args, _ctx) { post.alternatives.by_credible_votes }
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment