Skip to content
This repository was archived by the owner on Oct 26, 2022. It is now read-only.

Fix Connection Cache for ActiveRecord #68

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
26 changes: 22 additions & 4 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,39 @@ PATH
remote: .
specs:
graphql-cache (0.6.0)
graphql (~> 1, > 1.8)
graphql (~> 1, > 1.9.3)

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
thor (>= 0.14.0)
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)
graphql (1.9.17)
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)
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion bin/console
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 2 additions & 1 deletion graphql-cache.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ 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'

s.add_dependency 'graphql', '~> 1', '> 1.8'
s.add_dependency 'graphql', '~> 1', '> 1.9.3'
end
9 changes: 7 additions & 2 deletions lib/graphql/cache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions lib/graphql/cache/fetcher.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down
30 changes: 30 additions & 0 deletions lib/graphql/cache/field_extension.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# 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)
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], proc { value })
end
end
end
end
2 changes: 1 addition & 1 deletion lib/graphql/cache/key.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 6 additions & 16 deletions lib/graphql/cache/marshal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -55,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
Expand Down
19 changes: 19 additions & 0 deletions lib/graphql/cache/patch/connection_extension.rb
Original file line number Diff line number Diff line change
@@ -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
)
51 changes: 11 additions & 40 deletions lib/graphql/cache/resolver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,26 @@ 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
@field = field
end

def call(obj, args, ctx)
@orig_resolve_proc = field.resolve_proc

def call(obj, args, ctx, block)
resolve_proc = block #proc { block.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)
end
cache_config = field.instance_variable_get(:@__cache_config)

wrap_connections(value, args, parent: obj, context: ctx)
if field.connection?
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, cache_config).call(force_cache: ctx[:force_cache])
end
end

protected
Expand All @@ -35,32 +32,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
31 changes: 31 additions & 0 deletions lib/graphql/cache/resolvers/base_resolver.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

module GraphQL
module Cache
module Resolvers
class BaseResolver
def initialize(resolve_proc, key, cache_config)
@resolve_proc = resolve_proc
@key = key
@cache_config = cache_config
end

def call(*args)
raise NotImplementedError
end

private

attr_reader :resolve_proc, :key, :cache_config

def read
Marshal[key].read
end

def write(&block)
Marshal[key].write(cache_config, &block)
end
end
end
end
end
Loading