Last active
March 6, 2020 15:36
-
-
Save martinliptak/70d9ceae47535951a0de7128c4888329 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module Loaders | |
class AssociationLoader < GraphQL::Batch::Loader | |
def initialize( | |
user, | |
model, | |
association, | |
apply: [], | |
aggregate: nil, | |
aggregate_default_value: 0 | |
) | |
@user = user | |
@model = model | |
@association = association | |
@apply = apply | |
@aggregate = aggregate | |
@aggregate_default_value = aggregate_default_value | |
end | |
def perform(keys) | |
# Loading article -> tags, target class will be Tag. | |
unless @model.reflect_on_association(@association) | |
raise( | |
ArgumentError, | |
"Association #{@association} doesn't exist on #{@model.name}.", | |
) | |
end | |
target_class = @model.reflect_on_association(@association).klass | |
# Why do we need inverse associations? | |
# | |
# Imagine loading article -> tags. We could do Article.joins(:tags), but this | |
# 1) needlessly loads articles, which have already been | |
# loaded by the parent GraphQL query | |
# 2) doesn't apply LIMIT to tags | |
# | |
# Using inverse association, we can do Tag.joins(:article) avoiding both issues. | |
# | |
unless @model.reflect_on_association(@association).inverse_of | |
raise( | |
ArgumentError, | |
"Association #{@association} on #{@model.name} doesn't have an inverse association.", | |
) | |
end | |
inverse_association = @model.reflect_on_association(@association).inverse_of.name | |
# Make sure all loaded records are authorized | |
# (either with simple scopes on models or using a library like Pundit). | |
scope = target_class.authorized_scope_for(@user) | |
# Now doing Tag.joins(:articles).where(articles: { id: @model.id }) | |
scope = scope.joins(inverse_association) | |
scope = scope.where(@model.arel_table[:id].in(keys)) | |
# Additional named scopes or where conditions. | |
@apply.each do |method_and_params| | |
scope = scope.send(*method_and_params) | |
end | |
if @aggregate | |
# Group by Article.id | |
scope = scope.group(@model.arel_table[:id]) | |
# For example, `count` aggregates tag count per article. | |
scope = scope.send(*@aggregate) | |
fulfill_aggregation(keys, scope) | |
else | |
# Select Article.id as __loader_key | |
scope = scope.select( | |
@model.arel_table[:id].as("__loader_key"), | |
target_class.arel_table[Arel.star] | |
) | |
# Default limit for security | |
scope = scope.limit(GalaxycodrSchema.default_max_page_size) | |
if multiple_results_per_key? | |
fulfill_multiple_results(keys, scope) | |
else | |
fulfill_single_result(keys, scope) | |
end | |
end | |
end | |
private | |
def multiple_results_per_key? | |
@association.to_s == @association.to_s.pluralize.to_s | |
end | |
def fulfill_aggregation(keys, scope) | |
# Fulfill results | |
scope.each do |key, result| | |
fulfill(key, result) | |
end | |
# Default value is 0 or other value the user provides. | |
keys.each do |key| | |
next if fulfilled?(key) | |
fulfill(key, @aggregate_default_value) | |
end | |
end | |
def fulfill_multiple_results(keys, scope) | |
# Group by __loader_key and fulfill keys | |
scope | |
.group_by { |record| record[:__loader_key] } | |
.each { |key, records| fulfill(key, records) } | |
# Default value is an empty array | |
keys.each { |key| fulfill(key, []) unless fulfilled?(key) } | |
end | |
def fulfill_single_result(keys, scope) | |
scope | |
.each { |record| fulfill(record[:__loader_key], record) } | |
# Default value is nil | |
keys.each { |key| fulfill(key, nil) unless fulfilled?(key) } | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment