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
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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