graphql-fragment_cache 1.4.1 → 1.8.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 +4 -4
- data/CHANGELOG.md +25 -0
- data/README.md +128 -7
- data/lib/.rbnext/2.3/graphql/fragment_cache/cache_key_builder.rb +196 -0
- data/lib/.rbnext/2.7/graphql/fragment_cache/cache_key_builder.rb +29 -6
- data/lib/graphql/fragment_cache.rb +7 -0
- data/lib/graphql/fragment_cache/cache_key_builder.rb +29 -6
- data/lib/graphql/fragment_cache/cacher.rb +6 -2
- data/lib/graphql/fragment_cache/field_extension.rb +9 -2
- data/lib/graphql/fragment_cache/fragment.rb +6 -1
- data/lib/graphql/fragment_cache/object_helpers.rb +11 -0
- data/lib/graphql/fragment_cache/rails/cache_key_builder.rb +1 -0
- data/lib/graphql/fragment_cache/version.rb +1 -1
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7ad8755b7ad0579ac5b5a663a9c680fd5132ef818f39881b4df5d2a9c09a3d4b
|
4
|
+
data.tar.gz: b2132f313316e1f413b2fd07120b3c2a8c0465f66cc6183f32661a8f606074a4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 562098e2cfdb77fd07eef41b316fff4b052dcd7fab1b1b05672b5c2c5bf7f3627a7f3f754adae2e6a8c902816f7fb78428c85ab6d9e29395b33bfc1bfab4663e
|
7
|
+
data.tar.gz: 309be07ee6de769b32b2d13b258d3e0bd90ee043d4198d10f44c28edcfdbfafc3a52b0f81629fe0b40cc4efb8f8c62b5d801e3c42bec985b8426d205c41e724f
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,30 @@
|
|
2
2
|
|
3
3
|
## master
|
4
4
|
|
5
|
+
## 1.8.0 (2021-05-13)
|
6
|
+
|
7
|
+
- [PR#65](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/65) Add default options ([@jeromedalbert][])
|
8
|
+
|
9
|
+
## 1.7.0 (2021-04-30)
|
10
|
+
|
11
|
+
- [PR#62](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/62) Add a way to force a cache miss ([@jeromedalbert][])
|
12
|
+
- [PR#61](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/61) Add conditional caching ([@jeromedalbert][])
|
13
|
+
- [PR#64](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/64) Add a cache namespace ([@jeromedalbert][])
|
14
|
+
- [PR#63](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/63) Add a configure block notation ([@jeromedalbert][])
|
15
|
+
|
16
|
+
## 1.6.0 (2021-03-13)
|
17
|
+
|
18
|
+
- [PR#54](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/54) Include arguments in selections_cache_key ([@bbugh][])
|
19
|
+
|
20
|
+
## 1.5.1 (2021-03-10)
|
21
|
+
|
22
|
+
- [PR#53](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/53) Use thread-safe query result for final_value ([@bbugh][])
|
23
|
+
- [PR#51](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/51) Do not cache fragments without final value ([@DmitryTsepelev][])
|
24
|
+
|
25
|
+
## 1.5.0 (2021-02-20)
|
26
|
+
|
27
|
+
- [PR#50](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/50) Add object_cache_key to CacheKeyBuilder ([@bbugh][])
|
28
|
+
|
5
29
|
## 1.4.1 (2021-01-21)
|
6
30
|
|
7
31
|
- [PR#48](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/48) Support graphql-ruby 1.12 ([@DmitryTsepelev][])
|
@@ -86,3 +110,4 @@
|
|
86
110
|
[@ssnickolay]: https://github.com/ssnickolay
|
87
111
|
[@reabiliti]: https://github.com/reabiliti
|
88
112
|
[@bbugh]: https://github.com/bbugh
|
113
|
+
[@jeromedalbert]: https://github.com/jeromedalbert
|
data/README.md
CHANGED
@@ -80,11 +80,19 @@ end
|
|
80
80
|
|
81
81
|
## Cache key generation
|
82
82
|
|
83
|
-
Cache keys consist of implicit and explicit
|
83
|
+
Cache keys consist of the following parts: namespace, implicit key, and explicit key.
|
84
|
+
|
85
|
+
### Cache namespace
|
86
|
+
|
87
|
+
You can optionally define a namespace that will be prefixed to every cache key:
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
GraphQL::FragmentCache.namespace = "my-prefix"
|
91
|
+
```
|
84
92
|
|
85
93
|
### Implicit cache key
|
86
94
|
|
87
|
-
Implicit part of a cache key
|
95
|
+
Implicit part of a cache key contains the information about the schema and the current query. It includes:
|
88
96
|
|
89
97
|
- Hex gsdigest of the schema definition (to make sure cache is cleared when the schema changes).
|
90
98
|
- The current query fingerprint consisting of a _path_ to the field, arguments information and the selections set.
|
@@ -112,10 +120,10 @@ selections_cache_key = "[#{%w[id name].join(".")}]"
|
|
112
120
|
|
113
121
|
query_cache_key = Digest::SHA1.hexdigest("#{path_cache_key}#{selections_cache_key}")
|
114
122
|
|
115
|
-
cache_key = "#{schema_cache_key}/#{query_cache_key}"
|
123
|
+
cache_key = "#{schema_cache_key}/#{query_cache_key}/#{object_cache_key}"
|
116
124
|
```
|
117
125
|
|
118
|
-
You can override `schema_cache_key`, `query_cache_key` or `
|
126
|
+
You can override `schema_cache_key`, `query_cache_key`, `path_cache_key` or `object_cache_key` by passing parameters to the `cache_fragment` calls:
|
119
127
|
|
120
128
|
```ruby
|
121
129
|
class QueryType < BaseObject
|
@@ -140,7 +148,22 @@ class PostType < BaseObject
|
|
140
148
|
end
|
141
149
|
```
|
142
150
|
|
143
|
-
|
151
|
+
Overriding `object_cache_key` is helpful in the case where the value that is cached is different than the one used as a key, like a database query that is pre-processed before caching.
|
152
|
+
|
153
|
+
```ruby
|
154
|
+
class QueryType < BaseObject
|
155
|
+
field :post, PostType, null: true do
|
156
|
+
argument :id, ID, required: true
|
157
|
+
end
|
158
|
+
|
159
|
+
def post(id:)
|
160
|
+
query = Post.where("updated_at < ?", Time.now - 1.day)
|
161
|
+
cache_fragment(object_cache_key: query.cache_key) { query.some_process }
|
162
|
+
end
|
163
|
+
end
|
164
|
+
```
|
165
|
+
|
166
|
+
### User-provided cache key (custom key)
|
144
167
|
|
145
168
|
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):
|
146
169
|
|
@@ -169,6 +192,36 @@ cache_fragment(post)
|
|
169
192
|
cache_fragment(post) { post }
|
170
193
|
```
|
171
194
|
|
195
|
+
Using literals: Even when using a same string for all queries, the cache changes per argument and per selection set (because of the query_key).
|
196
|
+
|
197
|
+
```ruby
|
198
|
+
def post(id:)
|
199
|
+
cache_fragment("find_post") { Post.find(id) }
|
200
|
+
end
|
201
|
+
```
|
202
|
+
|
203
|
+
Combining with options:
|
204
|
+
|
205
|
+
```ruby
|
206
|
+
def post(id:)
|
207
|
+
cache_fragment("find_post", expires_in: 5.minutes) { Post.find(id) }
|
208
|
+
end
|
209
|
+
```
|
210
|
+
|
211
|
+
Dynamic cache key:
|
212
|
+
|
213
|
+
```ruby
|
214
|
+
def post(id:)
|
215
|
+
last_updated_at = Post.select(:updated_at).find_by(id: id)&.updated_at
|
216
|
+
cache_fragment(last_updated_at, expires_in: 5.minutes) { Post.find(id) }
|
217
|
+
end
|
218
|
+
```
|
219
|
+
|
220
|
+
Note the usage of `.select(:updated_at)` at the cache key field to make this verifying query as fastest and light as possible.
|
221
|
+
|
222
|
+
You can also add touch options for the belongs_to association e.g author's `belongs_to: :post` to have a `touch: true`.
|
223
|
+
So that it invalidates the Post when the author is updated.
|
224
|
+
|
172
225
|
When using `cache_fragment:` option, it's only possible to use the resolved value as a cache key by setting:
|
173
226
|
|
174
227
|
```ruby
|
@@ -198,6 +251,7 @@ end
|
|
198
251
|
|
199
252
|
The way cache key part is generated for the passed argument is the following:
|
200
253
|
|
254
|
+
- Use `object_cache_key: "some_cache_key"` if passed to `cache_fragment`
|
201
255
|
- Use `#graphql_cache_key` if implemented.
|
202
256
|
- Use `#cache_key` (or `#cache_key_with_version` for modern Rails) if implemented.
|
203
257
|
- Use `self.to_s` for _primitive_ types (strings, symbols, numbers, booleans).
|
@@ -244,12 +298,56 @@ class QueryType < BaseObject
|
|
244
298
|
end
|
245
299
|
```
|
246
300
|
|
301
|
+
## Conditional caching
|
302
|
+
|
303
|
+
Use the `if:` (or `unless:`) option:
|
304
|
+
|
305
|
+
```ruby
|
306
|
+
def post(id:)
|
307
|
+
cache_fragment(if: current_user.nil?) { Post.find(id) }
|
308
|
+
end
|
309
|
+
|
310
|
+
# or
|
311
|
+
|
312
|
+
field :post, PostType, cache_fragment: {if: -> { current_user.nil? }} do
|
313
|
+
argument :id, ID, required: true
|
314
|
+
end
|
315
|
+
```
|
316
|
+
|
317
|
+
## Default options
|
318
|
+
|
319
|
+
You can configure default options that will be passed to all `cache_fragment`
|
320
|
+
calls and `cache_fragment:` configurations. For example:
|
321
|
+
|
322
|
+
```ruby
|
323
|
+
GraphQL::FragmentCache.configure do |config|
|
324
|
+
config.default_options = {
|
325
|
+
expires_in: 1.hour, # Expire cache keys after 1 hour
|
326
|
+
schema_cache_key: nil # Do not clear the cache on each schema change
|
327
|
+
}
|
328
|
+
end
|
329
|
+
```
|
330
|
+
|
331
|
+
## Renewing the cache
|
332
|
+
|
333
|
+
You can force the cache to renew during query execution by adding
|
334
|
+
`renew_cache: true` to the query context:
|
335
|
+
|
336
|
+
```ruby
|
337
|
+
MyAppSchema.execute("query { posts { title } }", context: {renew_cache: true})
|
338
|
+
```
|
339
|
+
|
340
|
+
This will treat any cached value as missing even if it's present, and store
|
341
|
+
fresh new computed values in the cache. This can be useful for cache warmers.
|
342
|
+
|
247
343
|
## Cache storage and options
|
248
344
|
|
249
345
|
It's up to your to decide which caching engine to use, all you need is to configure the cache store:
|
250
346
|
|
251
347
|
```ruby
|
252
|
-
GraphQL::FragmentCache.
|
348
|
+
GraphQL::FragmentCache.configure do |config|
|
349
|
+
config.cache_store = MyCacheStore.new
|
350
|
+
end
|
253
351
|
```
|
254
352
|
|
255
353
|
Or, in Rails:
|
@@ -334,7 +432,30 @@ This can reduce a number of cache calls but _increase_ memory usage, because the
|
|
334
432
|
|
335
433
|
## Limitations
|
336
434
|
|
337
|
-
|
435
|
+
1. `Schema#execute`, [graphql-batch](https://github.com/Shopify/graphql-batch) and _graphql-ruby-fragment_cache_ do not [play well](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/issues/45) together. The problem appears when `cache_fragment` is _inside_ the `.then` block:
|
436
|
+
|
437
|
+
```ruby
|
438
|
+
def cached_author_inside_batch
|
439
|
+
AuthorLoader.load(object).then do |author|
|
440
|
+
cache_fragment(author, context: context)
|
441
|
+
end
|
442
|
+
end
|
443
|
+
```
|
444
|
+
|
445
|
+
The problem is that context is not [properly populated](https://github.com/rmosolgo/graphql-ruby/issues/3397) inside the block (the gem uses `:current_path` to build the cache key). There are two possible workarounds: use [dataloaders](https://graphql-ruby.org/dataloader/overview.html) or manage `:current_path` manually:
|
446
|
+
|
447
|
+
```ruby
|
448
|
+
def cached_author_inside_batch
|
449
|
+
outer_path = context.namespace(:interpreter)[:current_path]
|
450
|
+
|
451
|
+
AuthorLoader.load(object).then do |author|
|
452
|
+
context.namespace(:interpreter)[:current_path] = outer_path
|
453
|
+
cache_fragment(author, context: context)
|
454
|
+
end
|
455
|
+
end
|
456
|
+
```
|
457
|
+
|
458
|
+
2. Caching does not work for Union types, because of the `Lookahead` implementation: it requires the exact type to be passed to the `selection` method (you can find the [discussion](https://github.com/rmosolgo/graphql-ruby/pull/3007) here). This method is used for cache key building, and I haven't found a workaround yet ([PR in progress](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/30)). If you get `Failed to look ahead the field` error — please pass `query_cache_key` explicitly:
|
338
459
|
|
339
460
|
```ruby
|
340
461
|
field :cached_avatar_url, String, null: false
|
@@ -0,0 +1,196 @@
|
|
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 traverse_argument(argument)
|
15
|
+
return argument unless argument.is_a?(GraphQL::Schema::InputObject)
|
16
|
+
|
17
|
+
"{#{argument.map { |_1, _2| "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")}}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_selections_key
|
21
|
+
map { |val|
|
22
|
+
children = val.selections.empty? ? "" : "[#{val.selections.to_selections_key}]"
|
23
|
+
|
24
|
+
field_name = val.field.name
|
25
|
+
|
26
|
+
unless val.arguments.empty?
|
27
|
+
args = val.arguments.map { |_1, _2| "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")
|
28
|
+
field_name += "(#{args})"
|
29
|
+
end
|
30
|
+
|
31
|
+
"#{field_name}#{children}"
|
32
|
+
}.join(".")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
refine ::GraphQL::Language::Nodes::AbstractNode do
|
37
|
+
def alias?(_)
|
38
|
+
false
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
refine ::GraphQL::Language::Nodes::Field do
|
43
|
+
def alias?(val)
|
44
|
+
self.alias == val
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
refine ::GraphQL::Execution::Lookahead do
|
49
|
+
def selection_with_alias(name, **kwargs)
|
50
|
+
return selection(name, **kwargs) if selects?(name, **kwargs)
|
51
|
+
alias_selection(name, **kwargs)
|
52
|
+
end
|
53
|
+
|
54
|
+
def alias_selection(name, selected_type: @selected_type, arguments: nil)
|
55
|
+
return alias_selections[name] if alias_selections.key?(name)
|
56
|
+
|
57
|
+
alias_node = lookup_alias_node(ast_nodes, name)
|
58
|
+
return ::GraphQL::Execution::Lookahead::NULL_LOOKAHEAD unless alias_node
|
59
|
+
|
60
|
+
next_field_name = alias_node.name
|
61
|
+
|
62
|
+
# From https://github.com/rmosolgo/graphql-ruby/blob/1a9a20f3da629e63ea8e5ee8400be82218f9edc3/lib/graphql/execution/lookahead.rb#L91
|
63
|
+
next_field_defn = get_class_based_field(selected_type, next_field_name)
|
64
|
+
|
65
|
+
alias_selections[name] =
|
66
|
+
if next_field_defn
|
67
|
+
next_nodes = []
|
68
|
+
arguments = @query.arguments_for(alias_node, next_field_defn)
|
69
|
+
arguments = arguments.is_a?(::GraphQL::Execution::Interpreter::Arguments) ? arguments.keyword_arguments : arguments
|
70
|
+
@ast_nodes.each do |ast_node|
|
71
|
+
ast_node.selections.each do |selection|
|
72
|
+
find_selected_nodes(selection, next_field_name, next_field_defn, arguments: arguments, matches: next_nodes)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
if next_nodes.any?
|
77
|
+
::GraphQL::Execution::Lookahead.new(query: @query, ast_nodes: next_nodes, field: next_field_defn, owner_type: selected_type)
|
78
|
+
else
|
79
|
+
::GraphQL::Execution::Lookahead::NULL_LOOKAHEAD
|
80
|
+
end
|
81
|
+
else
|
82
|
+
::GraphQL::Execution::Lookahead::NULL_LOOKAHEAD
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def alias_selections
|
87
|
+
return @alias_selections if defined?(@alias_selections)
|
88
|
+
@alias_selections ||= {}
|
89
|
+
end
|
90
|
+
|
91
|
+
def lookup_alias_node(nodes, name)
|
92
|
+
return if nodes.empty?
|
93
|
+
|
94
|
+
nodes.find do |node|
|
95
|
+
if node.is_a?(GraphQL::Language::Nodes::FragmentSpread)
|
96
|
+
node = @query.fragments[node.name]
|
97
|
+
raise("Invariant: Can't look ahead to nonexistent fragment #{node.name} (found: #{@query.fragments.keys})") unless node
|
98
|
+
end
|
99
|
+
|
100
|
+
return node if node.alias?(name)
|
101
|
+
child = lookup_alias_node(node.children, name)
|
102
|
+
return child if child
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
})
|
107
|
+
|
108
|
+
# Builds cache key for fragment
|
109
|
+
class CacheKeyBuilder
|
110
|
+
using RubyNext
|
111
|
+
|
112
|
+
class << self
|
113
|
+
def call(**options)
|
114
|
+
new(**options).build
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
attr_reader :query, :path, :object, :schema
|
119
|
+
|
120
|
+
def initialize(object: nil, query:, path:, **options)
|
121
|
+
@object = object
|
122
|
+
@query = query
|
123
|
+
@schema = query.schema
|
124
|
+
@path = path
|
125
|
+
@options = options
|
126
|
+
end
|
127
|
+
|
128
|
+
def build
|
129
|
+
[
|
130
|
+
GraphQL::FragmentCache.namespace,
|
131
|
+
implicit_cache_key,
|
132
|
+
object_cache_key
|
133
|
+
].compact.join("/")
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
|
138
|
+
def implicit_cache_key
|
139
|
+
Digest::SHA1.hexdigest("#{schema_cache_key}/#{query_cache_key}")
|
140
|
+
end
|
141
|
+
|
142
|
+
def schema_cache_key
|
143
|
+
@options.fetch(:schema_cache_key) { schema.schema_cache_key }
|
144
|
+
end
|
145
|
+
|
146
|
+
def query_cache_key
|
147
|
+
@options.fetch(:query_cache_key) { "#{path_cache_key}[#{selections_cache_key}]" }
|
148
|
+
end
|
149
|
+
|
150
|
+
def selections_cache_key
|
151
|
+
current_root =
|
152
|
+
path.reduce(query.lookahead) { |lkhd, field_name|
|
153
|
+
# Handle cached fields inside collections:
|
154
|
+
next lkhd if field_name.is_a?(Integer)
|
155
|
+
|
156
|
+
lkhd.selection_with_alias(field_name)
|
157
|
+
}
|
158
|
+
|
159
|
+
current_root.selections.to_selections_key
|
160
|
+
end
|
161
|
+
|
162
|
+
def path_cache_key
|
163
|
+
@options.fetch(:path_cache_key) do
|
164
|
+
lookahead = query.lookahead
|
165
|
+
|
166
|
+
path.map { |field_name|
|
167
|
+
# Handle cached fields inside collections:
|
168
|
+
next field_name if field_name.is_a?(Integer)
|
169
|
+
|
170
|
+
lookahead = lookahead.selection_with_alias(field_name)
|
171
|
+
raise "Failed to look ahead the field: #{field_name}" if lookahead.is_a?(::GraphQL::Execution::Lookahead::NullLookahead)
|
172
|
+
|
173
|
+
next lookahead.field.name if lookahead.arguments.empty?
|
174
|
+
|
175
|
+
args = lookahead.arguments.map { |_1, _2| "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")
|
176
|
+
"#{lookahead.field.name}(#{args})"
|
177
|
+
}.join("/")
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def traverse_argument(argument)
|
182
|
+
return argument unless argument.is_a?(GraphQL::Schema::InputObject)
|
183
|
+
|
184
|
+
"{#{argument.map { |_1, _2| "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")}}"
|
185
|
+
end
|
186
|
+
|
187
|
+
def object_cache_key
|
188
|
+
@options[:object_cache_key] || object_key(object)
|
189
|
+
end
|
190
|
+
|
191
|
+
def object_key(obj)
|
192
|
+
((!obj.nil?) || nil) && obj._graphql_cache_key
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
@@ -11,10 +11,24 @@ module GraphQL
|
|
11
11
|
|
12
12
|
using(Module.new {
|
13
13
|
refine Array do
|
14
|
+
def traverse_argument(argument)
|
15
|
+
return argument unless argument.is_a?(GraphQL::Schema::InputObject)
|
16
|
+
|
17
|
+
"{#{argument.map { |_1, _2| "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")}}"
|
18
|
+
end
|
19
|
+
|
14
20
|
def to_selections_key
|
15
21
|
map { |val|
|
16
22
|
children = val.selections.empty? ? "" : "[#{val.selections.to_selections_key}]"
|
17
|
-
|
23
|
+
|
24
|
+
field_name = val.field.name
|
25
|
+
|
26
|
+
unless val.arguments.empty?
|
27
|
+
args = val.arguments.map { |_1, _2| "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")
|
28
|
+
field_name += "(#{args})"
|
29
|
+
end
|
30
|
+
|
31
|
+
"#{field_name}#{children}"
|
18
32
|
}.join(".")
|
19
33
|
end
|
20
34
|
end
|
@@ -112,14 +126,19 @@ module GraphQL
|
|
112
126
|
end
|
113
127
|
|
114
128
|
def build
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
129
|
+
[
|
130
|
+
GraphQL::FragmentCache.namespace,
|
131
|
+
implicit_cache_key,
|
132
|
+
object_cache_key
|
133
|
+
].compact.join("/")
|
119
134
|
end
|
120
135
|
|
121
136
|
private
|
122
137
|
|
138
|
+
def implicit_cache_key
|
139
|
+
Digest::SHA1.hexdigest("#{schema_cache_key}/#{query_cache_key}")
|
140
|
+
end
|
141
|
+
|
123
142
|
def schema_cache_key
|
124
143
|
@options.fetch(:schema_cache_key) { schema.schema_cache_key }
|
125
144
|
end
|
@@ -165,8 +184,12 @@ module GraphQL
|
|
165
184
|
"{#{argument.map { |_1, _2| "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")}}"
|
166
185
|
end
|
167
186
|
|
187
|
+
def object_cache_key
|
188
|
+
@options[:object_cache_key] || object_key(object)
|
189
|
+
end
|
190
|
+
|
168
191
|
def object_key(obj)
|
169
|
-
obj
|
192
|
+
obj&._graphql_cache_key
|
170
193
|
end
|
171
194
|
end
|
172
195
|
end
|
@@ -21,6 +21,8 @@ module GraphQL
|
|
21
21
|
module FragmentCache
|
22
22
|
class << self
|
23
23
|
attr_reader :cache_store
|
24
|
+
attr_accessor :namespace
|
25
|
+
attr_accessor :default_options
|
24
26
|
|
25
27
|
def use(schema_defn, options = {})
|
26
28
|
verify_interpreter_and_analysis!(schema_defn)
|
@@ -32,6 +34,10 @@ module GraphQL
|
|
32
34
|
GraphQL::Pagination::Connections.prepend(Connections::Patch)
|
33
35
|
end
|
34
36
|
|
37
|
+
def configure
|
38
|
+
yield self
|
39
|
+
end
|
40
|
+
|
35
41
|
def cache_store=(store)
|
36
42
|
unless store.respond_to?(:read)
|
37
43
|
raise ArgumentError, "Store must implement #read(key) method"
|
@@ -76,6 +82,7 @@ module GraphQL
|
|
76
82
|
end
|
77
83
|
|
78
84
|
self.cache_store = MemoryStore.new
|
85
|
+
self.default_options = {}
|
79
86
|
end
|
80
87
|
end
|
81
88
|
|
@@ -11,10 +11,24 @@ module GraphQL
|
|
11
11
|
|
12
12
|
using(Module.new {
|
13
13
|
refine Array do
|
14
|
+
def traverse_argument(argument)
|
15
|
+
return argument unless argument.is_a?(GraphQL::Schema::InputObject)
|
16
|
+
|
17
|
+
"{#{argument.map { "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")}}"
|
18
|
+
end
|
19
|
+
|
14
20
|
def to_selections_key
|
15
21
|
map { |val|
|
16
22
|
children = val.selections.empty? ? "" : "[#{val.selections.to_selections_key}]"
|
17
|
-
|
23
|
+
|
24
|
+
field_name = val.field.name
|
25
|
+
|
26
|
+
unless val.arguments.empty?
|
27
|
+
args = val.arguments.map { "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")
|
28
|
+
field_name += "(#{args})"
|
29
|
+
end
|
30
|
+
|
31
|
+
"#{field_name}#{children}"
|
18
32
|
}.join(".")
|
19
33
|
end
|
20
34
|
end
|
@@ -112,14 +126,19 @@ module GraphQL
|
|
112
126
|
end
|
113
127
|
|
114
128
|
def build
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
129
|
+
[
|
130
|
+
GraphQL::FragmentCache.namespace,
|
131
|
+
implicit_cache_key,
|
132
|
+
object_cache_key
|
133
|
+
].compact.join("/")
|
119
134
|
end
|
120
135
|
|
121
136
|
private
|
122
137
|
|
138
|
+
def implicit_cache_key
|
139
|
+
Digest::SHA1.hexdigest("#{schema_cache_key}/#{query_cache_key}")
|
140
|
+
end
|
141
|
+
|
123
142
|
def schema_cache_key
|
124
143
|
@options.fetch(:schema_cache_key) { schema.schema_cache_key }
|
125
144
|
end
|
@@ -165,8 +184,12 @@ module GraphQL
|
|
165
184
|
"{#{argument.map { "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")}}"
|
166
185
|
end
|
167
186
|
|
187
|
+
def object_cache_key
|
188
|
+
@options[:object_cache_key] || object_key(object)
|
189
|
+
end
|
190
|
+
|
168
191
|
def object_key(obj)
|
169
|
-
obj
|
192
|
+
obj&._graphql_cache_key
|
170
193
|
end
|
171
194
|
end
|
172
195
|
end
|
@@ -20,17 +20,21 @@ module GraphQL
|
|
20
20
|
private
|
21
21
|
|
22
22
|
def batched_persist(query)
|
23
|
-
query.
|
23
|
+
select_valid_fragments(query).group_by(&:options).each do |options, group|
|
24
24
|
hash = group.map { |fragment| [fragment.cache_key, fragment.value] }.to_h
|
25
25
|
FragmentCache.cache_store.write_multi(hash, **options)
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
29
29
|
def persist(query)
|
30
|
-
query.
|
30
|
+
select_valid_fragments(query).each do |fragment|
|
31
31
|
FragmentCache.cache_store.write(fragment.cache_key, fragment.value, **fragment.options)
|
32
32
|
end
|
33
33
|
end
|
34
|
+
|
35
|
+
def select_valid_fragments(query)
|
36
|
+
query.context.fragments.select(&:with_final_value?)
|
37
|
+
end
|
34
38
|
end
|
35
39
|
end
|
36
40
|
end
|
@@ -28,7 +28,8 @@ module GraphQL
|
|
28
28
|
end
|
29
29
|
|
30
30
|
def initialize(options:, **_rest)
|
31
|
-
@cache_options = options || {}
|
31
|
+
@cache_options = GraphQL::FragmentCache.default_options.merge(options || {})
|
32
|
+
@cache_options[:default_options_merged] = true
|
32
33
|
|
33
34
|
@context_key = @cache_options.delete(:context_key)
|
34
35
|
@cache_key = @cache_options.delete(:cache_key)
|
@@ -39,6 +40,13 @@ module GraphQL
|
|
39
40
|
def resolve(object:, arguments:, **_options)
|
40
41
|
resolved_value = NOT_RESOLVED
|
41
42
|
|
43
|
+
if @cache_options[:if].is_a?(Proc)
|
44
|
+
@cache_options[:if] = object.instance_exec(&@cache_options[:if])
|
45
|
+
end
|
46
|
+
if @cache_options[:unless].is_a?(Proc)
|
47
|
+
@cache_options[:unless] = object.instance_exec(&@cache_options[:unless])
|
48
|
+
end
|
49
|
+
|
42
50
|
object_for_key = if @context_key
|
43
51
|
Array(@context_key).map { |key| object.context[key] }
|
44
52
|
elsif @cache_key == :object
|
@@ -46,7 +54,6 @@ module GraphQL
|
|
46
54
|
elsif @cache_key == :value
|
47
55
|
resolved_value = yield(object, arguments)
|
48
56
|
end
|
49
|
-
|
50
57
|
cache_fragment_options = @cache_options.merge(object: object_for_key)
|
51
58
|
|
52
59
|
object.cache_fragment(**cache_fragment_options) do
|
@@ -19,6 +19,7 @@ module GraphQL
|
|
19
19
|
NIL_IN_CACHE = Object.new
|
20
20
|
|
21
21
|
def read(keep_in_context = false)
|
22
|
+
return nil if context[:renew_cache] == true
|
22
23
|
return read_from_context { value_from_cache } if keep_in_context
|
23
24
|
|
24
25
|
value_from_cache
|
@@ -28,6 +29,10 @@ module GraphQL
|
|
28
29
|
@cache_key ||= CacheKeyBuilder.call(path: path, query: context.query, **options)
|
29
30
|
end
|
30
31
|
|
32
|
+
def with_final_value?
|
33
|
+
!final_value.nil?
|
34
|
+
end
|
35
|
+
|
31
36
|
def value
|
32
37
|
final_value.dig(*path)
|
33
38
|
end
|
@@ -53,7 +58,7 @@ module GraphQL
|
|
53
58
|
end
|
54
59
|
|
55
60
|
def final_value
|
56
|
-
@final_value ||=
|
61
|
+
@final_value ||= context.query.result["data"]
|
57
62
|
end
|
58
63
|
end
|
59
64
|
end
|
@@ -25,6 +25,17 @@ module GraphQL
|
|
25
25
|
def cache_fragment(object_to_cache = NO_OBJECT, **options, &block)
|
26
26
|
raise ArgumentError, "Block or argument must be provided" unless block_given? || object_to_cache != NO_OBJECT
|
27
27
|
|
28
|
+
unless options.delete(:default_options_merged)
|
29
|
+
options = GraphQL::FragmentCache.default_options.merge(options)
|
30
|
+
end
|
31
|
+
|
32
|
+
if options.key?(:if) || options.key?(:unless)
|
33
|
+
disabled = options.key?(:if) ? !options.delete(:if) : options.delete(:unless)
|
34
|
+
if disabled
|
35
|
+
return block_given? ? block.call : object_to_cache
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
28
39
|
options[:object] = object_to_cache if object_to_cache != NO_OBJECT
|
29
40
|
|
30
41
|
context_to_use = options.delete(:context)
|
@@ -5,6 +5,7 @@ module GraphQL
|
|
5
5
|
# Extends key builder to use .expand_cache_key in Rails
|
6
6
|
class CacheKeyBuilder
|
7
7
|
def object_key(obj)
|
8
|
+
return nil if obj.nil?
|
8
9
|
return obj.graphql_cache_key if obj.respond_to?(:graphql_cache_key)
|
9
10
|
return obj.cache_key_with_version if obj.respond_to?(:cache_key_with_version)
|
10
11
|
return obj.cache_key if obj.respond_to?(:cache_key)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: graphql-fragment_cache
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- DmitryTsepelev
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-05-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: graphql
|
@@ -150,6 +150,20 @@ dependencies:
|
|
150
150
|
- - '='
|
151
151
|
- !ruby/object:Gem::Version
|
152
152
|
version: 0.4.9
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: graphql-batch
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
153
167
|
description: Fragment cache for graphql-ruby
|
154
168
|
email:
|
155
169
|
- dmitry.a.tsepelev@gmail.com
|
@@ -162,6 +176,7 @@ files:
|
|
162
176
|
- README.md
|
163
177
|
- bin/console
|
164
178
|
- bin/setup
|
179
|
+
- lib/.rbnext/2.3/graphql/fragment_cache/cache_key_builder.rb
|
165
180
|
- lib/.rbnext/2.3/graphql/fragment_cache/memory_store.rb
|
166
181
|
- lib/.rbnext/2.7/graphql/fragment_cache/cache_key_builder.rb
|
167
182
|
- lib/.rbnext/2.7/graphql/fragment_cache/ext/graphql_cache_key.rb
|