graphql-fragment_cache 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +11 -0
- data/LICENSE.txt +21 -0
- data/README.md +283 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/.rbnext/2.7/graphql/fragment_cache/cache_key_builder.rb +85 -0
- data/lib/.rbnext/2.7/graphql/fragment_cache/cacher.rb +20 -0
- data/lib/.rbnext/2.7/graphql/fragment_cache/ext/graphql_cache_key.rb +93 -0
- data/lib/graphql-fragment_cache.rb +6 -0
- data/lib/graphql/fragment_cache.rb +59 -0
- data/lib/graphql/fragment_cache/cache_key_builder.rb +85 -0
- data/lib/graphql/fragment_cache/cacher.rb +20 -0
- data/lib/graphql/fragment_cache/ext/context_fragments.rb +19 -0
- data/lib/graphql/fragment_cache/ext/graphql_cache_key.rb +93 -0
- data/lib/graphql/fragment_cache/field_extension.rb +54 -0
- data/lib/graphql/fragment_cache/fragment.rb +37 -0
- data/lib/graphql/fragment_cache/instrumentation.rb +21 -0
- data/lib/graphql/fragment_cache/memory_store.rb +56 -0
- data/lib/graphql/fragment_cache/object.rb +16 -0
- data/lib/graphql/fragment_cache/object_helpers.rb +24 -0
- data/lib/graphql/fragment_cache/rails/cache_key_builder.rb +16 -0
- data/lib/graphql/fragment_cache/railtie.rb +28 -0
- data/lib/graphql/fragment_cache/schema_patch.rb +14 -0
- data/lib/graphql/fragment_cache/version.rb +7 -0
- metadata +168 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 385b7910c87bd9fe82d32d4d1e6ac45c9d62600b2cbcc1422445c47fca70d021
|
4
|
+
data.tar.gz: a99f8ef1ffaa3026ee2f183b74abc1dea8fab099f0a2c811de1f211adf4e176d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3e1936a4c8d932cf022ffaa45c79d67b3217729f7b722f3209995706a9b1c6c4346c272078c1c9404707a1833b0ef786399bde0b76431eedead4ed0cb7cfcfa0
|
7
|
+
data.tar.gz: 2a550ee8ba377ec9d199266fb75bdd9fe51bb46595c8839d5d0a9a43f9c3874b897d73b718fb66b4f2891721dbac56a436d05f66ad197f5ca31a973f6c592b41
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# Change log
|
2
|
+
|
3
|
+
## master
|
4
|
+
|
5
|
+
## 0.1.0 (2020-04-14)
|
6
|
+
|
7
|
+
- Initial version ([@DmitryTsepelev][], [@palkan][], [@ssnickolay][])
|
8
|
+
|
9
|
+
[@DmitryTsepelev]: https://github.com/DmitryTsepelev
|
10
|
+
[@palkan]: https://github.com/palkan
|
11
|
+
[@ssnickolay]: https://github.com/ssnickolay
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2020 DmitryTsepelev
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,283 @@
|
|
1
|
+
# GraphQL::FragmentCache ![CI](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/workflows/CI/badge.svg?branch=master)
|
2
|
+
|
3
|
+
`GraphQL::FragmentCache` powers up [graphql-ruby](https://graphql-ruby.org) with the ability to cache response _fragments_: you can mark any field as cached and it will never be resolved again (at least, while cache is valid). For instance, the following code caches `title` for each post:
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
class PostType < BaseObject
|
7
|
+
field :id, ID, null: false
|
8
|
+
field :title, String, null: false, cache_fragment: true
|
9
|
+
end
|
10
|
+
```
|
11
|
+
|
12
|
+
<p align="center">
|
13
|
+
<a href="https://evilmartians.com/?utm_source=graphql-ruby-fragment_cache">
|
14
|
+
<img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54">
|
15
|
+
</a>
|
16
|
+
</p>
|
17
|
+
|
18
|
+
## Getting started
|
19
|
+
|
20
|
+
Add the gem to your Gemfile `gem 'graphql-fragment_cache'` and add the plugin to your schema class (make sure to turn interpreter mode on with AST analysis!):
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
class GraphqSchema < GraphQL::Schema
|
24
|
+
use GraphQL::Execution::Interpreter
|
25
|
+
use GraphQL::Analysis::AST
|
26
|
+
|
27
|
+
use GraphQL::FragmentCache
|
28
|
+
|
29
|
+
query QueryType
|
30
|
+
end
|
31
|
+
```
|
32
|
+
|
33
|
+
Include `GraphQL::FragmentCache::Object` to your base type class:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
class BaseType < GraphQL::Schema::Object
|
37
|
+
include GraphQL::FragmentCache::Object
|
38
|
+
end
|
39
|
+
```
|
40
|
+
|
41
|
+
Now you can add `cache_fragment:` option to your fields to turn caching on:
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
class PostType < BaseObject
|
45
|
+
field :id, ID, null: false
|
46
|
+
field :title, String, null: false, cache_fragment: true
|
47
|
+
end
|
48
|
+
```
|
49
|
+
|
50
|
+
Alternatively, you can use `cache_fragment` method inside resolvers:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
class QueryType < BaseObject
|
54
|
+
field :post, PostType, null: true do
|
55
|
+
argument :id, ID, required: true
|
56
|
+
end
|
57
|
+
|
58
|
+
def post(id:)
|
59
|
+
cache_fragment { Post.find(id) }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
```
|
63
|
+
|
64
|
+
## Cache key generation
|
65
|
+
|
66
|
+
Cache keys consist of implicit and explicit (provided by user) parts.
|
67
|
+
|
68
|
+
### Implicit cache key
|
69
|
+
|
70
|
+
Implicit part of a cache key (its prefix) contains the information about the schema and the current query. It includes:
|
71
|
+
|
72
|
+
- Hex gsdigest of the schema definition (to make sure cache is cleared when the schema changes).
|
73
|
+
- The current query fingerprint consisting of a _path_ to the field, arguments information and the selections set.
|
74
|
+
|
75
|
+
Let's take a look at the example:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
query = <<~GQL
|
79
|
+
query {
|
80
|
+
post(id: 1) {
|
81
|
+
id
|
82
|
+
title
|
83
|
+
cachedAuthor {
|
84
|
+
id
|
85
|
+
name
|
86
|
+
}
|
87
|
+
}
|
88
|
+
}
|
89
|
+
GQL
|
90
|
+
|
91
|
+
schema_cache_key = GraphqSchema.schema_cache_key
|
92
|
+
|
93
|
+
path_cache_key = "post(id:1)/cachedAuthor"
|
94
|
+
selections_cache_key = "[#{%w[id name].join(".")}]"
|
95
|
+
|
96
|
+
query_cache_key = Digest::SHA1.hexdigest("#{path_cache_key}#{selections_cache_key}")
|
97
|
+
|
98
|
+
cache_key = "#{schema_cache_key}/#{query_cache_key}"
|
99
|
+
```
|
100
|
+
|
101
|
+
You can override `schema_cache_key` or `query_cache_key` by passing parameters to the `cache_fragment` calls:
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
class QueryType < BaseObject
|
105
|
+
field :post, PostType, null: true do
|
106
|
+
argument :id, ID, required: true
|
107
|
+
end
|
108
|
+
|
109
|
+
def post(id:)
|
110
|
+
cache_fragment(query_cache_key: "post(#{id})") { Post.find(id) }
|
111
|
+
end
|
112
|
+
end
|
113
|
+
```
|
114
|
+
|
115
|
+
Same for the option:
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
class PostType < BaseObject
|
119
|
+
field :id, ID, null: false
|
120
|
+
field :title, String, null: false, cache_fragment: {query_cache_key: "post_title"}
|
121
|
+
end
|
122
|
+
```
|
123
|
+
|
124
|
+
### User-provided cache key
|
125
|
+
|
126
|
+
In most cases you want your cache key to depend on the resolved object (say, `ActiveRecord` model). You can do that by passing an argument to the `#cache_fragment` method in a similar way to Rails views [`#cache` method](https://guides.rubyonrails.org/caching_with_rails.html#fragment-caching):
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
def post(id:)
|
130
|
+
post = Post.find(id)
|
131
|
+
cache_fragment(post) { post }
|
132
|
+
end
|
133
|
+
```
|
134
|
+
|
135
|
+
You can pass arrays as well to build a compound cache key:
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
def post(id:)
|
139
|
+
post = Post.find(id)
|
140
|
+
cache_fragment([post, current_account]) { post }
|
141
|
+
end
|
142
|
+
```
|
143
|
+
|
144
|
+
You can omit the block if its return value is the same as the cached object:
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
# the following line
|
148
|
+
cache_fragment(post)
|
149
|
+
# is the same as
|
150
|
+
cache_fragment(post) { post }
|
151
|
+
```
|
152
|
+
|
153
|
+
When using `cache_fragment:` option, it's only possible to use the resolved value as a cache key by setting:
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
field :post, PostType, null: true, cache_fragment: {cache_key: :object} do
|
157
|
+
argument :id, ID, required: true
|
158
|
+
end
|
159
|
+
|
160
|
+
# this is equal to
|
161
|
+
def post(id:)
|
162
|
+
cache_fragment(Post.find(id))
|
163
|
+
end
|
164
|
+
```
|
165
|
+
|
166
|
+
Also, you can pass `:value` to the `cache_key:` argument to use the returned value to build a key:
|
167
|
+
|
168
|
+
```ruby
|
169
|
+
field :post, PostType, null: true, cache_fragment: {cache_key: :value} do
|
170
|
+
argument :id, ID, required: true
|
171
|
+
end
|
172
|
+
|
173
|
+
# this is equal to
|
174
|
+
def post(id:)
|
175
|
+
post = Post.find(id)
|
176
|
+
cache_fragment(post) { post }
|
177
|
+
end
|
178
|
+
```
|
179
|
+
|
180
|
+
The way cache key part is generated for the passed argument is the following:
|
181
|
+
|
182
|
+
- Use `#graphql_cache_key` if implemented.
|
183
|
+
- Use `#cache_key` (or `#cache_key_with_version` for modern Rails) if implemented.
|
184
|
+
- Use `self.to_s` for _primitive_ types (strings, symbols, numbers, booleans).
|
185
|
+
- Raise `ArgumentError` if none of the above.
|
186
|
+
|
187
|
+
### Context cache key
|
188
|
+
|
189
|
+
By default, we do not take context into account when calculating cache keys. That's because caching is more efficient when it's _context-free_.
|
190
|
+
|
191
|
+
However, if you want some fields to be cached per context, you can do that either by passing context objects directly to the `#cache_fragment` method (see above) or by adding a `context_key` option to `cache_fragment:`.
|
192
|
+
|
193
|
+
For instance, imagine a query that allows the current user's social profiles:
|
194
|
+
|
195
|
+
```gql
|
196
|
+
query {
|
197
|
+
socialProfiles {
|
198
|
+
provider
|
199
|
+
id
|
200
|
+
}
|
201
|
+
}
|
202
|
+
```
|
203
|
+
|
204
|
+
You can cache the result using the context (`context[:user]`) as a cache key:
|
205
|
+
|
206
|
+
```ruby
|
207
|
+
class QueryType < BaseObject
|
208
|
+
field :social_profiles, [SocialProfileType], null: false, cache_fragment: {context_key: :user}
|
209
|
+
|
210
|
+
def social_profiles
|
211
|
+
context[:user].social_profiles
|
212
|
+
end
|
213
|
+
end
|
214
|
+
```
|
215
|
+
|
216
|
+
This is equal to using `#cache_fragment` the following way:
|
217
|
+
|
218
|
+
```ruby
|
219
|
+
class QueryType < BaseObject
|
220
|
+
field :social_profiles, [SocialProfileType], null: false
|
221
|
+
|
222
|
+
def social_profiles
|
223
|
+
cache_fragment(context[:user]) { context[:user].social_profiles }
|
224
|
+
end
|
225
|
+
end
|
226
|
+
```
|
227
|
+
|
228
|
+
## Cache storage and options
|
229
|
+
|
230
|
+
It's up to your to decide which caching engine to use, all you need is to configure the cache store:
|
231
|
+
|
232
|
+
```ruby
|
233
|
+
GraphQL::FragmentCache.cache_store = MyCacheStore.new
|
234
|
+
```
|
235
|
+
|
236
|
+
Or, in Rails:
|
237
|
+
|
238
|
+
```ruby
|
239
|
+
# config/application.rb (or config/environments/<environment>.rb)
|
240
|
+
Rails.application.configure do |config|
|
241
|
+
# arguments and options are the same as for `config.cache_store`
|
242
|
+
config.graphql_fragment_cache.store = :redis_cache_store
|
243
|
+
end
|
244
|
+
```
|
245
|
+
|
246
|
+
⚠️ Cache store must implement `#read(key)` and `#write(key, value, **options)` methods.
|
247
|
+
|
248
|
+
The gem provides only in-memory store out-of-the-box (`GraphQL::FragmentCache::MemoryStore`). It's used by default.
|
249
|
+
|
250
|
+
You can pass store-specific options to `#cache_fragment` or `cache_fragment:`. For example, to set expiration (assuming the store's `#write` method supports `expires_in` option):
|
251
|
+
|
252
|
+
```ruby
|
253
|
+
class PostType < BaseObject
|
254
|
+
field :id, ID, null: false
|
255
|
+
field :title, String, null: false, cache_fragment: {expires_in: 5.minutes}
|
256
|
+
end
|
257
|
+
|
258
|
+
class QueryType < BaseObject
|
259
|
+
field :post, PostType, null: true do
|
260
|
+
argument :id, ID, required: true
|
261
|
+
end
|
262
|
+
|
263
|
+
def post(id:)
|
264
|
+
cache_fragment(expires_in: 5.minutes) { Post.find(id) }
|
265
|
+
end
|
266
|
+
end
|
267
|
+
```
|
268
|
+
|
269
|
+
## Limitations
|
270
|
+
|
271
|
+
- [Field aliases](https://spec.graphql.org/June2018/#sec-Field-Alias) are not currently supported (take a look at the failing spec [here](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/7))
|
272
|
+
|
273
|
+
## Credits
|
274
|
+
|
275
|
+
Based on the original [gist](https://gist.github.com/palkan/faad9f6ff1db16fcdb1c071ec50e4190) by [@palkan](https://github.com/palkan) and [@ssnickolay](https://github.com/ssnickolay).
|
276
|
+
|
277
|
+
## Contributing
|
278
|
+
|
279
|
+
Bug reports and pull requests are welcome on GitHub at [https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache).
|
280
|
+
|
281
|
+
## License
|
282
|
+
|
283
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "graphql/ruby/fragment_cache"
|
5
|
+
|
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.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "digest"
|
5
|
+
|
6
|
+
using RubyNext
|
7
|
+
|
8
|
+
module GraphQL
|
9
|
+
module FragmentCache
|
10
|
+
using Ext
|
11
|
+
|
12
|
+
using(Module.new {
|
13
|
+
refine Array do
|
14
|
+
def to_selections_key
|
15
|
+
map { |val|
|
16
|
+
children = val.selections.empty? ? "" : "[#{val.selections.to_selections_key}]"
|
17
|
+
"#{val.field.name}#{children}"
|
18
|
+
}.join(".")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
})
|
22
|
+
|
23
|
+
# Builds cache key for fragment
|
24
|
+
class CacheKeyBuilder
|
25
|
+
using RubyNext
|
26
|
+
|
27
|
+
class << self
|
28
|
+
def call(**options)
|
29
|
+
new(**options).build
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
attr_reader :query, :path, :object, :schema
|
34
|
+
|
35
|
+
def initialize(object: nil, query:, path:, **options)
|
36
|
+
@object = object
|
37
|
+
@query = query
|
38
|
+
@schema = query.schema
|
39
|
+
@path = path
|
40
|
+
@options = options
|
41
|
+
end
|
42
|
+
|
43
|
+
def build
|
44
|
+
Digest::SHA1.hexdigest("#{schema_cache_key}/#{query_cache_key}").then do |base_key|
|
45
|
+
next base_key unless object
|
46
|
+
"#{base_key}/#{object_key(object)}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def schema_cache_key
|
53
|
+
@options.fetch(:schema_cache_key, schema.schema_cache_key)
|
54
|
+
end
|
55
|
+
|
56
|
+
def query_cache_key
|
57
|
+
@options.fetch(:query_cache_key, "#{path_cache_key}[#{selections_cache_key}]")
|
58
|
+
end
|
59
|
+
|
60
|
+
def selections_cache_key
|
61
|
+
current_root =
|
62
|
+
path.reduce(query.lookahead) { |lkhd, name| lkhd.selection(name) }
|
63
|
+
|
64
|
+
current_root.selections.to_selections_key
|
65
|
+
end
|
66
|
+
|
67
|
+
def path_cache_key
|
68
|
+
lookahead = query.lookahead
|
69
|
+
|
70
|
+
path.map { |field_name|
|
71
|
+
lookahead = lookahead.selection(field_name)
|
72
|
+
|
73
|
+
next field_name if lookahead.arguments.empty?
|
74
|
+
|
75
|
+
args = lookahead.arguments.map { |_1, _2| "#{_1}:#{_2}" }.sort.join(",")
|
76
|
+
"#{field_name}(#{args})"
|
77
|
+
}.join("/")
|
78
|
+
end
|
79
|
+
|
80
|
+
def object_key(obj)
|
81
|
+
obj._graphql_cache_key
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module FragmentCache
|
5
|
+
using Ext
|
6
|
+
|
7
|
+
# Saves resolved fragment values to cache store
|
8
|
+
module Cacher
|
9
|
+
class << self
|
10
|
+
def call(query)
|
11
|
+
return unless query.context.fragments?
|
12
|
+
|
13
|
+
final_value = query.context.namespace(:interpreter)[:runtime].final_value
|
14
|
+
|
15
|
+
query.context.fragments.each { |_1| _1.persist(final_value) }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module FragmentCache
|
5
|
+
module Ext
|
6
|
+
# Adds #_graphql_cache_key method to Object,
|
7
|
+
# which just call #graphql_cache_key or #cache_key.
|
8
|
+
#
|
9
|
+
# For other core classes returns string representation.
|
10
|
+
#
|
11
|
+
# Raises ArgumentError otherwise.
|
12
|
+
#
|
13
|
+
# We use a refinement to avoid case/if statements for type checking
|
14
|
+
refine Object do
|
15
|
+
def _graphql_cache_key
|
16
|
+
return graphql_cache_key if respond_to?(:graphql_cache_key)
|
17
|
+
return cache_key if respond_to?(:cache_key)
|
18
|
+
return to_a._graphql_cache_key if respond_to?(:to_a)
|
19
|
+
|
20
|
+
to_s
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
refine Array do
|
25
|
+
def _graphql_cache_key
|
26
|
+
map { |_1| _1._graphql_cache_key }.join("/")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
refine NilClass do
|
31
|
+
def _graphql_cache_key
|
32
|
+
""
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
refine TrueClass do
|
37
|
+
def _graphql_cache_key
|
38
|
+
"t"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
refine FalseClass do
|
43
|
+
def _graphql_cache_key
|
44
|
+
"f"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
refine String do
|
49
|
+
def _graphql_cache_key
|
50
|
+
self
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
refine Symbol do
|
55
|
+
def _graphql_cache_key
|
56
|
+
to_s
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
if RUBY_PLATFORM.match?(/java/i)
|
61
|
+
refine Integer do
|
62
|
+
def _graphql_cache_key
|
63
|
+
to_s
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
refine Float do
|
68
|
+
def _graphql_cache_key
|
69
|
+
to_s
|
70
|
+
end
|
71
|
+
end
|
72
|
+
else
|
73
|
+
refine Numeric do
|
74
|
+
def _graphql_cache_key
|
75
|
+
to_s
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
refine Time do
|
81
|
+
def _graphql_cache_key
|
82
|
+
to_s
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
refine Module do
|
87
|
+
def _graphql_cache_key
|
88
|
+
name
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "graphql"
|
4
|
+
|
5
|
+
require "graphql/fragment_cache/ext/context_fragments"
|
6
|
+
require "graphql/fragment_cache/ext/graphql_cache_key"
|
7
|
+
|
8
|
+
require "graphql/fragment_cache/schema_patch"
|
9
|
+
require "graphql/fragment_cache/object"
|
10
|
+
require "graphql/fragment_cache/instrumentation"
|
11
|
+
|
12
|
+
require "graphql/fragment_cache/memory_store"
|
13
|
+
|
14
|
+
require "graphql/fragment_cache/version"
|
15
|
+
require "graphql/fragment_cache/railtie" if defined?(Rails::Railtie)
|
16
|
+
|
17
|
+
module GraphQL
|
18
|
+
# Plugin definition
|
19
|
+
module FragmentCache
|
20
|
+
class << self
|
21
|
+
attr_reader :cache_store
|
22
|
+
|
23
|
+
def use(schema_defn, options = {})
|
24
|
+
verify_interpreter!(schema_defn)
|
25
|
+
|
26
|
+
schema_defn.instrument(:query, Instrumentation)
|
27
|
+
schema_defn.extend(SchemaPatch)
|
28
|
+
end
|
29
|
+
|
30
|
+
def cache_store=(store)
|
31
|
+
unless store.respond_to?(:read)
|
32
|
+
raise ArgumentError, "Store must implement #read(key) method"
|
33
|
+
end
|
34
|
+
|
35
|
+
unless store.respond_to?(:write)
|
36
|
+
raise ArgumentError, "Store must implement #write(key, val, **options) method"
|
37
|
+
end
|
38
|
+
|
39
|
+
@cache_store = store
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def verify_interpreter!(schema_defn)
|
45
|
+
unless schema_defn.interpreter?
|
46
|
+
raise StandardError,
|
47
|
+
"GraphQL::Execution::Interpreter should be enabled for fragment caching"
|
48
|
+
end
|
49
|
+
|
50
|
+
unless schema_defn.analysis_engine == GraphQL::Analysis::AST
|
51
|
+
raise StandardError,
|
52
|
+
"GraphQL::Analysis::AST should be enabled for fragment caching"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
self.cache_store = MemoryStore.new
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "digest"
|
5
|
+
|
6
|
+
using RubyNext
|
7
|
+
|
8
|
+
module GraphQL
|
9
|
+
module FragmentCache
|
10
|
+
using Ext
|
11
|
+
|
12
|
+
using(Module.new {
|
13
|
+
refine Array do
|
14
|
+
def to_selections_key
|
15
|
+
map { |val|
|
16
|
+
children = val.selections.empty? ? "" : "[#{val.selections.to_selections_key}]"
|
17
|
+
"#{val.field.name}#{children}"
|
18
|
+
}.join(".")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
})
|
22
|
+
|
23
|
+
# Builds cache key for fragment
|
24
|
+
class CacheKeyBuilder
|
25
|
+
using RubyNext
|
26
|
+
|
27
|
+
class << self
|
28
|
+
def call(**options)
|
29
|
+
new(**options).build
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
attr_reader :query, :path, :object, :schema
|
34
|
+
|
35
|
+
def initialize(object: nil, query:, path:, **options)
|
36
|
+
@object = object
|
37
|
+
@query = query
|
38
|
+
@schema = query.schema
|
39
|
+
@path = path
|
40
|
+
@options = options
|
41
|
+
end
|
42
|
+
|
43
|
+
def build
|
44
|
+
Digest::SHA1.hexdigest("#{schema_cache_key}/#{query_cache_key}").then do |base_key|
|
45
|
+
next base_key unless object
|
46
|
+
"#{base_key}/#{object_key(object)}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def schema_cache_key
|
53
|
+
@options.fetch(:schema_cache_key, schema.schema_cache_key)
|
54
|
+
end
|
55
|
+
|
56
|
+
def query_cache_key
|
57
|
+
@options.fetch(:query_cache_key, "#{path_cache_key}[#{selections_cache_key}]")
|
58
|
+
end
|
59
|
+
|
60
|
+
def selections_cache_key
|
61
|
+
current_root =
|
62
|
+
path.reduce(query.lookahead) { |lkhd, name| lkhd.selection(name) }
|
63
|
+
|
64
|
+
current_root.selections.to_selections_key
|
65
|
+
end
|
66
|
+
|
67
|
+
def path_cache_key
|
68
|
+
lookahead = query.lookahead
|
69
|
+
|
70
|
+
path.map { |field_name|
|
71
|
+
lookahead = lookahead.selection(field_name)
|
72
|
+
|
73
|
+
next field_name if lookahead.arguments.empty?
|
74
|
+
|
75
|
+
args = lookahead.arguments.map { "#{_1}:#{_2}" }.sort.join(",")
|
76
|
+
"#{field_name}(#{args})"
|
77
|
+
}.join("/")
|
78
|
+
end
|
79
|
+
|
80
|
+
def object_key(obj)
|
81
|
+
obj._graphql_cache_key
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module FragmentCache
|
5
|
+
using Ext
|
6
|
+
|
7
|
+
# Saves resolved fragment values to cache store
|
8
|
+
module Cacher
|
9
|
+
class << self
|
10
|
+
def call(query)
|
11
|
+
return unless query.context.fragments?
|
12
|
+
|
13
|
+
final_value = query.context.namespace(:interpreter)[:runtime].final_value
|
14
|
+
|
15
|
+
query.context.fragments.each { _1.persist(final_value) }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module FragmentCache
|
5
|
+
module Ext
|
6
|
+
# Add ability to access fragments via `context.fragments`
|
7
|
+
# without dupclicating the storage logic and monkey-patching
|
8
|
+
refine GraphQL::Query::Context do
|
9
|
+
def fragments?
|
10
|
+
namespace(:fragment_cache)[:fragments]
|
11
|
+
end
|
12
|
+
|
13
|
+
def fragments
|
14
|
+
namespace(:fragment_cache)[:fragments] ||= []
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module FragmentCache
|
5
|
+
module Ext
|
6
|
+
# Adds #_graphql_cache_key method to Object,
|
7
|
+
# which just call #graphql_cache_key or #cache_key.
|
8
|
+
#
|
9
|
+
# For other core classes returns string representation.
|
10
|
+
#
|
11
|
+
# Raises ArgumentError otherwise.
|
12
|
+
#
|
13
|
+
# We use a refinement to avoid case/if statements for type checking
|
14
|
+
refine Object do
|
15
|
+
def _graphql_cache_key
|
16
|
+
return graphql_cache_key if respond_to?(:graphql_cache_key)
|
17
|
+
return cache_key if respond_to?(:cache_key)
|
18
|
+
return to_a._graphql_cache_key if respond_to?(:to_a)
|
19
|
+
|
20
|
+
to_s
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
refine Array do
|
25
|
+
def _graphql_cache_key
|
26
|
+
map { _1._graphql_cache_key }.join("/")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
refine NilClass do
|
31
|
+
def _graphql_cache_key
|
32
|
+
""
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
refine TrueClass do
|
37
|
+
def _graphql_cache_key
|
38
|
+
"t"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
refine FalseClass do
|
43
|
+
def _graphql_cache_key
|
44
|
+
"f"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
refine String do
|
49
|
+
def _graphql_cache_key
|
50
|
+
self
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
refine Symbol do
|
55
|
+
def _graphql_cache_key
|
56
|
+
to_s
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
if RUBY_PLATFORM.match?(/java/i)
|
61
|
+
refine Integer do
|
62
|
+
def _graphql_cache_key
|
63
|
+
to_s
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
refine Float do
|
68
|
+
def _graphql_cache_key
|
69
|
+
to_s
|
70
|
+
end
|
71
|
+
end
|
72
|
+
else
|
73
|
+
refine Numeric do
|
74
|
+
def _graphql_cache_key
|
75
|
+
to_s
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
refine Time do
|
81
|
+
def _graphql_cache_key
|
82
|
+
to_s
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
refine Module do
|
87
|
+
def _graphql_cache_key
|
88
|
+
name
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module FragmentCache
|
5
|
+
# Wraps resolver with cache method
|
6
|
+
class FieldExtension < GraphQL::Schema::FieldExtension
|
7
|
+
module Patch
|
8
|
+
def initialize(*args, **kwargs, &block)
|
9
|
+
cache_fragment = kwargs.delete(:cache_fragment)
|
10
|
+
|
11
|
+
if cache_fragment
|
12
|
+
kwargs[:extensions] ||= []
|
13
|
+
kwargs[:extensions] << build_extension(cache_fragment)
|
14
|
+
end
|
15
|
+
|
16
|
+
super
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def build_extension(options)
|
22
|
+
if options.is_a?(Hash)
|
23
|
+
{FieldExtension => options}
|
24
|
+
else
|
25
|
+
FieldExtension
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(options:, **_rest)
|
31
|
+
@cache_options = options || {}
|
32
|
+
|
33
|
+
@context_key = @cache_options.delete(:context_key)
|
34
|
+
@cache_key = @cache_options.delete(:cache_key)
|
35
|
+
end
|
36
|
+
|
37
|
+
def resolve(object:, arguments:, **_options)
|
38
|
+
resolved_value = yield(object, arguments)
|
39
|
+
|
40
|
+
object_for_key = if @context_key
|
41
|
+
Array(@context_key).map { |key| object.context[key] }
|
42
|
+
elsif @cache_key == :object
|
43
|
+
object.object
|
44
|
+
elsif @cache_key == :value
|
45
|
+
resolved_value
|
46
|
+
end
|
47
|
+
|
48
|
+
cache_fragment_options = @cache_options.merge(object: object_for_key)
|
49
|
+
|
50
|
+
object.cache_fragment(cache_fragment_options) { resolved_value }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "graphql/fragment_cache/cache_key_builder"
|
4
|
+
|
5
|
+
module GraphQL
|
6
|
+
module FragmentCache
|
7
|
+
# Represents a single fragment to cache
|
8
|
+
class Fragment
|
9
|
+
attr_reader :options, :path, :context
|
10
|
+
|
11
|
+
def initialize(context, **options)
|
12
|
+
@context = context
|
13
|
+
@options = options
|
14
|
+
@path = context.namespace(:interpreter)[:current_path]
|
15
|
+
end
|
16
|
+
|
17
|
+
def read
|
18
|
+
FragmentCache.cache_store.read(cache_key)
|
19
|
+
end
|
20
|
+
|
21
|
+
def persist(final_value)
|
22
|
+
value = resolve(final_value)
|
23
|
+
FragmentCache.cache_store.write(cache_key, value, **options)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def cache_key
|
29
|
+
@cache_key ||= CacheKeyBuilder.call(path: path, query: context.query, **options)
|
30
|
+
end
|
31
|
+
|
32
|
+
def resolve(final_value)
|
33
|
+
final_value.dig(*path)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "graphql/fragment_cache/cacher"
|
4
|
+
|
5
|
+
module GraphQL
|
6
|
+
module FragmentCache
|
7
|
+
# Adds hook for saving cached values after query is resolved
|
8
|
+
module Instrumentation
|
9
|
+
module_function
|
10
|
+
|
11
|
+
def before_query(query)
|
12
|
+
end
|
13
|
+
|
14
|
+
def after_query(query)
|
15
|
+
return unless query.valid?
|
16
|
+
|
17
|
+
Cacher.call(query)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
using RubyNext
|
4
|
+
|
5
|
+
module GraphQL
|
6
|
+
module FragmentCache
|
7
|
+
# Memory adapter for storing cached fragments
|
8
|
+
class MemoryStore
|
9
|
+
using RubyNext
|
10
|
+
|
11
|
+
class Entry < Struct.new(:value, :expires_at, keyword_init: true)
|
12
|
+
def expired?
|
13
|
+
expires_at && expires_at < Time.now
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :default_expires_in
|
18
|
+
|
19
|
+
def initialize(expires_in: nil, **other)
|
20
|
+
raise ArgumentError, "Unsupported options: #{other.keys.join(",")}" unless other.empty?
|
21
|
+
|
22
|
+
@default_expires_in = expires_in
|
23
|
+
@storage = {}
|
24
|
+
end
|
25
|
+
|
26
|
+
def read(key)
|
27
|
+
key = key.to_s
|
28
|
+
storage[key]&.then do |entry|
|
29
|
+
if entry.expired?
|
30
|
+
delete(key)
|
31
|
+
next
|
32
|
+
end
|
33
|
+
entry.value
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def write(key, value, expires_in: default_expires_in, **options)
|
38
|
+
key = key.to_s
|
39
|
+
@storage[key] = Entry.new(value: value, expires_at: expires_in ? Time.now + expires_in : nil)
|
40
|
+
end
|
41
|
+
|
42
|
+
def delete(key)
|
43
|
+
key = key.to_s
|
44
|
+
storage.delete(key)
|
45
|
+
end
|
46
|
+
|
47
|
+
def clear
|
48
|
+
storage.clear
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
attr_reader :storage
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "graphql/fragment_cache/object_helpers"
|
4
|
+
require "graphql/fragment_cache/field_extension"
|
5
|
+
|
6
|
+
module GraphQL
|
7
|
+
module FragmentCache
|
8
|
+
# Adds #cache_fragment method and kwarg option
|
9
|
+
module Object
|
10
|
+
def self.included(base)
|
11
|
+
base.include(GraphQL::FragmentCache::ObjectHelpers)
|
12
|
+
base.field_class.prepend(GraphQL::FragmentCache::FieldExtension::Patch)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "graphql/fragment_cache/fragment"
|
4
|
+
|
5
|
+
module GraphQL
|
6
|
+
module FragmentCache
|
7
|
+
using Ext
|
8
|
+
|
9
|
+
# Adds #cache_fragment method
|
10
|
+
module ObjectHelpers
|
11
|
+
def cache_fragment(object_to_cache = nil, **options, &block)
|
12
|
+
fragment = Fragment.new(context, options)
|
13
|
+
|
14
|
+
if (cached = fragment.read)
|
15
|
+
return raw_value(cached)
|
16
|
+
end
|
17
|
+
|
18
|
+
context.fragments << fragment
|
19
|
+
|
20
|
+
object_to_cache || block.call
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module FragmentCache
|
5
|
+
# Extends key builder to use .expand_cache_key in Rails
|
6
|
+
class CacheKeyBuilder
|
7
|
+
def object_key(obj)
|
8
|
+
return obj.graphql_cache_key if obj.respond_to?(:graphql_cache_key)
|
9
|
+
return obj.map { |item| object_key(item) }.join("/") if obj.is_a?(Array)
|
10
|
+
return object_key(obj.to_a) if obj.respond_to?(:to_a)
|
11
|
+
|
12
|
+
ActiveSupport::Cache.expand_cache_key(obj)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "graphql/fragment_cache/rails/cache_key_builder"
|
4
|
+
|
5
|
+
module GraphQL
|
6
|
+
module FragmentCache
|
7
|
+
class Railtie < ::Rails::Railtie # :nodoc:
|
8
|
+
# Provides Rails-specific configuration,
|
9
|
+
# accessible through `Rails.application.config.graphql_fragment_cache`
|
10
|
+
module Config
|
11
|
+
class << self
|
12
|
+
def store=(store)
|
13
|
+
# Handle both:
|
14
|
+
# store = :memory
|
15
|
+
# store = :mem_cache, ENV['MEMCACHE']
|
16
|
+
if store.is_a?(Symbol) || store.is_a?(Array)
|
17
|
+
store = ActiveSupport::Cache.lookup_store(store)
|
18
|
+
end
|
19
|
+
|
20
|
+
FragmentCache.cache_store = store
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
config.graphql_fragment_cache = Config
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "digest/sha1"
|
4
|
+
|
5
|
+
module GraphQL
|
6
|
+
module FragmentCache
|
7
|
+
# Patches GraphQL::Schema to support fragment cache
|
8
|
+
module SchemaPatch
|
9
|
+
def schema_cache_key
|
10
|
+
@schema_cache_key ||= Digest::SHA1.hexdigest(to_definition)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
metadata
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: graphql-fragment_cache
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- DmitryTsepelev
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-04-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: graphql
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.10.3
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.10.3
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: ruby-next-core
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.5.1
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.5.1
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: combustion
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.1'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.1'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '13.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '13.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3.9'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3.9'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: timecop
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: ruby-next
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0.5'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0.5'
|
111
|
+
description: Fragment cache for graphql-ruby
|
112
|
+
email:
|
113
|
+
- dmitry.a.tsepelev@gmail.com
|
114
|
+
executables: []
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- CHANGELOG.md
|
119
|
+
- LICENSE.txt
|
120
|
+
- README.md
|
121
|
+
- bin/console
|
122
|
+
- bin/setup
|
123
|
+
- lib/.rbnext/2.7/graphql/fragment_cache/cache_key_builder.rb
|
124
|
+
- lib/.rbnext/2.7/graphql/fragment_cache/cacher.rb
|
125
|
+
- lib/.rbnext/2.7/graphql/fragment_cache/ext/graphql_cache_key.rb
|
126
|
+
- lib/graphql-fragment_cache.rb
|
127
|
+
- lib/graphql/fragment_cache.rb
|
128
|
+
- lib/graphql/fragment_cache/cache_key_builder.rb
|
129
|
+
- lib/graphql/fragment_cache/cacher.rb
|
130
|
+
- lib/graphql/fragment_cache/ext/context_fragments.rb
|
131
|
+
- lib/graphql/fragment_cache/ext/graphql_cache_key.rb
|
132
|
+
- lib/graphql/fragment_cache/field_extension.rb
|
133
|
+
- lib/graphql/fragment_cache/fragment.rb
|
134
|
+
- lib/graphql/fragment_cache/instrumentation.rb
|
135
|
+
- lib/graphql/fragment_cache/memory_store.rb
|
136
|
+
- lib/graphql/fragment_cache/object.rb
|
137
|
+
- lib/graphql/fragment_cache/object_helpers.rb
|
138
|
+
- lib/graphql/fragment_cache/rails/cache_key_builder.rb
|
139
|
+
- lib/graphql/fragment_cache/railtie.rb
|
140
|
+
- lib/graphql/fragment_cache/schema_patch.rb
|
141
|
+
- lib/graphql/fragment_cache/version.rb
|
142
|
+
homepage: https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache
|
143
|
+
licenses:
|
144
|
+
- MIT
|
145
|
+
metadata:
|
146
|
+
homepage_uri: https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache
|
147
|
+
source_code_uri: https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache
|
148
|
+
changelog_uri: https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/CHANGELOG.md
|
149
|
+
post_install_message:
|
150
|
+
rdoc_options: []
|
151
|
+
require_paths:
|
152
|
+
- lib
|
153
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
154
|
+
requirements:
|
155
|
+
- - ">="
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: '2.5'
|
158
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
159
|
+
requirements:
|
160
|
+
- - ">="
|
161
|
+
- !ruby/object:Gem::Version
|
162
|
+
version: '0'
|
163
|
+
requirements: []
|
164
|
+
rubygems_version: 3.0.3
|
165
|
+
signing_key:
|
166
|
+
specification_version: 4
|
167
|
+
summary: Fragment cache for graphql-ruby
|
168
|
+
test_files: []
|