From cf6dd67af2df0c296f1d43012eb98b3743c8c71c Mon Sep 17 00:00:00 2001 From: Nikolay Sverchkov Date: Thu, 19 Dec 2019 13:22:55 +0300 Subject: [PATCH 1/8] Add specs for Sequel connection case --- spec/features/connections_spec.rb | 35 +++++++++++++++++++++++++++++++ spec/spec_helper.rb | 4 ++++ 2 files changed, 39 insertions(+) diff --git a/spec/features/connections_spec.rb b/spec/features/connections_spec.rb index 1a812f6..4e2a181 100644 --- a/spec/features/connections_spec.rb +++ b/spec/features/connections_spec.rb @@ -5,6 +5,17 @@ def execute(query, context = {}) end RSpec.describe 'caching connection fields' do + class StubLogger < Logger + def initialize + @strio = StringIO.new + super(@strio) + end + + def messages + @strio.string + end + end + let(:query) do %Q{ { @@ -21,10 +32,34 @@ def execute(query, context = {}) } end + let(:sql_logger) do + StubLogger.new.tap do |logger| + logger.formatter = proc do |_severity, _datetime, _progname, msg| + raw_sql = msg.match(/\(.*\)\s(?.*)/)["sql"] + + "#{raw_sql}\n" + end + end + end + + before { DB.logger = sql_logger } + it 'produces the same result on miss or hit' do cold_results = execute(query) warm_results = execute(query) expect(cold_results).to eq warm_results end + + it 'calls sql engine only one time per cached field' do + 5.times { execute(query) } + + expect(sql_logger.messages).to eq( + <<~SQL + SELECT * FROM `customers` ORDER BY `id` DESC LIMIT 1 + SELECT * FROM `customers` WHERE `id` = '1' + SELECT * FROM `orders` WHERE (`orders`.`customer_id` = 1) + SQL + ) + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 11673fa..fbcc3aa 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -29,6 +29,10 @@ DB.logger = GraphQL::Cache.logger end + config.before(:each) do + GraphQL::Cache.cache.clear + end + # required after GraphQL::Cache initialization because dev # schema uses cache and logger objects from it. require_relative '../test_schema' From 837bb750bc5be83b13aa4a2630ab73d7e3a377ab Mon Sep 17 00:00:00 2001 From: Nikolay Sverchkov Date: Thu, 19 Dec 2019 16:18:38 +0300 Subject: [PATCH 2/8] Implement ActiveRecord test polygon; Reproduce bug with association cache --- Gemfile.lock | 22 +++++- graphql-cache.gemspec | 1 + spec/features/connections_spec.rb | 76 +++++++++++++++------ spec/spec_helper.rb | 2 + spec/support/test_cache.rb | 5 +- test_schema/active_record/factories.rb | 18 +++++ test_schema/active_record/graphql_schema.rb | 23 +++++++ test_schema/active_record/init.rb | 9 +++ test_schema/active_record/models.rb | 9 +++ test_schema/active_record/schema.rb | 20 ++++++ test_schema/factories.rb | 2 +- 11 files changed, 162 insertions(+), 25 deletions(-) create mode 100644 test_schema/active_record/factories.rb create mode 100644 test_schema/active_record/graphql_schema.rb create mode 100644 test_schema/active_record/init.rb create mode 100644 test_schema/active_record/models.rb create mode 100644 test_schema/active_record/schema.rb diff --git a/Gemfile.lock b/Gemfile.lock index b16d4f8..a90833e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,6 +7,17 @@ PATH GEM remote: https://rubygems.org/ specs: + activemodel (6.0.2.1) + activesupport (= 6.0.2.1) + activerecord (6.0.2.1) + activemodel (= 6.0.2.1) + activesupport (= 6.0.2.1) + activesupport (6.0.2.1) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 0.7, < 2) + minitest (~> 5.1) + tzinfo (~> 1.1) + zeitwerk (~> 2.2) appraisal (2.2.0) bundler rake @@ -14,13 +25,16 @@ GEM codeclimate-test-reporter (1.0.9) simplecov (<= 0.13) coderay (1.1.2) + concurrent-ruby (1.1.5) diff-lcs (1.3) docile (1.1.5) graphql (1.9.3) + i18n (1.7.0) + concurrent-ruby (~> 1.0) json (2.2.0) method_source (0.9.2) mini_cache (1.1.0) - promise.rb (0.7.4) + minitest (5.13.0) pry (0.12.2) coderay (~> 1.1.0) method_source (~> 0.9.0) @@ -46,16 +60,20 @@ GEM simplecov-html (0.10.2) sqlite3 (1.4.0) thor (0.20.3) + thread_safe (0.3.6) + tzinfo (1.2.5) + thread_safe (~> 0.1) + zeitwerk (2.2.2) PLATFORMS ruby DEPENDENCIES + activerecord appraisal codeclimate-test-reporter graphql-cache! mini_cache - promise.rb pry rake (~> 10.0) rspec (~> 3.0) diff --git a/graphql-cache.gemspec b/graphql-cache.gemspec index 380240c..6857ba5 100644 --- a/graphql-cache.gemspec +++ b/graphql-cache.gemspec @@ -28,6 +28,7 @@ Gem::Specification.new do |s| s.add_development_dependency 'rake', '~> 10.0' s.add_development_dependency 'rspec', '~> 3.0' s.add_development_dependency 'sequel' + s.add_development_dependency 'activerecord' s.add_development_dependency 'simplecov' s.add_development_dependency 'sqlite3' diff --git a/spec/features/connections_spec.rb b/spec/features/connections_spec.rb index 4e2a181..9e529a9 100644 --- a/spec/features/connections_spec.rb +++ b/spec/features/connections_spec.rb @@ -1,9 +1,5 @@ require 'spec_helper' -def execute(query, context = {}) - CacheSchema.execute(query, context: context) -end - RSpec.describe 'caching connection fields' do class StubLogger < Logger def initialize @@ -19,7 +15,7 @@ def messages let(:query) do %Q{ { - customer(id: #{Customer.last.id}) { + customer(id: #{customer.id}) { orders { edges { node { @@ -35,31 +31,71 @@ def messages let(:sql_logger) do StubLogger.new.tap do |logger| logger.formatter = proc do |_severity, _datetime, _progname, msg| - raw_sql = msg.match(/\(.*\)\s(?.*)/)["sql"] + raw_sql = msg.match(/.*(?SELECT .*)/)["sql"] "#{raw_sql}\n" end end end - before { DB.logger = sql_logger } + shared_examples "be a correct cold and warm" do + let(:reference) do + {"data" => {"customer" => {"orders" => {"edges" => [{"node" => {"id" =>1 }}, {"node" => {"id" => 2}}, {"node" => {"id" => 3}}]}}}} + end + + it 'produces the same result on miss or hit' do + cold_results = execute(query) + warm_results = execute(query) + + expect(cold_results).to eq(reference) + expect(cold_results).to eq warm_results + end + end + + describe 'Seqeul' do + def execute(query, context = {}) + CacheSchema.execute(query, context: context) + end + let(:customer) { Customer.last } + + before { DB.logger = sql_logger } + + it_behaves_like "be a correct cold and warm" - it 'produces the same result on miss or hit' do - cold_results = execute(query) - warm_results = execute(query) + it 'calls sql engine only one time per cached field' do + 5.times { execute(query) } - expect(cold_results).to eq warm_results + expect(sql_logger.messages).to eq( + <<~SQL + SELECT * FROM `customers` ORDER BY `id` DESC LIMIT 1 + SELECT * FROM `customers` WHERE `id` = '1' + SELECT * FROM `orders` WHERE (`orders`.`customer_id` = 1) + SQL + ) + end end - it 'calls sql engine only one time per cached field' do - 5.times { execute(query) } + describe 'ActiveRecord' do + def execute(query, context = {}) + AR::CacheSchema.execute(query, context: context) + end + + let(:customer) { AR::Customer.last } - expect(sql_logger.messages).to eq( - <<~SQL - SELECT * FROM `customers` ORDER BY `id` DESC LIMIT 1 - SELECT * FROM `customers` WHERE `id` = '1' - SELECT * FROM `orders` WHERE (`orders`.`customer_id` = 1) - SQL - ) + before { ActiveRecord::Base.logger = sql_logger } + + it_behaves_like "be a correct cold and warm" + + it 'calls sql engine only one time per cached field' do + 5.times { execute(query) } + + expect(sql_logger.messages).to eq( + <<~SQL + SELECT "customers".* FROM "customers" ORDER BY "customers"."id" DESC LIMIT ? [["LIMIT", 1]] + SELECT "customers".* FROM "customers" WHERE "customers"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] + SELECT "orders".* FROM "orders" WHERE "orders"."customer_id" = ? [["customer_id", 1]] + SQL + ) + end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index fbcc3aa..9724106 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,3 +1,4 @@ +require 'active_record' # should be before graphql-ruby require 'bundler/setup' require 'pry' @@ -36,6 +37,7 @@ # required after GraphQL::Cache initialization because dev # schema uses cache and logger objects from it. require_relative '../test_schema' + require_relative '../test_schema/active_record/init' config.include TestMacros config.extend TestMacros::ClassMethods diff --git a/spec/support/test_cache.rb b/spec/support/test_cache.rb index 83e975f..05a9276 100644 --- a/spec/support/test_cache.rb +++ b/spec/support/test_cache.rb @@ -2,11 +2,12 @@ class TestCache def write(key, doc, opts={}) - cache[key] = doc + # we duplicate the value to get rid of ruby object level caching + cache[key] = ::Marshal.load(::Marshal.dump(doc)) end def read(key) - cache[key] + ::Marshal.load(::Marshal.dump(cache[key])) end def cache diff --git a/test_schema/active_record/factories.rb b/test_schema/active_record/factories.rb new file mode 100644 index 0000000..ebdaf00 --- /dev/null +++ b/test_schema/active_record/factories.rb @@ -0,0 +1,18 @@ +module AR + class Factories + def self.bootstrap + customer = AR::Customer.create( + display_name: 'Michael', + email: 'michael@example.com' + ) + + AR::Order.create(customer_id: customer.id, number: new_num, total_price_cents: 1399) + AR::Order.create(customer_id: customer.id, number: new_num, total_price_cents: 1399) + AR::Order.create(customer_id: customer.id, number: new_num, total_price_cents: 1399) + end + + def self.new_num + AR::Order.count + 1000 + end + end +end diff --git a/test_schema/active_record/graphql_schema.rb b/test_schema/active_record/graphql_schema.rb new file mode 100644 index 0000000..6392a73 --- /dev/null +++ b/test_schema/active_record/graphql_schema.rb @@ -0,0 +1,23 @@ +require 'benchmark' + +module AR + class BaseType < ::BaseType; end + class OrderType < ::OrderType; end + + class CustomerType < ::CustomerType; end + + class QueryType < ::QueryType + def customer(id:) + AR::Customer.find(id) + end + end + + class CacheSchema < ::CacheSchema + query AR::QueryType + use GraphQL::Cache + + def self.resolve_type(_type, obj, _ctx) + "AR::#{obj.class.name}Type" + end + end +end diff --git a/test_schema/active_record/init.rb b/test_schema/active_record/init.rb new file mode 100644 index 0000000..496af08 --- /dev/null +++ b/test_schema/active_record/init.rb @@ -0,0 +1,9 @@ +require 'logger' + +require_relative './schema' +require_relative './models' +require_relative './graphql_schema' +require_relative './factories' + +ActiveRecord::Base.logger = GraphQL::Cache.logger +AR::Factories.bootstrap diff --git a/test_schema/active_record/models.rb b/test_schema/active_record/models.rb new file mode 100644 index 0000000..e2cb449 --- /dev/null +++ b/test_schema/active_record/models.rb @@ -0,0 +1,9 @@ +module AR + class Order < ActiveRecord::Base + belongs_to :customer + end + + class Customer < ActiveRecord::Base + has_many :orders + end +end diff --git a/test_schema/active_record/schema.rb b/test_schema/active_record/schema.rb new file mode 100644 index 0000000..a16bcc2 --- /dev/null +++ b/test_schema/active_record/schema.rb @@ -0,0 +1,20 @@ +require 'active_record' + +ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") + +ActiveRecord::Schema.define do + self.verbose = false + + create_table :schema_migrations, force: true + + create_table :customers, force: true do |t| + t.string :display_name + t.string :email + end + + create_table :orders, force: true do |t| + t.integer :customer_id + t.integer :number + t.integer :total_price_cents + end +end diff --git a/test_schema/factories.rb b/test_schema/factories.rb index 7241e0a..fd0c8b2 100644 --- a/test_schema/factories.rb +++ b/test_schema/factories.rb @@ -1,4 +1,4 @@ -module Factories +class Factories def self.bootstrap customer = Customer.create( display_name: 'Michael', From 828b8c71fa11c48f86931f816cbbbec4a7847df7 Mon Sep 17 00:00:00 2001 From: Nikolay Sverchkov Date: Thu, 19 Dec 2019 17:45:46 +0300 Subject: [PATCH 3/8] Fix ActiveRecord connection caching --- lib/graphql/cache/fetcher.rb | 3 ++ lib/graphql/cache/marshal.rb | 21 ++------ lib/graphql/cache/resolver.rb | 50 ++++------------- lib/graphql/cache/resolvers/base_resolver.rb | 31 +++++++++++ .../cache/resolvers/connection_resolver.rb | 54 +++++++++++++++++++ .../cache/resolvers/scalar_resolver.rb | 17 ++++++ spec/features/connections_spec.rb | 20 +++++-- spec/graphql/cache/marshal_spec.rb | 33 ++---------- 8 files changed, 140 insertions(+), 89 deletions(-) create mode 100644 lib/graphql/cache/resolvers/base_resolver.rb create mode 100644 lib/graphql/cache/resolvers/connection_resolver.rb create mode 100644 lib/graphql/cache/resolvers/scalar_resolver.rb diff --git a/lib/graphql/cache/fetcher.rb b/lib/graphql/cache/fetcher.rb index 40f0021..4d1319f 100644 --- a/lib/graphql/cache/fetcher.rb +++ b/lib/graphql/cache/fetcher.rb @@ -1,5 +1,8 @@ # frozen_string_literal: true +require 'graphql/cache/resolvers/base_resolver' +require 'graphql/cache/resolvers/scalar_resolver' +require 'graphql/cache/resolvers/connection_resolver' require 'graphql/cache/resolver' module GraphQL diff --git a/lib/graphql/cache/marshal.rb b/lib/graphql/cache/marshal.rb index fb529a3..fc6f353 100644 --- a/lib/graphql/cache/marshal.rb +++ b/lib/graphql/cache/marshal.rb @@ -24,23 +24,12 @@ def initialize(key) self.key = key.to_s end - # Read a value from cache if it exists and re-hydrate it or - # execute the block and write it's result to cache - # - # @param config [Hash] The object passed to `cache:` on the field definition + # Read a value from cache # @return [Object] - def read(config, force: false, &block) - # write new data from resolver if forced - return write(config, &block) if force - - cached = cache.read(key) - - if cached.nil? - logger.debug "Cache miss: (#{key})" - write config, &block - else - logger.debug "Cache hit: (#{key})" - cached + def read + cache.read(key).tap do |cached| + logger.debug "Cache miss: (#{key})" if cached.nil? + logger.debug "Cache hit: (#{key})" if cached end end diff --git a/lib/graphql/cache/resolver.rb b/lib/graphql/cache/resolver.rb index 98660ca..dd9b940 100644 --- a/lib/graphql/cache/resolver.rb +++ b/lib/graphql/cache/resolver.rb @@ -4,11 +4,7 @@ module GraphQL module Cache # Represents the caching resolver that wraps the existing resolver proc class Resolver - attr_accessor :type - - attr_accessor :field - - attr_accessor :orig_resolve_proc + attr_accessor :type, :field, :orig_resolve_proc def initialize(type, field) @type = type @@ -16,17 +12,17 @@ def initialize(type, field) end def call(obj, args, ctx) - @orig_resolve_proc = field.resolve_proc - + resolve_proc = proc { field.resolve_proc.call(obj, args, ctx) } key = cache_key(obj, args, ctx) - - value = Marshal[key].read( - field.metadata[:cache], force: ctx[:force_cache] - ) do - @orig_resolve_proc.call(obj, args, ctx) + metadata = field.metadata[:cache] + + if field.connection? + Resolvers::ConnectionResolver.new(resolve_proc, key, metadata).call( + args: args, field: field, parent: obj, context: ctx, force_cache: ctx[:force_cache] + ) + else + Resolvers::ScalarResolver.new(resolve_proc, key, metadata).call(force_cache: ctx[:force_cache]) end - - wrap_connections(value, args, parent: obj, context: ctx) end protected @@ -35,32 +31,6 @@ def call(obj, args, ctx) def cache_key(obj, args, ctx) Key.new(obj, args, type, field, ctx).to_s end - - # @private - def wrap_connections(value, args, **kwargs) - # return raw value if field isn't a connection (no need to wrap) - return value unless field.connection? - - # return cached value if it is already a connection object - # this occurs when the value is being resolved by GraphQL - # and not being read from cache - return value if value.class.ancestors.include?( - GraphQL::Relay::BaseConnection - ) - - create_connection(value, args, **kwargs) - end - - # @private - def create_connection(value, args, **kwargs) - GraphQL::Relay::BaseConnection.connection_for_nodes(value).new( - value, - args, - field: field, - parent: kwargs[:parent], - context: kwargs[:context] - ) - end end end end diff --git a/lib/graphql/cache/resolvers/base_resolver.rb b/lib/graphql/cache/resolvers/base_resolver.rb new file mode 100644 index 0000000..370afce --- /dev/null +++ b/lib/graphql/cache/resolvers/base_resolver.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module GraphQL + module Cache + module Resolvers + class BaseResolver + def initialize(resolve_proc, key, metadata) + @resolve_proc = resolve_proc + @key = key + @metadata = metadata + end + + def call(*args) + raise NotImplementedError + end + + private + + attr_reader :resolve_proc, :key, :metadata + + def read + Marshal[key].read + end + + def write(&block) + Marshal[key].write(metadata, &block) + end + end + end + end +end diff --git a/lib/graphql/cache/resolvers/connection_resolver.rb b/lib/graphql/cache/resolvers/connection_resolver.rb new file mode 100644 index 0000000..f7aa69c --- /dev/null +++ b/lib/graphql/cache/resolvers/connection_resolver.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module GraphQL + module Cache + module Resolvers + # Pass cache write method into GraphQL::Relay::BaseConnection + # and wrap them original Connection methods + class ConnectionResolver < BaseResolver + class ConnectionCache < Module + module WrappedMethods + def paged_nodes + cache_write = instance_variable_get(:@__cache_write) + + cache_write.call { super } + end + end + + def initialize(write) + @write = write + end + + def extended(base) + base.extend(WrappedMethods) + base.instance_variable_set(:@__cache_write, @write) + end + end + + def call(args:, field:, parent:, context:, force_cache:) + if force_cache || (cached = read).nil? + define_connection_cache(resolve_proc.call) + else + wrap_connection(cached, args, field, parent: parent, context: context) + end + end + + private + + def wrap_connection(value, args, field, **kwargs) + GraphQL::Relay::BaseConnection.connection_for_nodes(value).new( + value, + args, + field: field, + parent: kwargs[:parent], + context: kwargs[:context] + ) + end + + def define_connection_cache(connection) + connection.extend(ConnectionCache.new(method(:write))) + end + end + end + end +end diff --git a/lib/graphql/cache/resolvers/scalar_resolver.rb b/lib/graphql/cache/resolvers/scalar_resolver.rb new file mode 100644 index 0000000..1052eb3 --- /dev/null +++ b/lib/graphql/cache/resolvers/scalar_resolver.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module GraphQL + module Cache + module Resolvers + class ScalarResolver < BaseResolver + def call(force_cache:) + return write if force_cache + + cached = read + + cached.nil? ? write { resolve_proc.call } : cached + end + end + end + end +end diff --git a/spec/features/connections_spec.rb b/spec/features/connections_spec.rb index 9e529a9..18d9451 100644 --- a/spec/features/connections_spec.rb +++ b/spec/features/connections_spec.rb @@ -40,7 +40,19 @@ def messages shared_examples "be a correct cold and warm" do let(:reference) do - {"data" => {"customer" => {"orders" => {"edges" => [{"node" => {"id" =>1 }}, {"node" => {"id" => 2}}, {"node" => {"id" => 3}}]}}}} + { + "data" => { + "customer" => { + "orders" => { + "edges" => [ + {"node" => {"id" => 1}}, + {"node" => {"id" => 2}}, + {"node" => {"id" => 3}} + ] + } + } + } + } end it 'produces the same result on miss or hit' do @@ -91,9 +103,9 @@ def execute(query, context = {}) expect(sql_logger.messages).to eq( <<~SQL - SELECT "customers".* FROM "customers" ORDER BY "customers"."id" DESC LIMIT ? [["LIMIT", 1]] - SELECT "customers".* FROM "customers" WHERE "customers"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]] - SELECT "orders".* FROM "orders" WHERE "orders"."customer_id" = ? [["customer_id", 1]] + SELECT \"customers\".* FROM \"customers\" ORDER BY \"customers\".\"id\" DESC LIMIT ?\e[0m [[\"LIMIT\", 1]] + SELECT \"customers\".* FROM \"customers\" WHERE \"customers\".\"id\" = ? LIMIT ?\e[0m [[\"id\", 1], [\"LIMIT\", 1]] + SELECT \"orders\".* FROM \"orders\" WHERE \"orders\".\"customer_id\" = ?\e[0m [[\"customer_id\", 1]] SQL ) end diff --git a/spec/graphql/cache/marshal_spec.rb b/spec/graphql/cache/marshal_spec.rb index 5b2a285..a9be18e 100644 --- a/spec/graphql/cache/marshal_spec.rb +++ b/spec/graphql/cache/marshal_spec.rb @@ -32,19 +32,6 @@ module Cache describe '#read' do let(:config) { true } - let(:block) { double('block', call: 'foo') } - - context 'when force is set' do - it 'should execute the block' do - expect(block).to receive(:call) - subject.read(config, force: true) { block.call } - end - - it 'should write to cache' do - expect(cache).to receive(:write).with(key, doc, expires_in: GraphQL::Cache.expiry) - subject.write(config) { doc } - end - end context 'when cache object exists' do before do @@ -52,27 +39,15 @@ module Cache end it 'should return cached value' do - expect(subject.read(config) { block.call }).to eq doc - end - - it 'should not execute the block' do - expect(block).to_not receive(:call) - subject.read(config) { block.call } + expect(subject.read).to eq doc end end context 'when cache object does not exist' do - before do - cache.clear - end - - it 'should return the evaluated value' do - expect(subject.read(config) { block.call }).to eq block.call - end + before { cache.clear } - it 'should execute the block' do - expect(block).to receive(:call) - subject.read(config) { block.call } + it 'should return nil' do + expect(subject.read).to be_nil end end end From 9618cab59e4e5271620a7f6135ae6acddf3d8384 Mon Sep 17 00:00:00 2001 From: Nikolay Sverchkov Date: Fri, 20 Dec 2019 10:12:00 +0300 Subject: [PATCH 4/8] Move sequel setup into special folder --- bin/console | 2 +- spec/spec_helper.rb | 7 ++++--- test_schema/active_record/factories.rb | 18 ----------------- test_schema/active_record/init.rb | 5 +++-- test_schema/factories.rb | 23 +++++++++++++++------- test_schema/{ => sequel}/graphql_schema.rb | 0 test_schema/sequel/init.rb | 9 +++++++++ test_schema/{ => sequel}/models.rb | 0 test_schema/{ => sequel}/schema.rb | 0 9 files changed, 33 insertions(+), 31 deletions(-) delete mode 100644 test_schema/active_record/factories.rb rename test_schema/{ => sequel}/graphql_schema.rb (100%) create mode 100644 test_schema/sequel/init.rb rename test_schema/{ => sequel}/models.rb (100%) rename test_schema/{ => sequel}/schema.rb (100%) diff --git a/bin/console b/bin/console index d6bf2bc..db0ae21 100755 --- a/bin/console +++ b/bin/console @@ -20,7 +20,7 @@ end # required after GraphQL::Cache initialization because dev # schema uses cache and logger objects from it. -require_relative '../test_schema' +require_relative '../test_schema/sequel/init' require "pry" Pry.start diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 9724106..aac4423 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,4 +1,4 @@ -require 'active_record' # should be before graphql-ruby +require 'active_record' # should be required before graphql-ruby require 'bundler/setup' require 'pry' @@ -36,8 +36,9 @@ # required after GraphQL::Cache initialization because dev # schema uses cache and logger objects from it. - require_relative '../test_schema' - require_relative '../test_schema/active_record/init' + %i[sequel active_record].each do |orm| + require_relative "../test_schema/#{orm}/init" + end config.include TestMacros config.extend TestMacros::ClassMethods diff --git a/test_schema/active_record/factories.rb b/test_schema/active_record/factories.rb deleted file mode 100644 index ebdaf00..0000000 --- a/test_schema/active_record/factories.rb +++ /dev/null @@ -1,18 +0,0 @@ -module AR - class Factories - def self.bootstrap - customer = AR::Customer.create( - display_name: 'Michael', - email: 'michael@example.com' - ) - - AR::Order.create(customer_id: customer.id, number: new_num, total_price_cents: 1399) - AR::Order.create(customer_id: customer.id, number: new_num, total_price_cents: 1399) - AR::Order.create(customer_id: customer.id, number: new_num, total_price_cents: 1399) - end - - def self.new_num - AR::Order.count + 1000 - end - end -end diff --git a/test_schema/active_record/init.rb b/test_schema/active_record/init.rb index 496af08..8d5107f 100644 --- a/test_schema/active_record/init.rb +++ b/test_schema/active_record/init.rb @@ -3,7 +3,8 @@ require_relative './schema' require_relative './models' require_relative './graphql_schema' -require_relative './factories' +require_relative '../factories' ActiveRecord::Base.logger = GraphQL::Cache.logger -AR::Factories.bootstrap +Factories.new(order: AR::Order, customer: AR::Customer).bootstrap + diff --git a/test_schema/factories.rb b/test_schema/factories.rb index fd0c8b2..20f53d4 100644 --- a/test_schema/factories.rb +++ b/test_schema/factories.rb @@ -1,16 +1,25 @@ class Factories - def self.bootstrap - customer = Customer.create( + def initialize(order:, customer:) + @order_class = order + @customer_class = customer + end + + def bootstrap + customer = customer_class.create( display_name: 'Michael', email: 'michael@example.com' ) - Order.create(customer_id: customer.id, number: new_num, total_price_cents: 1399) - Order.create(customer_id: customer.id, number: new_num, total_price_cents: 1399) - Order.create(customer_id: customer.id, number: new_num, total_price_cents: 1399) + order_class.create(customer_id: customer.id, number: new_num, total_price_cents: 1399) + order_class.create(customer_id: customer.id, number: new_num, total_price_cents: 1399) + order_class.create(customer_id: customer.id, number: new_num, total_price_cents: 1399) end - def self.new_num - Order.count + 1000 + def new_num + order_class.count + 1000 end + + private + + attr_reader :order_class, :customer_class end diff --git a/test_schema/graphql_schema.rb b/test_schema/sequel/graphql_schema.rb similarity index 100% rename from test_schema/graphql_schema.rb rename to test_schema/sequel/graphql_schema.rb diff --git a/test_schema/sequel/init.rb b/test_schema/sequel/init.rb new file mode 100644 index 0000000..01139c4 --- /dev/null +++ b/test_schema/sequel/init.rb @@ -0,0 +1,9 @@ +require 'logger' + +require_relative './schema' +require_relative './models' +require_relative './graphql_schema' +require_relative '../factories' + +Factories.new(order: Order, customer: Customer).bootstrap +DB.loggers = [GraphQL::Cache.logger] diff --git a/test_schema/models.rb b/test_schema/sequel/models.rb similarity index 100% rename from test_schema/models.rb rename to test_schema/sequel/models.rb diff --git a/test_schema/schema.rb b/test_schema/sequel/schema.rb similarity index 100% rename from test_schema/schema.rb rename to test_schema/sequel/schema.rb From dfc6701f5e2cd04a628411e788f28dc250a794cd Mon Sep 17 00:00:00 2001 From: Nikolay Sverchkov Date: Fri, 20 Dec 2019 13:04:42 +0300 Subject: [PATCH 5/8] Restore relation connection with original nodes --- .../cache/resolvers/connection_resolver.rb | 43 ++++++++++++++----- spec/features/connections_spec.rb | 6 +-- spec/spec_helper.rb | 5 ++- spec/support/test_cache.rb | 3 +- test_schema/active_record/graphql_schema.rb | 2 + test_schema/sequel/graphql_schema.rb | 2 + 6 files changed, 45 insertions(+), 16 deletions(-) diff --git a/lib/graphql/cache/resolvers/connection_resolver.rb b/lib/graphql/cache/resolvers/connection_resolver.rb index f7aa69c..83a5476 100644 --- a/lib/graphql/cache/resolvers/connection_resolver.rb +++ b/lib/graphql/cache/resolvers/connection_resolver.rb @@ -3,15 +3,19 @@ module GraphQL module Cache module Resolvers - # Pass cache write method into GraphQL::Relay::BaseConnection - # and wrap them original Connection methods class ConnectionResolver < BaseResolver - class ConnectionCache < Module + NodesCache = Struct.new(:nodes, :paged_nodes) + + # Pass cache write method into GraphQL::Relay::RelationConnection + class RelationConnectionOverload < Module module WrappedMethods def paged_nodes cache_write = instance_variable_get(:@__cache_write) - cache_write.call { super } + super.tap do |result| + # save original relation (aka @nodes) and loaded records + cache_write.call { NodesCache.new(@nodes, result) } + end end end @@ -27,7 +31,7 @@ def extended(base) def call(args:, field:, parent:, context:, force_cache:) if force_cache || (cached = read).nil? - define_connection_cache(resolve_proc.call) + define_relation_cache(resolve_proc.call) else wrap_connection(cached, args, field, parent: parent, context: context) end @@ -35,18 +39,35 @@ def call(args:, field:, parent:, context:, force_cache:) private - def wrap_connection(value, args, field, **kwargs) - GraphQL::Relay::BaseConnection.connection_for_nodes(value).new( - value, + def wrap_connection(cached, args, field, **kwargs) + nodes, paged_nodes = parse(cached) + + GraphQL::Relay::BaseConnection.connection_for_nodes(nodes).new( + nodes, args, field: field, parent: kwargs[:parent], context: kwargs[:context] - ) + ).tap do |connection| + # restore cached paged_nodes + connection.instance_variable_set(:@paged_nodes, paged_nodes) if paged_nodes + end + end + + def define_relation_cache(connection) + if connection.is_a?(GraphQL::Relay::RelationConnection) + # inject cached logic into the relation connection + connection.extend(RelationConnectionOverload.new(method(:write))) + else + # cache loaded connection (works for ArrayConnection) + write { connection } + end end - def define_connection_cache(connection) - connection.extend(ConnectionCache.new(method(:write))) + def parse(cached) + return [cached, nil] unless cached.is_a?(NodesCache) + + [cached.nodes, cached.paged_nodes] end end end diff --git a/spec/features/connections_spec.rb b/spec/features/connections_spec.rb index 18d9451..5a2cf8c 100644 --- a/spec/features/connections_spec.rb +++ b/spec/features/connections_spec.rb @@ -101,11 +101,11 @@ def execute(query, context = {}) it 'calls sql engine only one time per cached field' do 5.times { execute(query) } - expect(sql_logger.messages).to eq( - <<~SQL + expect(sql_logger.messages.squish).to eq( + <<~SQL.squish SELECT \"customers\".* FROM \"customers\" ORDER BY \"customers\".\"id\" DESC LIMIT ?\e[0m [[\"LIMIT\", 1]] SELECT \"customers\".* FROM \"customers\" WHERE \"customers\".\"id\" = ? LIMIT ?\e[0m [[\"id\", 1], [\"LIMIT\", 1]] - SELECT \"orders\".* FROM \"orders\" WHERE \"orders\".\"customer_id\" = ?\e[0m [[\"customer_id\", 1]] + SELECT \"orders\".* FROM \"orders\" WHERE \"orders\".\"customer_id\" = ? LIMIT ?\e[0m [[\"customer_id\", 1], [\"LIMIT\", 50]] SQL ) end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index aac4423..2732a78 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,4 +1,7 @@ -require 'active_record' # should be required before graphql-ruby +# ORMs should be required before graphql-ruby +require 'active_record' +require 'sequel' + require 'bundler/setup' require 'pry' diff --git a/spec/support/test_cache.rb b/spec/support/test_cache.rb index 05a9276..a8bdbb9 100644 --- a/spec/support/test_cache.rb +++ b/spec/support/test_cache.rb @@ -2,7 +2,8 @@ class TestCache def write(key, doc, opts={}) - # we duplicate the value to get rid of ruby object level caching + # duplicate the value to get rid of ruby object level caching + # and reproduce Rails.cache logic cache[key] = ::Marshal.load(::Marshal.dump(doc)) end diff --git a/test_schema/active_record/graphql_schema.rb b/test_schema/active_record/graphql_schema.rb index 6392a73..db1fc88 100644 --- a/test_schema/active_record/graphql_schema.rb +++ b/test_schema/active_record/graphql_schema.rb @@ -16,6 +16,8 @@ class CacheSchema < ::CacheSchema query AR::QueryType use GraphQL::Cache + default_max_page_size 50 + def self.resolve_type(_type, obj, _ctx) "AR::#{obj.class.name}Type" end diff --git a/test_schema/sequel/graphql_schema.rb b/test_schema/sequel/graphql_schema.rb index f569efb..6de7d18 100644 --- a/test_schema/sequel/graphql_schema.rb +++ b/test_schema/sequel/graphql_schema.rb @@ -31,6 +31,8 @@ class CacheSchema < GraphQL::Schema use GraphQL::Cache + default_max_page_size 50 + def self.resolve_type(_type, obj, _ctx) "#{obj.class.name}Type" end From e567c04a5a9b3539b072f2287c84e5dcc22008b8 Mon Sep 17 00:00:00 2001 From: Nikolay Sverchkov Date: Wed, 15 Jan 2020 13:35:28 +0300 Subject: [PATCH 6/8] Support GraphQL interpreter mode --- Gemfile.lock | 4 +- graphql-cache.gemspec | 2 +- lib/graphql/cache.rb | 9 +++- lib/graphql/cache/field_extension.rb | 16 ++++++ lib/graphql/cache/key.rb | 2 +- lib/graphql/cache/marshal.rb | 1 + .../cache/patch/connection_extension.rb | 19 +++++++ lib/graphql/cache/resolver.rb | 11 +++-- lib/graphql/cache/resolvers/base_resolver.rb | 8 +-- .../cache/resolvers/connection_resolver.rb | 49 ++++++++++--------- test_schema/sequel/graphql_schema.rb | 7 ++- 11 files changed, 89 insertions(+), 39 deletions(-) create mode 100644 lib/graphql/cache/field_extension.rb create mode 100644 lib/graphql/cache/patch/connection_extension.rb diff --git a/Gemfile.lock b/Gemfile.lock index a90833e..728b9d4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,7 +2,7 @@ PATH remote: . specs: graphql-cache (0.6.0) - graphql (~> 1, > 1.8) + graphql (~> 1, > 1.9.3) GEM remote: https://rubygems.org/ @@ -28,7 +28,7 @@ GEM concurrent-ruby (1.1.5) diff-lcs (1.3) docile (1.1.5) - graphql (1.9.3) + graphql (1.9.17) i18n (1.7.0) concurrent-ruby (~> 1.0) json (2.2.0) diff --git a/graphql-cache.gemspec b/graphql-cache.gemspec index 6857ba5..a89250c 100644 --- a/graphql-cache.gemspec +++ b/graphql-cache.gemspec @@ -32,5 +32,5 @@ Gem::Specification.new do |s| s.add_development_dependency 'simplecov' s.add_development_dependency 'sqlite3' - s.add_dependency 'graphql', '~> 1', '> 1.8' + s.add_dependency 'graphql', '~> 1', '> 1.9.3' end diff --git a/lib/graphql/cache.rb b/lib/graphql/cache.rb index 9a4dfa2..b758105 100644 --- a/lib/graphql/cache.rb +++ b/lib/graphql/cache.rb @@ -5,6 +5,8 @@ require 'graphql/cache/key' require 'graphql/cache/marshal' require 'graphql/cache/fetcher' +require 'graphql/cache/field_extension' +require 'graphql/cache/patch/connection_extension' module GraphQL module Cache @@ -45,8 +47,11 @@ def configure # bootstrap necessary instrumentation and tracing # tie-ins def self.use(schema_def, options: {}) - fetcher = ::GraphQL::Cache::Fetcher.new - schema_def.instrument(:field, fetcher) + # please, use GraphQL::Cache::FieldExtension if use Interpreter mode + if Gem::Version.new(GraphQL::VERSION) >= Gem::Version.new('1.9.0.pre3') + fetcher = ::GraphQL::Cache::Fetcher.new + schema_def.instrument(:field, fetcher) + end end end end diff --git a/lib/graphql/cache/field_extension.rb b/lib/graphql/cache/field_extension.rb new file mode 100644 index 0000000..2a6f4e3 --- /dev/null +++ b/lib/graphql/cache/field_extension.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module GraphQL + module Cache + class FieldExtension < GraphQL::Schema::FieldExtension + def apply + field.instance_variable_set(:@__cache_config, options.present? ? options : true) + end + + def resolve(object:, arguments:, **rest, &block) + GraphQL::Cache::Resolver.new(field.owner, field) + .call(object, arguments, rest[:context], &block) + end + end + end +end diff --git a/lib/graphql/cache/key.rb b/lib/graphql/cache/key.rb index 5b95c18..1bea668 100644 --- a/lib/graphql/cache/key.rb +++ b/lib/graphql/cache/key.rb @@ -33,7 +33,7 @@ def initialize(obj, args, type, field, context = {}) @type = type @field = field @context = context - @metadata = field.metadata[:cache] + @metadata = field.instance_variable_get(:@cache_config) @metadata = { cache: @metadata } unless @metadata.is_a?(Hash) end diff --git a/lib/graphql/cache/marshal.rb b/lib/graphql/cache/marshal.rb index fc6f353..7d741c8 100644 --- a/lib/graphql/cache/marshal.rb +++ b/lib/graphql/cache/marshal.rb @@ -44,6 +44,7 @@ def write(config) with_resolved_document(document) do |resolved_document| cache.write(key, resolved_document, expires_in: expiry(config)) + logger.debug "Cache was added: (#{key} with config #{config})" resolved end diff --git a/lib/graphql/cache/patch/connection_extension.rb b/lib/graphql/cache/patch/connection_extension.rb new file mode 100644 index 0000000..265a437 --- /dev/null +++ b/lib/graphql/cache/patch/connection_extension.rb @@ -0,0 +1,19 @@ +module GraphQL + module Cache + module Patch + module ConnectionExtension + def after_resolve(value:, object:, arguments:, context:, memo:) + # in Cached Extension we wrap the original value to the Connection + # so we do not have to do it againt + return value if value.is_a?(GraphQL::Relay::BaseConnection) + + super + end + end + end + end +end + +GraphQL::Schema::Field::ConnectionExtension.prepend( + GraphQL::Cache::Patch::ConnectionExtension +) diff --git a/lib/graphql/cache/resolver.rb b/lib/graphql/cache/resolver.rb index dd9b940..8883ec4 100644 --- a/lib/graphql/cache/resolver.rb +++ b/lib/graphql/cache/resolver.rb @@ -11,17 +11,18 @@ def initialize(type, field) @field = field end - def call(obj, args, ctx) - resolve_proc = proc { field.resolve_proc.call(obj, args, ctx) } + def call(obj, args, ctx, &block) + resolve_proc = proc { block.call(obj, args, ctx) } key = cache_key(obj, args, ctx) - metadata = field.metadata[:cache] + + cache_config = field.instance_variable_get(:@__cache_config) if field.connection? - Resolvers::ConnectionResolver.new(resolve_proc, key, metadata).call( + Resolvers::ConnectionResolver.new(resolve_proc, key, cache_config).call( args: args, field: field, parent: obj, context: ctx, force_cache: ctx[:force_cache] ) else - Resolvers::ScalarResolver.new(resolve_proc, key, metadata).call(force_cache: ctx[:force_cache]) + Resolvers::ScalarResolver.new(resolve_proc, key, cache_config).call(force_cache: ctx[:force_cache]) end end diff --git a/lib/graphql/cache/resolvers/base_resolver.rb b/lib/graphql/cache/resolvers/base_resolver.rb index 370afce..abede03 100644 --- a/lib/graphql/cache/resolvers/base_resolver.rb +++ b/lib/graphql/cache/resolvers/base_resolver.rb @@ -4,10 +4,10 @@ module GraphQL module Cache module Resolvers class BaseResolver - def initialize(resolve_proc, key, metadata) + def initialize(resolve_proc, key, cache_config) @resolve_proc = resolve_proc @key = key - @metadata = metadata + @cache_config = cache_config end def call(*args) @@ -16,14 +16,14 @@ def call(*args) private - attr_reader :resolve_proc, :key, :metadata + attr_reader :resolve_proc, :key, :cache_config def read Marshal[key].read end def write(&block) - Marshal[key].write(metadata, &block) + Marshal[key].write(cache_config, &block) end end end diff --git a/lib/graphql/cache/resolvers/connection_resolver.rb b/lib/graphql/cache/resolvers/connection_resolver.rb index 83a5476..8f24cae 100644 --- a/lib/graphql/cache/resolvers/connection_resolver.rb +++ b/lib/graphql/cache/resolvers/connection_resolver.rb @@ -30,44 +30,49 @@ def extended(base) end def call(args:, field:, parent:, context:, force_cache:) - if force_cache || (cached = read).nil? - define_relation_cache(resolve_proc.call) + if force_cache || (cache = read).nil? + define_relation_cache(resolve_proc.call, args, field, parent: parent, context: context) else - wrap_connection(cached, args, field, parent: parent, context: context) + use(cache, args, field, parent: parent, context: context) end end private - def wrap_connection(cached, args, field, **kwargs) - nodes, paged_nodes = parse(cached) + def define_relation_cache(nodes, args, field, **kwargs) + if nodes.is_a?(GraphQL::Relay::RelationConnection) + # inject cached logic into the relation connection + # works with non Interpreter mode + nodes + else + # nodes are Array or ActiveRecord relation + wrap_to_connection(nodes, args, field, kwargs) + end.extend(RelationConnectionOverload.new(method(:write))) + end + + def use(cache, args, field, **kwargs) + nodes, paged_nodes = parse(cache) + + wrap_to_connection(nodes, args, field, kwargs).tap do |conn| + # restore cached paged_nodes (works for AR relations) + conn.instance_variable_set(:@paged_nodes, paged_nodes) if paged_nodes + end + end + def wrap_to_connection(nodes, args, field, **kwargs) GraphQL::Relay::BaseConnection.connection_for_nodes(nodes).new( nodes, args, field: field, parent: kwargs[:parent], context: kwargs[:context] - ).tap do |connection| - # restore cached paged_nodes - connection.instance_variable_set(:@paged_nodes, paged_nodes) if paged_nodes - end - end - - def define_relation_cache(connection) - if connection.is_a?(GraphQL::Relay::RelationConnection) - # inject cached logic into the relation connection - connection.extend(RelationConnectionOverload.new(method(:write))) - else - # cache loaded connection (works for ArrayConnection) - write { connection } - end + ) end - def parse(cached) - return [cached, nil] unless cached.is_a?(NodesCache) + def parse(cache) + return [cache, nil] unless cache.is_a?(NodesCache) - [cached.nodes, cached.paged_nodes] + [cache.nodes, cache.paged_nodes] end end end diff --git a/test_schema/sequel/graphql_schema.rb b/test_schema/sequel/graphql_schema.rb index 6de7d18..ce5dcfb 100644 --- a/test_schema/sequel/graphql_schema.rb +++ b/test_schema/sequel/graphql_schema.rb @@ -13,11 +13,13 @@ class OrderType < BaseType class CustomerType < BaseType field :display_name, String, null: false field :email, String, null: false - field :orders, OrderType.connection_type, null: false, cache: true + field :orders, OrderType.connection_type, null: false, extensions: [ + ::GraphQL::Cache::FieldExtension + ] end class QueryType < BaseType - field :customer, CustomerType, null: true, cache: true do + field :customer, CustomerType, null: true, extensions: [::GraphQL::Cache::FieldExtension] do argument :id, ID, 'Unique Identifier for querying a specific user', required: true end @@ -29,6 +31,7 @@ def customer(id:) class CacheSchema < GraphQL::Schema query QueryType + use GraphQL::Execution::Interpreter use GraphQL::Cache default_max_page_size 50 From 84f892664af8737231494dd519c19972b1a3d3d6 Mon Sep 17 00:00:00 2001 From: Nikolay Sverchkov Date: Wed, 25 Mar 2020 15:46:55 +0300 Subject: [PATCH 7/8] Add specs for scalar caching --- spec/features/connections_spec.rb | 7 ++++- spec/features/scalar_spec.rb | 38 ++++++++++++++++++++++++++++ test_schema/sequel/graphql_schema.rb | 2 +- 3 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 spec/features/scalar_spec.rb diff --git a/spec/features/connections_spec.rb b/spec/features/connections_spec.rb index 5a2cf8c..e0c29fc 100644 --- a/spec/features/connections_spec.rb +++ b/spec/features/connections_spec.rb @@ -94,7 +94,12 @@ def execute(query, context = {}) let(:customer) { AR::Customer.last } - before { ActiveRecord::Base.logger = sql_logger } + around(:each) do |example| + default_logger = ActiveRecord::Base.logger + ActiveRecord::Base.logger = sql_logger + example.run + ActiveRecord::Base.logger = default_logger + end it_behaves_like "be a correct cold and warm" diff --git a/spec/features/scalar_spec.rb b/spec/features/scalar_spec.rb new file mode 100644 index 0000000..b480ac9 --- /dev/null +++ b/spec/features/scalar_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +RSpec.describe 'caching scalar fields' do + let(:query) do + %Q{ + { + customer(id: #{customer.id}) { + orders { + edges { + node { + totalPriceCents + } + } + } + } + } + } + end + + describe 'ActiveRecord' do + def execute(query, context = {}) + AR::CacheSchema.execute(query, context: context) + end + + let(:customer) { AR::Customer.last } + + before do + customer.orders.delete_all + customer.orders.create(total_price_cents: 100)# only one order + end + + it 'calls order total_price_cents only one times' do + expect_any_instance_of(AR::Order).to receive(:total_price_cents).once.and_call_original + + 5.times { execute(query) } + end + end +end diff --git a/test_schema/sequel/graphql_schema.rb b/test_schema/sequel/graphql_schema.rb index ce5dcfb..5c2307b 100644 --- a/test_schema/sequel/graphql_schema.rb +++ b/test_schema/sequel/graphql_schema.rb @@ -7,7 +7,7 @@ class BaseType < GraphQL::Schema::Object class OrderType < BaseType field :id, Int, null: false field :number, Int, null: true - field :total_price_cents, Int, null: true + field :total_price_cents, Int, null: true, extensions: [::GraphQL::Cache::FieldExtension] end class CustomerType < BaseType From d8b17755f78275212241841b602344eaac807f6b Mon Sep 17 00:00:00 2001 From: Nikolay Sverchkov Date: Wed, 25 Mar 2020 18:28:23 +0300 Subject: [PATCH 8/8] Handle connection and scalar field differently --- lib/graphql/cache/field_extension.rb | 18 ++++++++++++++++-- lib/graphql/cache/resolver.rb | 4 ++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/graphql/cache/field_extension.rb b/lib/graphql/cache/field_extension.rb index 2a6f4e3..35bb9fc 100644 --- a/lib/graphql/cache/field_extension.rb +++ b/lib/graphql/cache/field_extension.rb @@ -7,9 +7,23 @@ def apply field.instance_variable_set(:@__cache_config, options.present? ? options : true) end - def resolve(object:, arguments:, **rest, &block) + def resolve(object:, arguments:, **rest) + if field.connection? + yield(object, arguments, object: object, arguments: arguments) + else + GraphQL::Cache::Resolver.new(field.owner, field) + .call(object, arguments, rest[:context], proc { yield(object, arguments) }) + end + end + + def after_resolve(value:, memo:, **rest) + return value unless field.connection? + + arguments = memo[:arguments] + object = memo[:object] + GraphQL::Cache::Resolver.new(field.owner, field) - .call(object, arguments, rest[:context], &block) + .call(object, arguments, rest[:context], proc { value }) end end end diff --git a/lib/graphql/cache/resolver.rb b/lib/graphql/cache/resolver.rb index 8883ec4..4911385 100644 --- a/lib/graphql/cache/resolver.rb +++ b/lib/graphql/cache/resolver.rb @@ -11,8 +11,8 @@ def initialize(type, field) @field = field end - def call(obj, args, ctx, &block) - resolve_proc = proc { block.call(obj, args, ctx) } + def call(obj, args, ctx, block) + resolve_proc = block #proc { block.call(obj, args, ctx) } key = cache_key(obj, args, ctx) cache_config = field.instance_variable_get(:@__cache_config)