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 +4 -4
- data/Gemfile.lock +7 -1
- data/README.md +71 -40
- data/bin/console +21 -9
- data/graphql-cache.gemspec +3 -0
- data/lib/graphql/cache.rb +10 -15
- data/lib/graphql/cache/deconstructor.rb +78 -0
- data/lib/graphql/cache/fetcher.rb +29 -0
- data/lib/graphql/cache/key.rb +88 -0
- data/lib/graphql/cache/marshal.rb +7 -17
- data/lib/graphql/cache/version.rb +1 -1
- data/test_schema.rb +9 -0
- data/test_schema/factories.rb +16 -0
- data/test_schema/graphql_schema.rb +44 -0
- data/test_schema/models.rb +7 -0
- data/test_schema/schema.rb +16 -0
- metadata +53 -5
- data/lib/graphql/cache/builder.rb +0 -133
- data/lib/graphql/cache/middleware.rb +0 -90
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fb2192273e278a5586abc0e640ef6057f28e072f38b46740a5e0ebf6364f7be1
|
4
|
+
data.tar.gz: 59d4fd50a85cbc58a6109373f4fb4dca69aa54548960e7be62af5cb98b4cf6cd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
+
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
|
-
|
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
|
-
|
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
|
-
|
20
|
+
Add this line to your application's Gemfile:
|
9
21
|
|
10
|
-
|
11
|
-
|
12
|
-
|
22
|
+
```ruby
|
23
|
+
gem 'graphql-cache'
|
24
|
+
```
|
13
25
|
|
14
|
-
|
26
|
+
And then execute:
|
15
27
|
|
16
|
-
|
28
|
+
```sh
|
29
|
+
$ bundle
|
30
|
+
```
|
17
31
|
|
18
|
-
|
32
|
+
Or install it yourself as:
|
19
33
|
|
20
|
-
|
34
|
+
```sh
|
35
|
+
$ gem install graphql-cache
|
36
|
+
```
|
21
37
|
|
22
38
|
## Setup
|
23
39
|
|
24
|
-
|
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
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
76
|
+
Any object, list, or connection field can be cached by simply adding `cache: true` to the field definition:
|
46
77
|
|
47
|
-
|
48
|
-
|
49
|
-
|
78
|
+
```ruby
|
79
|
+
field :calculated_field, Int, cache: true
|
80
|
+
```
|
50
81
|
|
51
|
-
|
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
|
-
|
54
|
-
|
55
|
-
|
84
|
+
```ruby
|
85
|
+
field :calculated_field, Int, cache: { expiry: 10800 } # expires key after 180 minutes
|
86
|
+
```
|
56
87
|
|
57
|
-
|
88
|
+
When passing a hash in the `cache` parameter the possible options are:
|
58
89
|
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
4
|
-
require
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'graphql/cache'
|
5
|
+
require 'logger'
|
6
|
+
require 'mini_cache'
|
7
|
+
require 'sequel'
|
5
8
|
|
6
|
-
#
|
7
|
-
|
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
|
-
|
10
|
-
|
11
|
-
|
16
|
+
GraphQL::Cache.configure do |config|
|
17
|
+
config.cache = CacheStore.new
|
18
|
+
config.logger = Logger.new(STDOUT)
|
19
|
+
end
|
12
20
|
|
13
|
-
|
14
|
-
|
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
|
data/graphql-cache.gemspec
CHANGED
@@ -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 = '
|
47
|
-
|
48
|
-
#
|
49
|
-
#
|
50
|
-
#
|
51
|
-
|
52
|
-
|
53
|
-
|
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/
|
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
|
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
|
-
|
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::
|
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 =
|
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
|
-
|
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
|
data/test_schema.rb
ADDED
@@ -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,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.
|
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-
|
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/
|
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.
|
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
|