graphql-cache 0.2.5 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f508c5138bea5539cbc6e9c95bb41d5af2f5b87ef245acf972ec0a37936dbdd
4
- data.tar.gz: 7af8a0cc6611334403b91f2c9ac53dba1c0fe8f60a8f9c33775e62aadbb295b4
3
+ metadata.gz: fb2192273e278a5586abc0e640ef6057f28e072f38b46740a5e0ebf6364f7be1
4
+ data.tar.gz: 59d4fd50a85cbc58a6109373f4fb4dca69aa54548960e7be62af5cb98b4cf6cd
5
5
  SHA512:
6
- metadata.gz: b8d43648e3bc7dbb3f524adca874cc5724fab27f14814176f8a0100eb1eb18356670d05389a01252118be05cd329d9b5a035eae4bec0b770a132e85ec6034743
7
- data.tar.gz: d0d78b24cf23ea06e2011b43c5cac05ed7f4610ef2396035d0658f42c0bb4dc471c996ce800c9e1b892e2e52d6e114d1330c6ab658391bd38fda228c32bbcec8
6
+ metadata.gz: 06c5fe310bf069f2017be61070b3eb809edf006473ef0f4c3e9eb27fda37278cb683a95cffceef3bd74fade59a9b76560bb7234bdfc09adc788b734667ebd110
7
+ data.tar.gz: dd2f2eed8a84be514eb760a98c62040f2854cc1d98c6ccb4790fe314d50b0ea75015b1ef0600e0239b080701619b0ed5496b6f305847855f2fadfa91adf4c943
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- graphql-cache (0.2.4)
4
+ graphql-cache (0.2.5)
5
5
  graphql (~> 1.8.0)
6
6
 
7
7
  GEM
@@ -19,6 +19,7 @@ GEM
19
19
  graphql (1.8.4)
20
20
  json (2.1.0)
21
21
  method_source (0.9.0)
22
+ mini_cache (1.1.0)
22
23
  pry (0.11.3)
23
24
  coderay (~> 1.1.0)
24
25
  method_source (~> 0.9.0)
@@ -36,11 +37,13 @@ GEM
36
37
  diff-lcs (>= 1.2.0, < 2.0)
37
38
  rspec-support (~> 3.7.0)
38
39
  rspec-support (3.7.1)
40
+ sequel (5.9.0)
39
41
  simplecov (0.16.1)
40
42
  docile (~> 1.1)
41
43
  json (>= 1.8, < 3)
42
44
  simplecov-html (~> 0.10.0)
43
45
  simplecov-html (0.10.2)
46
+ sqlite3 (1.3.13)
44
47
  thor (0.20.0)
45
48
 
46
49
  PLATFORMS
@@ -51,10 +54,13 @@ DEPENDENCIES
51
54
  bundler (~> 1.16)
52
55
  codeclimate-test-reporter
53
56
  graphql-cache!
57
+ mini_cache
54
58
  pry
55
59
  rake (~> 10.0)
56
60
  rspec (~> 3.0)
61
+ sequel
57
62
  simplecov
63
+ sqlite3
58
64
 
59
65
  BUNDLED WITH
60
66
  1.16.2
data/README.md CHANGED
@@ -1,79 +1,110 @@
1
+ <img height=90 src=https://img.stackshare.io/misc/graphql-cache.png>
2
+
1
3
  # GraphQL Cache
2
- [![Build Status](https://travis-ci.org/Leanstack/graphql-cache.svg?branch=master)](https://travis-ci.org/Leanstack/graphql-cache) [![Test Coverage](https://api.codeclimate.com/v1/badges/c8560834b10db0618175/test_coverage)](https://codeclimate.com/github/Leanstack/graphql-cache/test_coverage) [![Maintainability](https://api.codeclimate.com/v1/badges/c8560834b10db0618175/maintainability)](https://codeclimate.com/github/Leanstack/graphql-cache/maintainability)
4
+ [![Gem Version](https://badge.fury.io/rb/graphql-cache.svg)](https://badge.fury.io/rb/graphql-cache) [![Build Status](https://travis-ci.org/stackshareio/graphql-cache.svg?branch=master)](https://travis-ci.org/stackshareio/graphql-cache) [![Test Coverage](https://api.codeclimate.com/v1/badges/524c0f23ed1dbf0f9338/test_coverage)](https://codeclimate.com/github/stackshareio/graphql-cache/test_coverage) [![Maintainability](https://api.codeclimate.com/v1/badges/524c0f23ed1dbf0f9338/maintainability)](https://codeclimate.com/github/stackshareio/graphql-cache/maintainability)
5
+
6
+ A custom middleware for [graphql-ruby](https://github.com/rmosolgo/graphql-ruby)
7
+
8
+ ## Goals
3
9
 
4
- GraphQL Cache is a custom middleware for graphql-ruby providing field-level caching. It is currently a work in progress and the API is subject to change prior to the release v1.0.0.
10
+ - Provide resolver-level caching for [GraphQL](https://graphql.org) APIs written in ruby
11
+ - Configurable to work with or without Rails
12
+ - [API Documentation](https://www.rubydoc.info/gems/graphql-cache)
13
+
14
+ ## Why?
15
+
16
+ At [StackShare](https://stackshare.io) we've been rolling out [graphql-ruby](https://github.com/rmosolgo/graphql-ruby) for several of our new features and found ourselves in need of a caching solution. We could have simply used `Rails.cache` in our resolvers, but this creates very verbose types or resolver classes. It also means that each and every resolver must define it's own expiration and key. GraphQL Cache solves that problem by integrating caching functionality into the [graphql-ruby](https://github.com/rmosolgo/graphql-ruby) resolution process making caching transparent on most fields except for a metadata flag denoting the field as cached. More details on our motivation for creating this [here](https://stackshare.io/posts/introducing-graphql-cache).
5
17
 
6
18
  ## Installation
7
19
 
8
- Add this line to your application's Gemfile:
20
+ Add this line to your application's Gemfile:
9
21
 
10
- ```ruby
11
- gem 'graphql-cache'
12
- ```
22
+ ```ruby
23
+ gem 'graphql-cache'
24
+ ```
13
25
 
14
- And then execute:
26
+ And then execute:
15
27
 
16
- $ bundle
28
+ ```sh
29
+ $ bundle
30
+ ```
17
31
 
18
- Or install it yourself as:
32
+ Or install it yourself as:
19
33
 
20
- $ gem install graphql-cache
34
+ ```sh
35
+ $ gem install graphql-cache
36
+ ```
21
37
 
22
38
  ## Setup
23
39
 
24
- 1. Add `middleware GraphQL::Cache::Middleware` to your schema
25
- 2. Add `field_class GraphQL::Cache::Field` to your base object type
26
-
27
- ## Configuration
40
+ 1. Use GraphQL Cache as a plugin in your schema.
28
41
 
29
- GraphQL Cache can be configured in an initializer:
42
+ ```ruby
43
+ class MySchema < GraphQL::Schema
44
+ query Types::Query
30
45
 
46
+ use GraphQL::Cache
47
+ end
48
+ ```
49
+ 2. Add the custom caching field class to your base object class. This adds the `cache` metadata key when defining fields.
31
50
  ```ruby
32
- # config/initializers/graphql_cache.rb
33
-
34
- GraphQL::Cache.configure do |config|
35
- config.namespace = 'GraphQL::Cache' # Cache key prefix for keys generated by graphql-cache
36
- config.cache = Rails.cache # The cache object to use for caching
37
- config.logger = Rails.logger # Logger to receive cache-related log messages
38
- config.expiry = 5400 # 90 minutes (in seconds)
39
- config.force = false # Cache override, when true no caching takes place
51
+ module Types
52
+ class Base < GraphQL::Schema::Object
53
+ field_class GraphQL::Cache::Field
54
+ end
40
55
  end
41
56
  ```
42
57
 
58
+ ## Configuration
59
+
60
+ GraphQL Cache can be configured in an initializer:
61
+
62
+ ```ruby
63
+ # config/initializers/graphql_cache.rb
64
+
65
+ GraphQL::Cache.configure do |config|
66
+ config.namespace = 'GraphQL::Cache' # Cache key prefix for keys generated by graphql-cache
67
+ config.cache = Rails.cache # The cache object to use for caching
68
+ config.logger = Rails.logger # Logger to receive cache-related log messages
69
+ config.expiry = 5400 # 90 minutes (in seconds)
70
+ config.force = false # Cache override, when true no caching takes place
71
+ end
72
+ ```
73
+
43
74
  ## Usage
44
75
 
45
- Any object, list, or connection field can be cached by simply adding `cache: true` to the field definition:
76
+ Any object, list, or connection field can be cached by simply adding `cache: true` to the field definition:
46
77
 
47
- ```ruby
48
- field :calculated_field, Int, cache: true
49
- ```
78
+ ```ruby
79
+ field :calculated_field, Int, cache: true
80
+ ```
50
81
 
51
- By default all keys will have an expiration of `GraphQL::Cache.expiry` which defaults to 90 minutes. If you want to set a field-specific expiration time pass a hash to the `cache` parameter like this:
82
+ By default all keys will have an expiration of `GraphQL::Cache.expiry` which defaults to 90 minutes. If you want to set a field-specific expiration time pass a hash to the `cache` parameter like this:
52
83
 
53
- ```ruby
54
- field :calculated_field, Int, cache: { expiry: 10800 } # expires key after 180 minutes
55
- ```
84
+ ```ruby
85
+ field :calculated_field, Int, cache: { expiry: 10800 } # expires key after 180 minutes
86
+ ```
56
87
 
57
- When passing a hash in the `cache` parameter the possible options are:
88
+ When passing a hash in the `cache` parameter the possible options are:
58
89
 
59
- - `expiry`: expiration time for this field's key in seconds (default: 5400)
60
- - `force`: for cache misses on this field (default: false)
61
- - `prefix`: cache key prefix (appended after GraphQL::Cache.namespace)
90
+ - `expiry`: expiration time for this field's key in seconds (default: 5400)
91
+ - `force`: for cache misses on this field (default: false)
92
+ - `prefix`: cache key prefix (appended after GraphQL::Cache.namespace)
62
93
 
63
94
  ## Development
64
95
 
65
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
96
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
66
97
 
67
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
98
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
68
99
 
69
100
  ## Contributing
70
101
 
71
- Bug reports and pull requests are welcome on GitHub at https://github.com/Leanstack/graphql-cache. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
102
+ Bug reports and pull requests are welcome on GitHub at https://github.com/stackshareio/graphql-cache. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
72
103
 
73
104
  ## License
74
105
 
75
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
106
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
76
107
 
77
108
  ## Code of Conduct
78
109
 
79
- Everyone interacting in the Graphql::Cache projects codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/Leanstack/graphql-cache/blob/master/CODE_OF_CONDUCT.md).
110
+ Everyone interacting in the graphql-cache project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/stackshareio/graphql-cache/blob/master/CODE_OF_CONDUCT.md).
data/bin/console CHANGED
@@ -1,14 +1,26 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require "bundler/setup"
4
- require "graphql/cache"
3
+ require 'bundler/setup'
4
+ require 'graphql/cache'
5
+ require 'logger'
6
+ require 'mini_cache'
7
+ require 'sequel'
5
8
 
6
- # You can add fixtures and/or initialization code here to make experimenting
7
- # with your gem easier. You can also use a different console, if you like.
9
+ # Setup MiniCache for in-memory cache for dev/test
10
+ class CacheStore < MiniCache::Store
11
+ alias read get
12
+ alias write set
13
+ alias clear reset
14
+ end
8
15
 
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
- # require "pry"
11
- # Pry.start
16
+ GraphQL::Cache.configure do |config|
17
+ config.cache = CacheStore.new
18
+ config.logger = Logger.new(STDOUT)
19
+ end
12
20
 
13
- require "irb"
14
- IRB.start(__FILE__)
21
+ # required after GraphQL::Cache initialization because dev
22
+ # schema uses cache and logger objects from it.
23
+ require_relative '../test_schema'
24
+
25
+ require "pry"
26
+ Pry.start
@@ -24,10 +24,13 @@ Gem::Specification.new do |s|
24
24
  s.add_development_dependency 'appraisal'
25
25
  s.add_development_dependency 'bundler', '~> 1.16'
26
26
  s.add_development_dependency 'codeclimate-test-reporter'
27
+ s.add_development_dependency 'mini_cache'
27
28
  s.add_development_dependency 'pry'
28
29
  s.add_development_dependency 'rake', '~> 10.0'
29
30
  s.add_development_dependency 'rspec', '~> 3.0'
31
+ s.add_development_dependency 'sequel'
30
32
  s.add_development_dependency 'simplecov'
33
+ s.add_development_dependency 'sqlite3'
31
34
 
32
35
  s.add_dependency 'graphql', '~> 1.8.0'
33
36
  end
data/lib/graphql/cache.rb CHANGED
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'graphql/cache/version'
4
- require 'graphql/cache/middleware'
5
4
  require 'graphql/cache/field'
5
+ require 'graphql/cache/key'
6
6
  require 'graphql/cache/marshal'
7
+ require 'graphql/cache/fetcher'
7
8
 
8
9
  module GraphQL
9
10
  module Cache
@@ -43,20 +44,14 @@ module GraphQL
43
44
  # Default configuration
44
45
  @expiry = 5400
45
46
  @force = false
46
- @namespace = 'GraphQL::Cache'
47
-
48
- # Fetches/writes a value for `key` from the cache
49
- #
50
- # Always evaluates the block unless config[:metadata][:cache] is truthy
51
- #
52
- # @param key [String] the cache key to attempt to fetch
53
- # @param config [Hash] a hash of middleware config values used to marshal cache data
54
- # @option config [Hash] :metadata The metadata collected from the field definition
55
- # @return [Object]
56
- def self.fetch(key, config: {}, &block)
57
- return block.call unless config[:metadata][:cache]
58
-
59
- Marshal[key].read(config, &block)
47
+ @namespace = 'graphql'
48
+
49
+ # Called by plugin framework in graphql-ruby to
50
+ # bootstrap necessary instrumentation and tracing
51
+ # tie-ins
52
+ def self.use(schema_def, options: {})
53
+ fetcher = ::GraphQL::Cache::Fetcher.new
54
+ schema_def.instrument(:field, fetcher)
60
55
  end
61
56
  end
62
57
  end
@@ -0,0 +1,78 @@
1
+ module GraphQL
2
+ module Cache
3
+ # GraphQL objects can't be serialized to cache so we have
4
+ # to maintain an abstraction between the raw cache value
5
+ # and the GraphQL expected object. This class exposes methods
6
+ # for deconstructing an object to be stored in cache
7
+ #
8
+ class Deconstructor
9
+ # The raw value to perform actions on. Could be a raw cached value, or
10
+ # a raw GraphQL Field.
11
+ #
12
+ # @return [Object]
13
+ attr_accessor :raw
14
+
15
+ # A flag indicating the type of object construction to
16
+ # use when building a new GraphQL object. Can be one of
17
+ # 'array', 'collectionproxy', 'relation'. These values
18
+ # have been chosen because it is easy to use the class
19
+ # names of the possible object types for this purpose.
20
+ #
21
+ # @return [String] 'array' or 'collectionproxy' or 'relation'
22
+ attr_accessor :method
23
+
24
+ # Initializer helper that generates a valid `method` string based
25
+ # on `raw.class.name`.
26
+ #
27
+ # @return [Object] A newly initialized GraphQL::Cache::Deconstructor instance
28
+ def self.[](raw)
29
+ build_method = namify(raw.class.name)
30
+ new(raw, build_method)
31
+ end
32
+
33
+ # Ruby-only means of "demodularizing" a string
34
+ def self.namify(str)
35
+ str.split('::').last.downcase
36
+ end
37
+
38
+ def initialize(raw, method)
39
+ self.raw = raw
40
+ self.method = method
41
+ end
42
+
43
+ # Deconstructs a GraphQL field into a cachable value
44
+ #
45
+ # @return [Object] A value suitable for writing to cache
46
+ def perform
47
+ if %(array collectionproxy).include? method
48
+ deconstruct_array(raw)
49
+ elsif raw.class.ancestors.include? GraphQL::Relay::BaseConnection
50
+ raw.nodes
51
+ else
52
+ deconstruct_object(raw)
53
+ end
54
+ end
55
+
56
+ # @private
57
+ def deconstruct_array(raw)
58
+ return [] if raw.empty?
59
+
60
+ if raw.first.class.ancestors.include? GraphQL::Schema::Object
61
+ raw.map(&:object)
62
+ else
63
+ raw
64
+ end
65
+ end
66
+
67
+ # @private
68
+ def deconstruct_object(raw)
69
+ if raw.respond_to?(:object)
70
+ raw.object
71
+ else
72
+ raw
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+
@@ -0,0 +1,29 @@
1
+ module GraphQL
2
+ module Cache
3
+ class Fetcher
4
+ def instrument(type, field)
5
+ old_resolve_proc = field.resolve_proc
6
+
7
+ new_resolve_proc = lambda do |obj, args, ctx|
8
+ unless field.metadata[:cache]
9
+ return old_resolve_proc.call(obj, args, ctx)
10
+ end
11
+
12
+ key = cache_key(obj, args, type, field)
13
+
14
+ Marshal[key].read(field.metadata[:cache]) do
15
+ old_resolve_proc.call(obj, args, ctx)
16
+ end
17
+ end
18
+
19
+ # Return a copy of `field`, with the new resolve proc
20
+ field.redefine { resolve(new_resolve_proc) }
21
+ end
22
+
23
+ # @private
24
+ def cache_key(obj, args, type, field)
25
+ Key.new(obj, args, type, field).to_s
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,88 @@
1
+ module GraphQL
2
+ module Cache
3
+ # Represents a cache key generated from the graphql context
4
+ # provided when initialized
5
+ class Key
6
+ # The resolved parent object (object this resolver method is called on)
7
+ attr_accessor :object
8
+
9
+ # Arguments passed during graphql query execution
10
+ attr_accessor :arguments
11
+
12
+ # The graphql parent type
13
+ attr_accessor :type
14
+
15
+ # The graphql field being resolved
16
+ attr_accessor :field
17
+
18
+ # Metadata passed to the cache key on field definition
19
+ attr_accessor :metadata
20
+
21
+ # Initializes a new Key with the given graphql query context
22
+ def initialize(obj, args, type, field)
23
+ @object = obj.object
24
+ @arguments = args
25
+ @type = type
26
+ @field = field
27
+ @metadata = field.metadata[:cache]
28
+
29
+ @metadata = { cache: @metadata } unless @metadata.is_a?(Hash)
30
+ end
31
+
32
+ # Returns the string representation of this cache key
33
+ # suitable for using as a key when writing to cache
34
+ def to_s
35
+ @to_s ||= [
36
+ GraphQL::Cache.namespace,
37
+ type_clause,
38
+ field_clause,
39
+ arguments_clause,
40
+ object_clause
41
+ ].flatten.compact.join(':')
42
+ end
43
+
44
+ # Produces the portion of the key representing the parent object
45
+ def object_clause
46
+ return nil unless object
47
+
48
+ "#{object.class.name}:#{object_identifier}"
49
+ end
50
+
51
+ # Produces the portion of the key representing the parent type
52
+ def type_clause
53
+ type.name
54
+ end
55
+
56
+ # Produces the portion of the key representing the resolving field
57
+ def field_clause
58
+ field.name
59
+ end
60
+
61
+ # Produces the portion of the key representing the query arguments
62
+ def arguments_clause
63
+ @arguments_clause ||= arguments.to_h.to_a.flatten
64
+ end
65
+
66
+ # @private
67
+ def object_identifier
68
+ case metadata[:key]
69
+ when Symbol
70
+ object.send(metadata[:key])
71
+ when Proc
72
+ metadata[:key].call(object)
73
+ when NilClass
74
+ guess_id
75
+ else
76
+ metadata[:key]
77
+ end
78
+ end
79
+
80
+ # @private
81
+ def guess_id
82
+ return object.cache_key if object.respond_to?(:cache_key)
83
+ return object.id if object.respond_to?(:id)
84
+ object.object_id
85
+ end
86
+ end
87
+ end
88
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'graphql/cache/builder'
3
+ require 'graphql/cache/deconstructor'
4
4
 
5
5
  module GraphQL
6
6
  module Cache
@@ -27,7 +27,7 @@ module GraphQL
27
27
  # Read a value from cache if it exists and re-hydrate it or
28
28
  # execute the block and write it's result to cache
29
29
  #
30
- # @param config [Hash] The middleware resolution config
30
+ # @param config [Hash] The object passed to `cache:` on the field definition
31
31
  # @return [Object]
32
32
  def read(config, &block)
33
33
  cached = cache.read(key)
@@ -37,17 +37,17 @@ module GraphQL
37
37
  write config, &block
38
38
  else
39
39
  logger.debug "Cache hit: (#{key})"
40
- build cached, config
40
+ cached
41
41
  end
42
42
  end
43
43
 
44
44
  # Executes the resolution block and writes the result to cache
45
45
  #
46
- # @see GraphQL::Cache::Builder#deconstruct
46
+ # @see GraphQL::Cache::Deconstruct#perform
47
47
  # @param config [Hash] The middleware resolution config hash
48
48
  def write(config)
49
49
  resolved = yield
50
- document = Builder[resolved].deconstruct
50
+ document = Deconstructor[resolved].perform
51
51
 
52
52
  cache.write(key, document, expires_in: expiry(config))
53
53
  resolved
@@ -55,23 +55,13 @@ module GraphQL
55
55
 
56
56
  # @private
57
57
  def expiry(config)
58
- cache_config = config[:metadata][:cache]
59
-
60
- if cache_config.is_a?(Hash) && cache_config[:expiry]
61
- config[:metadata][:cache][:expiry]
58
+ if config.is_a?(Hash) && config[:expiry]
59
+ config[:expiry]
62
60
  else
63
61
  GraphQL::Cache.expiry
64
62
  end
65
63
  end
66
64
 
67
- # Uses {GraphQL::Cache::Builder} to build a valid GraphQL object
68
- # from a cached value
69
- #
70
- # @return [Object] An object suitable to return from a GraphQL middleware
71
- def build(cached, config)
72
- Builder[cached].build(config)
73
- end
74
-
75
65
  # @private
76
66
  def cache
77
67
  GraphQL::Cache.cache
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module Cache
5
- VERSION = '0.2.5'
5
+ VERSION = '0.3.0'
6
6
  end
7
7
  end
data/test_schema.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'logger'
2
+
3
+ require_relative './test_schema/schema'
4
+ require_relative './test_schema/models'
5
+ require_relative './test_schema/graphql_schema'
6
+ require_relative './test_schema/factories'
7
+
8
+ Factories.bootstrap
9
+ DB.loggers = [GraphQL::Cache.logger]
@@ -0,0 +1,16 @@
1
+ module Factories
2
+ def self.bootstrap
3
+ customer = Customer.create(
4
+ display_name: 'Michael',
5
+ email: 'michael@example.com'
6
+ )
7
+
8
+ Order.create(customer_id: customer.id, number: new_num, total_price_cents: 1399)
9
+ Order.create(customer_id: customer.id, number: new_num, total_price_cents: 1399)
10
+ Order.create(customer_id: customer.id, number: new_num, total_price_cents: 1399)
11
+ end
12
+
13
+ def self.new_num
14
+ Order.count + 1000
15
+ end
16
+ end
@@ -0,0 +1,44 @@
1
+ require 'benchmark'
2
+
3
+ class BaseType < GraphQL::Schema::Object
4
+ field_class GraphQL::Cache::Field
5
+ end
6
+
7
+ class OrderType < BaseType
8
+ field :id, Int, null: false
9
+ field :number, Int, null: true
10
+ field :total_price_cents, Int, null: true
11
+ end
12
+
13
+ class CustomerType < BaseType
14
+ field :display_name, String, null: false
15
+ field :email, String, null: false
16
+ field :orders, OrderType.connection_type, null: false, cache: true
17
+ end
18
+
19
+ class QueryType < BaseType
20
+ field :customer, CustomerType, null: true, cache: true do
21
+ argument :id, ID, 'Unique Identifier for querying a specific user', required: true
22
+ end
23
+
24
+ def customer(id:)
25
+ Customer[id]
26
+ end
27
+ end
28
+
29
+ class CacheSchema < GraphQL::Schema
30
+ query QueryType
31
+
32
+ use GraphQL::Cache
33
+
34
+ def self.resolve_type(_type, obj, _ctx)
35
+ "#{obj.class.name}Type"
36
+ end
37
+
38
+ def self.texecute(*args, **kwargs)
39
+ result = nil
40
+ measurement = Benchmark.measure { result = execute(*args, *kwargs) }
41
+ GraphQL::Cache.logger.debug("Query executed in #{measurement.real}")
42
+ result
43
+ end
44
+ end
@@ -0,0 +1,7 @@
1
+ class Order < Sequel::Model
2
+ one_to_one :customer
3
+ end
4
+
5
+ class Customer < Sequel::Model
6
+ one_to_many :orders
7
+ end
@@ -0,0 +1,16 @@
1
+ require 'sequel'
2
+
3
+ DB = Sequel.sqlite(logger: Logger.new('/dev/null'))
4
+
5
+ DB.create_table :customers do
6
+ primary_key :id
7
+ String :display_name
8
+ String :email
9
+ end
10
+
11
+ DB.create_table :orders do
12
+ primary_key :id
13
+ Integer :customer_id
14
+ Integer :number
15
+ Integer :total_price_cents
16
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql-cache
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.5
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Kelly
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-06-25 00:00:00.000000000 Z
11
+ date: 2018-07-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: appraisal
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: mini_cache
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: pry
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -94,6 +108,20 @@ dependencies:
94
108
  - - "~>"
95
109
  - !ruby/object:Gem::Version
96
110
  version: '3.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: sequel
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
97
125
  - !ruby/object:Gem::Dependency
98
126
  name: simplecov
99
127
  requirement: !ruby/object:Gem::Requirement
@@ -108,6 +136,20 @@ dependencies:
108
136
  - - ">="
109
137
  - !ruby/object:Gem::Version
110
138
  version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: sqlite3
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
111
153
  - !ruby/object:Gem::Dependency
112
154
  name: graphql
113
155
  requirement: !ruby/object:Gem::Requirement
@@ -149,12 +191,18 @@ files:
149
191
  - gemfiles/graphql_1.8.gemfile
150
192
  - graphql-cache.gemspec
151
193
  - lib/graphql/cache.rb
152
- - lib/graphql/cache/builder.rb
194
+ - lib/graphql/cache/deconstructor.rb
195
+ - lib/graphql/cache/fetcher.rb
153
196
  - lib/graphql/cache/field.rb
197
+ - lib/graphql/cache/key.rb
154
198
  - lib/graphql/cache/marshal.rb
155
- - lib/graphql/cache/middleware.rb
156
199
  - lib/graphql/cache/rails.rb
157
200
  - lib/graphql/cache/version.rb
201
+ - test_schema.rb
202
+ - test_schema/factories.rb
203
+ - test_schema/graphql_schema.rb
204
+ - test_schema/models.rb
205
+ - test_schema/schema.rb
158
206
  homepage: https://github.com/Leanstack/graphql-cache
159
207
  licenses:
160
208
  - MIT
@@ -175,7 +223,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
175
223
  version: '0'
176
224
  requirements: []
177
225
  rubyforge_project:
178
- rubygems_version: 2.7.3
226
+ rubygems_version: 2.7.6
179
227
  signing_key:
180
228
  specification_version: 4
181
229
  summary: Caching middleware for graphql-ruby
@@ -1,133 +0,0 @@
1
- module GraphQL
2
- module Cache
3
- # GraphQL objects can't be serialized to cache so we have
4
- # to maintain an abstraction between the raw cache value
5
- # and the GraphQL expected object. This class exposes methods
6
- # for both deconstructing an object to be stored in cache
7
- # and re-hydrating a GraphQL object from raw cache values
8
- #
9
- class Builder
10
- # The raw value to perform actions on. Could be a raw cached value, or
11
- # a raw GraphQL Field.
12
- #
13
- # @return [Object]
14
- attr_accessor :raw
15
-
16
- # A flag indicating the type of object construction to
17
- # use when building a new GraphQL object. Can be one of
18
- # 'array', 'collectionproxy', 'relation'. These values
19
- # have been chosen because it is easy to use the class
20
- # names of the possible object types for this purpose.
21
- #
22
- # @return [String] 'array' or 'collectionproxy' or 'relation'
23
- attr_accessor :method
24
-
25
- # The middleware config hash describing a field's resolution
26
- #
27
- # @see GraphQL::Cache::Middleware#initialize
28
- # @return [Hash]
29
- attr_accessor :config
30
-
31
- # Initializer helper that generates a valid `method` string based
32
- # on `raw.class.name`.
33
- #
34
- # @return [Object] A newly initialized GraphQL::Cache::Builder instance
35
- def self.[](raw)
36
- build_method = namify(raw.class.name)
37
- new(raw, build_method)
38
- end
39
-
40
- # Ruby-only means of "demodularizing" a string
41
- def self.namify(str)
42
- str.split('::').last.downcase
43
- end
44
-
45
- def initialize(raw, method)
46
- self.raw = raw
47
- self.method = method
48
- end
49
-
50
- # Builds a compitable GraphQL object based on the resolution config
51
- #
52
- # @return [Object] An object suitable for returning from a GraphQL middlware call
53
- def build(config)
54
- self.config = config
55
-
56
- return build_array if method == 'array'
57
- return build_relation if method == 'collectionproxy' || method == 'relation'
58
- build_object
59
- end
60
-
61
- # Deconstructs a GraphQL field into a cachable value
62
- #
63
- # @return [Object] A value suitable for writing to cache
64
- def deconstruct
65
- return deconstruct_array(raw) if raw.class == Array
66
- return raw.nodes if raw.class.ancestors.include? GraphQL::Relay::BaseConnection
67
-
68
- deconstruct_object(raw)
69
- end
70
-
71
- # @private
72
- def deconstruct_object(raw)
73
- if raw.respond_to?(:object)
74
- raw.object
75
- else
76
- raw
77
- end
78
- end
79
-
80
- # @private
81
- def deconstruct_array(raw)
82
- return [] if raw.empty?
83
-
84
- if raw.first.class.ancestors.include? GraphQL::Schema::Object
85
- raw.map(&:object)
86
- else
87
- raw
88
- end
89
- end
90
-
91
- # @private
92
- def build_relation
93
- GraphQL::Relay::BaseConnection.connection_for_nodes(raw).new(
94
- raw,
95
- config[:field_args],
96
- field: config[:query_context].field,
97
- parent: config[:parent_object].object,
98
- context: config[:query_context]
99
- )
100
- end
101
-
102
- # @private
103
- def build_array
104
- gql_def = config[:field_definition].type.unwrap.graphql_definition
105
-
106
- raw.map do |item|
107
- if gql_def.kind.name == 'OBJECT'
108
- gql_def.metadata[:type_class].authorized_new(
109
- item,
110
- config[:query_context]
111
- )
112
- else
113
- item
114
- end
115
- end
116
- end
117
-
118
- # @private
119
- def build_object
120
- gql_def = config[:field_definition].type.unwrap.graphql_definition
121
- klass = gql_def.metadata[:type_class]
122
- if gql_def.kind.name == 'OBJECT' && klass
123
- klass.authorized_new(
124
- raw,
125
- config[:query_context]
126
- )
127
- else
128
- raw
129
- end
130
- end
131
- end
132
- end
133
- end
@@ -1,90 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module GraphQL
4
- module Cache
5
- # graphql-ruby middleware to wrap resolvers for caching
6
- class Middleware
7
- attr_accessor :parent_type, :parent_object, :object, :cache,
8
- :field_definition, :field_args, :query_context
9
-
10
- # Called by graphql-ruby during middleware processing
11
- def self.call(*args, &block)
12
- new(*args).call(&block)
13
- end
14
-
15
- def initialize(parent_type,
16
- parent_object,
17
- field_definition,
18
- field_args,
19
- query_context)
20
- self.parent_type = parent_type
21
- self.parent_object = parent_object
22
- self.field_definition = field_definition
23
- self.field_args = field_args
24
- self.query_context = query_context
25
- self.cache = ::GraphQL::Cache.cache
26
-
27
- return unless parent_object
28
-
29
- self.object = parent_object.nodes if parent_object.respond_to? :nodes
30
- self.object = parent_object.object if parent_object.respond_to? :object
31
- end
32
-
33
- # @private
34
- def cache_config
35
- {
36
- parent_type: parent_type,
37
- parent_object: parent_object,
38
- field_definition: field_definition,
39
- field_args: field_args,
40
- query_context: query_context,
41
- object: object
42
- }.merge metadata_hash
43
- end
44
-
45
- # @private
46
- def metadata_hash
47
- {
48
- metadata: {
49
- cache: field_definition.metadata[:cache]
50
- }
51
- }
52
- end
53
-
54
- # The primary caching entry point
55
- #
56
- # @return [Object]
57
- def call(&block)
58
- GraphQL::Cache.fetch(
59
- cache_key,
60
- config: cache_config,
61
- &block
62
- )
63
- end
64
-
65
- # @private
66
- def cache_key
67
- @cache_key ||= [
68
- GraphQL::Cache.namespace,
69
- object_key,
70
- field_definition.name,
71
- field_args.keys
72
- ].flatten
73
- end
74
-
75
- # @private
76
- def object_key
77
- return nil unless object
78
-
79
- "#{object.class.name}:#{id_from_object}"
80
- end
81
-
82
- # @private
83
- def id_from_object
84
- return object.id if object.respond_to? :id
85
- return object.fetch(:id, nil) if object.respond_to? :fetch
86
- return object.fetch('id', nil) if object.respond_to? :fetch
87
- end
88
- end
89
- end
90
- end