Skip to content

Commit

Permalink
Support has_and_belongs_to_many
Browse files Browse the repository at this point in the history
This support likely allows any association.
  • Loading branch information
ksylvest committed Oct 27, 2024
1 parent 52fdfa8 commit b9b099a
Show file tree
Hide file tree
Showing 6 changed files with 197 additions and 2 deletions.
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
graphql-sources (1.4.0)
graphql-sources (1.5.0)
graphql
rails
zeitwerk
Expand Down
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,50 @@ end

## Usage

### Loading `has_and_belongs_to_many` Associations

```ruby
class Student
has_and_belongs_to_many :courses
end
```

```ruby
class Course
has_and_belongs_to_many :students
end
```

```ruby
class StudentType < GraphQL::Schema::Object
field :courses, [CourseType], null: false

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

```ruby
class CourseType < GraphQL::Schema::Object
field :students, [StudentType], null: false

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

### Loading `belongs_to` Associations

```ruby
Expand Down
127 changes: 127 additions & 0 deletions lib/graphql/sources/active_record_association.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# frozen_string_literal: true

module GraphQL
module Sources
# A class for loading an ActiveRecord association.
#
# @example `has_and_belongs_to_many`
#
# 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 "courses_students".* FROM "courses_students" WHERE "courses_students"."student_id" = IN (...)
# # SELECT "courses".* 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 "courses_students".* FROM "courses_students" WHERE "courses_students"."course_id" = IN (...)
# # SELECT "students".* FROM "students" WHERE "students"."id" IN (...)
# dataloader
# .with(GraphQL::Sources::ActiveRecordAssociation, :students)
# .load(object)
# end
# end
#
#
# @example `has_many` / `belongs_to`
#
# class User
# has_many :comments
# end
#
# class Comment
# belongs_to :user
# end
#
# class UserType < GraphQL::Schema::Object
# field :comments, [CommentType], null: false
#
# # @return [Array<Comment>]
# def comments
# # SELECT "comments".* FROM "comments" WHERE "comments"."user_id" = IN (...)
# dataloader
# .with(GraphQL::Sources::ActiveRecordAssociation, :comments)
# .load(object)
# end
# end
#
# class CommentType < GraphQL::Schema::Object
# field :user, UserType, null: false
#
# # @return [User]
# def user
# # SELECT "users".* FROM "users" WHERE "users"."id" IN (...)
# dataloader
# .with(GraphQL::Sources::ActiveRecordAssociation, :user)
# .load(object)
# end
# end
#
# @example `has_one` / `belongs_to`
#
# class User
# has_one :profile
# end
#
# class Profile
# belongs_to :user
# end
#
# class UserType < GraphQL::Schema::Object
# field :profile, ProfileType, null: false
#
# # @return [Profile]
# def profile
# # SELECT "profiles".* FROM "profiles" WHERE "profiles"."user_id" = IN (...)
# dataloader
# .with(GraphQL::Sources::ActiveRecordAssociation, :profile)
# .load(object)
# end
# end
#
# class ProfileType < GraphQL::Schema::Object
# field :user, UserType, null: false
#
# # @return [User]
# def user
# # SELECT "users".* FROM "users" WHERE "users"."id" IN (...)
# dataloader
# .with(GraphQL::Sources::ActiveRecordAssociation, :user)
# .load(object)
# end
# end
class ActiveRecordAssociation < GraphQL::Dataloader::Source
# @param association [Symbol] an association to use for loading (e.g. :user)
def initialize(association)
super()
@association = association
end

# @param records [Array<ActiveRecord::Base>] an array of records
def fetch(records)
preloader = ActiveRecord::Associations::Preloader.new(records: records, associations: [@association])
preloader.call

records.map { |record| record.association(@association).target }
end
end
end
end
2 changes: 1 addition & 1 deletion lib/graphql/sources/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

module GraphQL
module Sources
VERSION = '1.4.0'
VERSION = '1.5.0'
end
end
1 change: 1 addition & 0 deletions spec/dummy/app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

class User < ApplicationRecord
has_one :profile
has_many :comments
has_one_attached :avatar
has_many_attached :photos
end
23 changes: 23 additions & 0 deletions spec/graphql/sources/active_record_association_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe GraphQL::Sources::ActiveRecordAssociation do
describe '#fetch' do
subject(:result) do
GraphQL::Dataloader.with_dataloading do |dataloader|
dataloader
.with(described_class, :comments)
.request(user)
.load
end
end

let!(:user) { create(:user) }
let!(:comments) { create_pair(:comment, user: user) }

it 'loads many records' do
expect(result).to match_array(comments)
end
end
end

0 comments on commit b9b099a

Please sign in to comment.