graphql-fragment_cache 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 
|
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: []
|