Skip to content

Instantly share code, notes, and snippets.

@theorygeek
Last active June 6, 2025 19:25
Show Gist options
  • Save theorygeek/a1a59a2bf9c59e4b3706ac68d12c8434 to your computer and use it in GitHub Desktop.
Save theorygeek/a1a59a2bf9c59e4b3706ac68d12c8434 to your computer and use it in GitHub Desktop.

Revisions

  1. theorygeek revised this gist Apr 26, 2017. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions post_type.rb
    Original file line number Diff line number Diff line change
    @@ -2,10 +2,10 @@
    name "Post"

    field :comments, !types[!CommentType] do
    preload :comments
    preload comments: :author
    resolve -> (post, args, ctx) { post.comments }
    end

    # Or you can use the more terse syntax:
    field :comments, !types[!CommentType], preload: :comments, property: :comments
    field :comments, !types[!CommentType], preload: { comments: :author }, property: :comments
    end
  2. theorygeek revised this gist Apr 26, 2017. 1 changed file with 11 additions and 0 deletions.
    11 changes: 11 additions & 0 deletions post_type.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,11 @@
    PostType = GraphQL::ObjectType.define do
    name "Post"

    field :comments, !types[!CommentType] do
    preload :comments
    resolve -> (post, args, ctx) { post.comments }
    end

    # Or you can use the more terse syntax:
    field :comments, !types[!CommentType], preload: :comments, property: :comments
    end
  3. theorygeek created this gist Apr 26, 2017.
    23 changes: 23 additions & 0 deletions association_loader.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,23 @@
    # frozen_string_literal: true
    class AssociationLoader < GraphQL::Batch::Loader
    attr_reader :klass, :association

    def initialize(klass, association)
    raise ArgumentError, "association to load must be a symbol (got #{association.inspect})" unless association.is_a?(Symbol)
    raise ArgumentError, "cannot load associations for class #{klass.name}" unless klass < ActiveRecord::Base
    raise TypeError, "association #{association} does not exist on #{klass.name}" unless klass.reflect_on_association(association)

    @klass = klass
    @association = association
    end

    def load(model)
    raise TypeError, "loader for #{klass.name} can't load associations for objects of type #{model.class.name}" unless model.is_a?(klass)
    model.association(@association).loaded? ? Promise.resolve(model) : super
    end

    def perform(models)
    ActiveRecord::Associations::Preloader.new.preload(models, association)
    models.each { |m| fulfill(m, m) }
    end
    end
    55 changes: 55 additions & 0 deletions association_preload_instrumentation.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,55 @@
    # frozen_string_literal: true
    class AssociationPreloadInstrumentation
    def instrument(_type, field)
    return field unless field.metadata.include?(:preload)

    old_resolver = field.resolve_proc
    new_resolver = -> (object, args, ctx) do
    preload(object, field.metadata[:preload]).then do
    old_resolver.call(object, args, ctx)
    end
    end

    field.redefine do
    resolve(new_resolver)
    end
    end

    private

    def preload(object, associations)
    if associations.is_a?(Symbol)
    preload_single_association(object, associations)
    else
    promises = []

    Array.wrap(associations).each do |value|
    case value
    when Symbol
    promises << preload_single_association(object, value)
    when Array
    value.each { |sub_value| promises << preload(object, sub_value) }
    when Hash
    value.each do |key, sub_value|
    promises << preload_single_association(object, key).then do
    next_value = object.public_send(key)
    case next_value
    when ActiveRecord::Base
    preload(next_value, sub_value)
    else
    Promise.all(Array.wrap(next_value).map { |next_model| preload(next_model, sub_value) })
    end
    end
    end
    end
    end

    Promise.all(promises)
    end
    end

    def preload_single_association(object, association)
    return Promise.resolve(object) if object.association(association).loaded?
    AssociationLoader.for(object.class, association).load(object)
    end
    end
    8 changes: 8 additions & 0 deletions initializer.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,8 @@
    # frozen_string_literal: true
    # This goes in a Rails initializer, or some other code that runs before your schema/types are required
    GraphQL::Field.accepts_definitions(
    preload: -> (type, *args) do
    type.metadata[:preload] ||= []
    type.metadata[:preload].concat(args)
    end
    )
    5 changes: 5 additions & 0 deletions schema.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,5 @@
    # frozen_string_literal: true
    Schema = GraphQL::Schema.define do
    # configuration stuff
    instrument(:field, AssociationPreloadInstrumentation.new)
    end