GraphQL::Sources

Installation

Install the gem and add to the application's Gemfile by executing:

$ bundle add graphql-sources

If bundler is not being used to manage dependencies, install the gem by executing:

$ gem install graphql-sources

The GraphQL::Dataloader plugin must be installed in the schema:

class AppSchema < GraphQL::Schema
  use GraphQL::Dataloader
  # ...
end

Usage

Loading belongs_to / has_many Associations

class Purchase < ActiveRecord::Base
  belongs_to :customer
end
class Customer < ActiveRecord::Base
  has_many :purchases
end
class PurchaseType < GraphQL::Schema::Object
  field :customer, CustomerType, null: false

  # @return [Customer]
  def customer
    # SELECT * FROM "customers" WHERE "customers"."id" IN (...)
    dataloader
      .with(GraphQL::Sources::ActiveRecordAssociation, :customer)
      .load(object)
  end
end
class CustomerType < GraphQL::Schema::Object
  field :purchases, [PurchaseType], null: false

  def purchases
    # SELECT * FROM "customers" WHERE "customers"."id" IN (...)
    dataloader
      .with(GraphQL::Sources::ActiveRecordAssociation, :purchases)
      .load(object)
  end
end

Loading belongs_to / has_one Associations

class Profile < ActiveRecord::Base
  belongs_to :user
end
class User < ActiveRecord::Base
  has_one :profile
end
class ProfileType < GraphQL::Schema::Object
  field :user, [UserType], null: false

  # @return [User]
  def user
    # SELECT * FROM "users" WHERE "users"."id" IN (...)
    dataloader
      .with(GraphQL::Sources::ActiveRecordAssociation, :user)
      .load(object)
  end
end
class UserType < GraphQL::Schema::Object
  field :profile, [ProfileType], null: false

  # @return [Profile]
  def profile
      # SELECT * FROM "profiles" WHERE "profiles"."id" IN (...)
    dataloader
      .with(GraphQL::Sources::ActiveRecordAssociation, :profile)
      .load(object)
  end
end

Loading has_and_belongs_to_many Associations

class Student
  has_and_belongs_to_many :courses
end
class Course
  has_and_belongs_to_many :students
end
class StudentType < GraphQL::Schema::Object
  field :courses, [CourseType], null: false

  # @return [Array<Course>]
  def courses
    # SELECT * FROM "courses_students" WHERE "courses_students"."student_id" = IN (...)
    # SELECT * FROM "courses" WHERE "courses"."id" IN (...)
    dataloader
      .with(GraphQL::Sources::ActiveRecordAssociation, :courses)
      .load(object)
  end
end
class CourseType < GraphQL::Schema::Object
  field :students, [StudentType], null: false

  # @return [Array<Student>]
  def students
    # SELECT * FROM "courses_students" WHERE "courses_students"."course_id" = IN (...)
    # SELECT * FROM "students" WHERE "students"."id" IN (...)
    dataloader
      .with(GraphQL::Sources::ActiveRecordAssociation, :students)
      .load(object)
  end
end

Loading has_one_attached Associations

class User
  has_one_attached :photo
end
class UserType < GraphQL::Schema::Object
  field :avatar, AttachedType, null: false

  # @return [ActiveStorage::Attachment]
  def avatar
    # SELECT "active_storage_attachments".*
    # FROM "active_storage_attachments"
    # WHERE "active_storage_attachments"."name" = 'avatar'
    #   AND "active_storage_attachments"."record_type" = 'User'
    #   AND "active_storage_attachments"."record_id" IN (...)
    dataloader
      .with(GraphQL::Sources::ActiveStorageHasOneAttached, :avatar)
      .load(object)
  end
end

Loading has_many_attached Associations

class User
  has_many_attached :photos
end
class UserType < GraphQL::Schema::Object
  field :photos, [AttachedType], null: false

  # @return [Array<ActiveStorage::Attachment>]
  def photos
    # SELECT "active_storage_attachments".*
    # FROM "active_storage_attachments"
    # WHERE "active_storage_attachments"."name" = 'photos'
    #   AND "active_storage_attachments"."record_type" = 'User'
    #   AND "active_storage_attachments"."record_id" IN (...)
    dataloader
      .with(GraphQL::Sources::ActiveStorageHasManyAttached, :photos)
      .load(object)
  end
end

Loading ActiveRecord Object / ActiveRecord Collection

class Event < ActiveRecord::Base
  belongs_to :device
end
class Device < ActiveRecord::Base
  has_many :events
end
class EventType < GraphQL::Schema::Object
  field :device, DeviceType, null: false

  # @return [Device]
  def device
    # SELECT * FROM "devices" WHERE "devices"."id" IN (...)
    dataloader
      .with(GraphQL::Sources::ActiveRecordObject, ::Device, key: :id)
      .load(object.device_id)
  end
end
class DeviceType < GraphQL::Schema::Object
  field :events, [EventType], null: false

  def events
    # SELECT * FROM "events" WHERE "events"."device_id" IN (...)
    dataloader
      .with(GraphQL::Sources::ActiveRecordCollection, ::Event, key: :device_id)
      .load(object)
  end
end

Loading Counts

class Like
  belongs_to :post
end
class Post
  has_many :likes
end
class PostType < GraphQL::Schema::Object
  field :likes, Integer, null: false

  def likes
    dataloader
      .with(GraphQL::Sources::ActiveRecordCount, ::Like, key: :post_id)
      .load(object.id)
  end
end
SELECT "likes"."post_id", COUNT(*)
FROM "likes"
WHERE "likes"."post_id" IN (1, 2, 3, ...)
GROUP BY "likes"."post_id"

Loading Exists

class User
  has_many :purchases
end
class Purchase
  belongs_to :product
  belongs_to :user
end
class Product
  has_many :purchases
end
class ProductType
  field :purchased, Boolean, null: false

  def purchased
    dataloader
      .with(GraphQL::Sources::ActiveRecordExists, ::Purchase.where(user: context.user), key: :product_id)
      .load(object.id)
  end
end

Loading with Rails.cache

class UserType < GraphQL::Schema::Object
  field :location, String, null: false

  def location
    dataloader
      .with(GraphQL::Sources::RailsCache)
      .load(key: "geocode:#{object.latest_ip}", fallback: -> { Geocode.for(object.latest_ip) })
  end
end

Status

CircleCI Maintainability Test Coverage

License

The gem is available as open source under the terms of the MIT License.