graphql-fragment_cache 1.3.0 → 1.6.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 +21 -0
- data/README.md +36 -2
- data/lib/.rbnext/2.7/graphql/fragment_cache/cache_key_builder.rb +22 -3
- data/lib/graphql/fragment_cache.rb +26 -10
- data/lib/graphql/fragment_cache/cache_key_builder.rb +22 -3
- data/lib/graphql/fragment_cache/cacher.rb +6 -2
- data/lib/graphql/fragment_cache/ext/context_fragments.rb +4 -0
- data/lib/graphql/fragment_cache/field_extension.rb +1 -1
- data/lib/graphql/fragment_cache/fragment.rb +25 -5
- data/lib/graphql/fragment_cache/object_helpers.rb +9 -4
- data/lib/graphql/fragment_cache/schema/tracer.rb +1 -1
- data/lib/graphql/fragment_cache/version.rb +1 -1
- metadata +20 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a5c6796b976bd78fc44c483dd9b39684081ee07bfb8b791d3a9bf00a31893ae7
|
4
|
+
data.tar.gz: fc772a7e8c4319bd5b5bff5978ad20293334ac872be2fed170bc9084326874a0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 597046420b6342ca1c7e1429c1e44d36fa3a0eca9592d194318213614f3dc95c10eadf5ea616a13e8582f354caed9bf425b639149b15a7d9c3647e970fd3a780
|
7
|
+
data.tar.gz: 1aa900bb9840ccb836498b79ef0031859304a328bebbce86fc32399e468d43ae1279daedc1bb6444e4366450ab5b11974dec679d4e11d8688fbea9f6bdf1c4a0
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,27 @@
|
|
2
2
|
|
3
3
|
## master
|
4
4
|
|
5
|
+
## 1.6.0 (2021-03-13)
|
6
|
+
|
7
|
+
- [PR#54](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/54) Include arguments in selections_cache_key ([@bbugh][])
|
8
|
+
|
9
|
+
## 1.5.1 (2021-03-10)
|
10
|
+
|
11
|
+
- [PR#53](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/53) Use thread-safe query result for final_value ([@bbugh][])
|
12
|
+
- [PR#51](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/51) Do not cache fragments without final value ([@DmitryTsepelev][])
|
13
|
+
|
14
|
+
## 1.5.0 (2021-02-20)
|
15
|
+
|
16
|
+
- [PR#50](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/50) Add object_cache_key to CacheKeyBuilder ([@bbugh][])
|
17
|
+
|
18
|
+
## 1.4.1 (2021-01-21)
|
19
|
+
|
20
|
+
- [PR#48](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/48) Support graphql-ruby 1.12 ([@DmitryTsepelev][])
|
21
|
+
|
22
|
+
## 1.4.0 (2020-12-03)
|
23
|
+
|
24
|
+
- [PR#41](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/41) Add `keep_in_context` option ([@DmitryTsepelev][])
|
25
|
+
|
5
26
|
## 1.3.0 (2020-11-25)
|
6
27
|
|
7
28
|
- [PR#39](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/39) Implement `path_cache_key` option ([@DmitryTsepelev][])
|
data/README.md
CHANGED
@@ -112,10 +112,10 @@ selections_cache_key = "[#{%w[id name].join(".")}]"
|
|
112
112
|
|
113
113
|
query_cache_key = Digest::SHA1.hexdigest("#{path_cache_key}#{selections_cache_key}")
|
114
114
|
|
115
|
-
cache_key = "#{schema_cache_key}/#{query_cache_key}"
|
115
|
+
cache_key = "#{schema_cache_key}/#{query_cache_key}/#{object_cache_key}"
|
116
116
|
```
|
117
117
|
|
118
|
-
You can override `schema_cache_key`, `query_cache_key` or `
|
118
|
+
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
119
|
|
120
120
|
```ruby
|
121
121
|
class QueryType < BaseObject
|
@@ -140,6 +140,21 @@ class PostType < BaseObject
|
|
140
140
|
end
|
141
141
|
```
|
142
142
|
|
143
|
+
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.
|
144
|
+
|
145
|
+
```ruby
|
146
|
+
class QueryType < BaseObject
|
147
|
+
field :post, PostType, null: true do
|
148
|
+
argument :id, ID, required: true
|
149
|
+
end
|
150
|
+
|
151
|
+
def post(id:)
|
152
|
+
query = Post.where("updated_at < ?", Time.now - 1.day)
|
153
|
+
cache_fragment(object_cache_key: query.cache_key) { query.some_process }
|
154
|
+
end
|
155
|
+
end
|
156
|
+
```
|
157
|
+
|
143
158
|
### User-provided cache key
|
144
159
|
|
145
160
|
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):
|
@@ -198,6 +213,7 @@ end
|
|
198
213
|
|
199
214
|
The way cache key part is generated for the passed argument is the following:
|
200
215
|
|
216
|
+
- Use `object_cache_key: "some_cache_key"` if passed to `cache_fragment`
|
201
217
|
- Use `#graphql_cache_key` if implemented.
|
202
218
|
- Use `#cache_key` (or `#cache_key_with_version` for modern Rails) if implemented.
|
203
219
|
- Use `self.to_s` for _primitive_ types (strings, symbols, numbers, booleans).
|
@@ -314,6 +330,24 @@ end
|
|
314
330
|
|
315
331
|
With this approach you can use `#cache_fragment` in any place you have an access to the `context`. When context is not available, the error `cannot find context, please pass it explicitly` will be thrown.
|
316
332
|
|
333
|
+
## In–memory fragments
|
334
|
+
|
335
|
+
If you have a fragment that accessed from multiple times (e.g., if you have a list of items that belong to the same owner, and owner is cached), you can avoid multiple cache reads by using `:keep_in_context` option:
|
336
|
+
|
337
|
+
```ruby
|
338
|
+
class QueryType < BaseObject
|
339
|
+
field :post, PostType, null: true do
|
340
|
+
argument :id, ID, required: true
|
341
|
+
end
|
342
|
+
|
343
|
+
def post(id:)
|
344
|
+
cache_fragment(keep_in_context: true, expires_in: 5.minutes) { Post.find(id) }
|
345
|
+
end
|
346
|
+
end
|
347
|
+
```
|
348
|
+
|
349
|
+
This can reduce a number of cache calls but _increase_ memory usage, because the value returned from cache will be kept in the GraphQL context until the query is fully resolved.
|
350
|
+
|
317
351
|
## Limitations
|
318
352
|
|
319
353
|
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:
|
@@ -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
|
@@ -113,8 +127,13 @@ module GraphQL
|
|
113
127
|
|
114
128
|
def build
|
115
129
|
Digest::SHA1.hexdigest("#{schema_cache_key}/#{query_cache_key}").then do |base_key|
|
116
|
-
|
117
|
-
|
130
|
+
if @options[:object_cache_key]
|
131
|
+
"#{base_key}/#{@options[:object_cache_key]}"
|
132
|
+
elsif object
|
133
|
+
"#{base_key}/#{object_key(object)}"
|
134
|
+
else
|
135
|
+
base_key
|
136
|
+
end
|
118
137
|
end
|
119
138
|
end
|
120
139
|
|
@@ -23,7 +23,7 @@ module GraphQL
|
|
23
23
|
attr_reader :cache_store
|
24
24
|
|
25
25
|
def use(schema_defn, options = {})
|
26
|
-
|
26
|
+
verify_interpreter_and_analysis!(schema_defn)
|
27
27
|
|
28
28
|
schema_defn.tracer(Schema::Tracer)
|
29
29
|
schema_defn.instrument(:query, Schema::Instrumentation)
|
@@ -44,17 +44,33 @@ module GraphQL
|
|
44
44
|
@cache_store = store
|
45
45
|
end
|
46
46
|
|
47
|
-
|
47
|
+
def graphql_ruby_1_12_or_later?
|
48
|
+
Gem::Dependency.new("graphql", ">= 1.12.0").match?("graphql", GraphQL::VERSION)
|
49
|
+
end
|
48
50
|
|
49
|
-
|
50
|
-
unless schema_defn.interpreter?
|
51
|
-
raise StandardError,
|
52
|
-
"GraphQL::Execution::Interpreter should be enabled for fragment caching"
|
53
|
-
end
|
51
|
+
private
|
54
52
|
|
55
|
-
|
56
|
-
|
57
|
-
|
53
|
+
def verify_interpreter_and_analysis!(schema_defn)
|
54
|
+
if graphql_ruby_1_12_or_later?
|
55
|
+
unless schema_defn.interpreter?
|
56
|
+
raise StandardError,
|
57
|
+
"GraphQL::Execution::Execute should not be enabled for fragment caching"
|
58
|
+
end
|
59
|
+
|
60
|
+
unless schema_defn.analysis_engine == GraphQL::Analysis::AST
|
61
|
+
raise StandardError,
|
62
|
+
"GraphQL::Analysis should not be enabled for fragment caching"
|
63
|
+
end
|
64
|
+
else
|
65
|
+
unless schema_defn.interpreter?
|
66
|
+
raise StandardError,
|
67
|
+
"GraphQL::Execution::Interpreter should be enabled for fragment caching"
|
68
|
+
end
|
69
|
+
|
70
|
+
unless schema_defn.analysis_engine == GraphQL::Analysis::AST
|
71
|
+
raise StandardError,
|
72
|
+
"GraphQL::Analysis::AST should be enabled for fragment caching"
|
73
|
+
end
|
58
74
|
end
|
59
75
|
end
|
60
76
|
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}:#{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
|
@@ -113,8 +127,13 @@ module GraphQL
|
|
113
127
|
|
114
128
|
def build
|
115
129
|
Digest::SHA1.hexdigest("#{schema_cache_key}/#{query_cache_key}").then do |base_key|
|
116
|
-
|
117
|
-
|
130
|
+
if @options[:object_cache_key]
|
131
|
+
"#{base_key}/#{@options[:object_cache_key]}"
|
132
|
+
elsif object
|
133
|
+
"#{base_key}/#{object_key(object)}"
|
134
|
+
else
|
135
|
+
base_key
|
136
|
+
end
|
118
137
|
end
|
119
138
|
end
|
120
139
|
|
@@ -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
|
@@ -49,7 +49,7 @@ module GraphQL
|
|
49
49
|
|
50
50
|
cache_fragment_options = @cache_options.merge(object: object_for_key)
|
51
51
|
|
52
|
-
object.cache_fragment(cache_fragment_options) do
|
52
|
+
object.cache_fragment(**cache_fragment_options) do
|
53
53
|
resolved_value == NOT_RESOLVED ? yield(object, arguments) : resolved_value
|
54
54
|
end
|
55
55
|
end
|
@@ -4,6 +4,8 @@ require "graphql/fragment_cache/cache_key_builder"
|
|
4
4
|
|
5
5
|
module GraphQL
|
6
6
|
module FragmentCache
|
7
|
+
using Ext
|
8
|
+
|
7
9
|
# Represents a single fragment to cache
|
8
10
|
class Fragment
|
9
11
|
attr_reader :options, :path, :context
|
@@ -16,28 +18,46 @@ module GraphQL
|
|
16
18
|
|
17
19
|
NIL_IN_CACHE = Object.new
|
18
20
|
|
19
|
-
def read
|
20
|
-
|
21
|
-
|
22
|
-
|
21
|
+
def read(keep_in_context = false)
|
22
|
+
return read_from_context { value_from_cache } if keep_in_context
|
23
|
+
|
24
|
+
value_from_cache
|
23
25
|
end
|
24
26
|
|
25
27
|
def cache_key
|
26
28
|
@cache_key ||= CacheKeyBuilder.call(path: path, query: context.query, **options)
|
27
29
|
end
|
28
30
|
|
31
|
+
def with_final_value?
|
32
|
+
!final_value.nil?
|
33
|
+
end
|
34
|
+
|
29
35
|
def value
|
30
36
|
final_value.dig(*path)
|
31
37
|
end
|
32
38
|
|
33
39
|
private
|
34
40
|
|
41
|
+
def read_from_context
|
42
|
+
if (loaded_value = context.loaded_fragments[cache_key])
|
43
|
+
return loaded_value
|
44
|
+
end
|
45
|
+
|
46
|
+
yield.tap { |value| context.loaded_fragments[cache_key] = value }
|
47
|
+
end
|
48
|
+
|
49
|
+
def value_from_cache
|
50
|
+
FragmentCache.cache_store.read(cache_key).tap do |cached|
|
51
|
+
return NIL_IN_CACHE if cached.nil? && FragmentCache.cache_store.exist?(cache_key)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
35
55
|
def interpreter_context
|
36
56
|
context.namespace(:interpreter)
|
37
57
|
end
|
38
58
|
|
39
59
|
def final_value
|
40
|
-
@final_value ||=
|
60
|
+
@final_value ||= context.query.result["data"]
|
41
61
|
end
|
42
62
|
end
|
43
63
|
end
|
@@ -11,9 +11,13 @@ module GraphQL
|
|
11
11
|
extend Forwardable
|
12
12
|
|
13
13
|
def self.included(base)
|
14
|
-
return if base
|
14
|
+
return if base.method_defined?(:raw_value)
|
15
15
|
|
16
|
-
base.include(
|
16
|
+
base.include(Module.new {
|
17
|
+
def raw_value(obj)
|
18
|
+
GraphQL::Execution::Interpreter::RawValue.new(obj)
|
19
|
+
end
|
20
|
+
})
|
17
21
|
end
|
18
22
|
|
19
23
|
NO_OBJECT = Object.new
|
@@ -27,9 +31,10 @@ module GraphQL
|
|
27
31
|
context_to_use = context if context_to_use.nil? && respond_to?(:context)
|
28
32
|
raise ArgumentError, "cannot find context, please pass it explicitly" unless context_to_use
|
29
33
|
|
30
|
-
fragment = Fragment.new(context_to_use, options)
|
34
|
+
fragment = Fragment.new(context_to_use, **options)
|
31
35
|
|
32
|
-
|
36
|
+
keep_in_context = options.delete(:keep_in_context)
|
37
|
+
if (cached = fragment.read(keep_in_context))
|
33
38
|
return cached == Fragment::NIL_IN_CACHE ? nil : raw_value(cached)
|
34
39
|
end
|
35
40
|
|
@@ -23,7 +23,7 @@ module GraphQL
|
|
23
23
|
end
|
24
24
|
|
25
25
|
def verify_connections!(context)
|
26
|
-
return if context.schema.new_connections?
|
26
|
+
return if GraphQL::FragmentCache.graphql_ruby_1_12_or_later? || context.schema.new_connections?
|
27
27
|
|
28
28
|
raise StandardError,
|
29
29
|
"GraphQL::Pagination::Connections should be enabled for connection caching"
|
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.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- DmitryTsepelev
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-03-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
|
@@ -190,7 +204,7 @@ metadata:
|
|
190
204
|
homepage_uri: https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache
|
191
205
|
source_code_uri: https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache
|
192
206
|
changelog_uri: https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/CHANGELOG.md
|
193
|
-
post_install_message:
|
207
|
+
post_install_message:
|
194
208
|
rdoc_options: []
|
195
209
|
require_paths:
|
196
210
|
- lib
|
@@ -205,8 +219,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
205
219
|
- !ruby/object:Gem::Version
|
206
220
|
version: '0'
|
207
221
|
requirements: []
|
208
|
-
rubygems_version: 3.
|
209
|
-
signing_key:
|
222
|
+
rubygems_version: 3.1.2
|
223
|
+
signing_key:
|
210
224
|
specification_version: 4
|
211
225
|
summary: Fragment cache for graphql-ruby
|
212
226
|
test_files: []
|