diff --git a/CHANGELOG.md b/CHANGELOG.md index ddbd58f..ac0fdbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## master +- [PR#130](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/130) Dataloader support ([@DmitryTsepelev][]) - [PR#125](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/125) Introduce cache lookup instrumentation hook ([@danielhartnell][]) ## 1.20.5 (2024-11-02) diff --git a/lib/graphql/fragment_cache/object_helpers.rb b/lib/graphql/fragment_cache/object_helpers.rb index 0dc41b2..b2c4f55 100644 --- a/lib/graphql/fragment_cache/object_helpers.rb +++ b/lib/graphql/fragment_cache/object_helpers.rb @@ -48,6 +48,11 @@ def cache_fragment(object_to_cache = NO_OBJECT, **options, &block) fragment = Fragment.new(context_to_use, **options) + if options.delete(:dataloader) + object_to_cache = block.call + block = nil + end + GraphQL::FragmentCache::Schema::LazyCacheResolver.new(fragment, context_to_use, object_to_cache, &block) end end diff --git a/spec/graphql/fragment_cache/object_helpers_spec.rb b/spec/graphql/fragment_cache/object_helpers_spec.rb index c5b9161..c2f2e19 100644 --- a/spec/graphql/fragment_cache/object_helpers_spec.rb +++ b/spec/graphql/fragment_cache/object_helpers_spec.rb @@ -777,6 +777,59 @@ def post(id:, expires_in: nil) end end + describe "caching fields with dataloader" do + let(:query) do + <<~GQL + query GetPosts { + posts { + id + dataloaderCachedAuthor { + name + } + } + } + GQL + end + + let(:schema) do + build_schema do + use GraphQL::Dataloader + query(Types::Query) + end + end + + let(:user1) { User.new(id: 1, name: "User #1") } + let(:user2) { User.new(id: 2, name: "User #2") } + + let!(:post1) { Post.create(id: 1, title: "object test 1", author: user1) } + let!(:post2) { Post.create(id: 2, title: "object test 2", author: user2) } + + before do + allow(User).to receive(:find_by_post_ids).and_call_original + + # warmup cache + execute_query + expect(User).to have_received(:find_by_post_ids).with([post1.id, post2.id]) + + # make objects dirty + user1.name = "User #1 new" + user2.name = "User #2 new" + end + + it "returns cached results" do + expect(execute_query.dig("data", "posts")).to eq([ + { + "id" => "1", + "dataloaderCachedAuthor" => {"name" => "User #1"} + }, + { + "id" => "2", + "dataloaderCachedAuthor" => {"name" => "User #2"} + } + ]) + end + end + describe "conditional caching" do let(:schema) do field_resolver = resolver diff --git a/spec/support/models/user.rb b/spec/support/models/user.rb index 27b84d4..cafdd4b 100644 --- a/spec/support/models/user.rb +++ b/spec/support/models/user.rb @@ -4,6 +4,12 @@ class User attr_reader :id attr_accessor :name + class << self + def find_by_post_ids(post_ids) + post_ids.map { |id| Post.find(id).author } + end + end + def initialize(id:, name:) @id = id @name = name diff --git a/spec/support/test_schema.rb b/spec/support/test_schema.rb index cffd253..7b9c805 100644 --- a/spec/support/test_schema.rb +++ b/spec/support/test_schema.rb @@ -7,6 +7,12 @@ def perform(posts) end end +class AuthorDataloaderSource < GraphQL::Dataloader::Source + def fetch(post_ids) + User.find_by_post_ids(post_ids) + end +end + module Types class Base < GraphQL::Schema::Object include GraphQL::FragmentCache::Object @@ -41,6 +47,7 @@ class Post < Base field :cached_author, User, null: false field :batched_cached_author, User, null: false field :cached_author_inside_batch, User, null: false + field :dataloader_cached_author, User, null: false field :meta, String, null: true @@ -60,6 +67,12 @@ def cached_author_inside_batch cache_fragment(author, context: context) end end + + def dataloader_cached_author + cache_fragment(dataloader: true) do + dataloader.with(AuthorDataloaderSource).load(object.id) + end + end end class PostInput < GraphQL::Schema::InputObject