graphql-fragment_cache 1.1.0 → 1.5.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 +38 -2
- data/lib/.rbnext/2.7/graphql/fragment_cache/cache_key_builder.rb +19 -12
- data/lib/graphql/fragment_cache.rb +26 -10
- data/lib/graphql/fragment_cache/cache_key_builder.rb +19 -12
- 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 +20 -4
- data/lib/graphql/fragment_cache/object_helpers.rb +9 -4
- data/lib/graphql/fragment_cache/rails/cache_key_builder.rb +2 -0
- data/lib/graphql/fragment_cache/schema/tracer.rb +1 -1
- data/lib/graphql/fragment_cache/version.rb +1 -1
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fc02c6847d8e55983814bba96e0bd303893a1639648530048f0ccf6ab1527eab
|
4
|
+
data.tar.gz: 52530f6e108ed656077a02a78a44dd2cf498e4004b9d27897572f6f77aaec353
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e5b95e2b3f68011a8cdc0fc23b3a5f117f0ce9c711efcfb504230f6a84fb8c6c53a156d6671f97c8ea52cad199de597954badb2e456d33303b80e6e2e2105bec
|
7
|
+
data.tar.gz: d7275ef053d1127a17476244bd7253cb98dab81df865195b7bae7241bcd6106564af13848dfeaf39676acb96e962be73fb7782274ae4ccce2190065234b5fbf9
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,26 @@
|
|
2
2
|
|
3
3
|
## master
|
4
4
|
|
5
|
+
## 1.5.0 (2021-02-20)
|
6
|
+
|
7
|
+
- [PR#50](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/50) Add object_cache_key to CacheKeyBuilder ([@bbugh][])
|
8
|
+
|
9
|
+
## 1.4.1 (2021-01-21)
|
10
|
+
|
11
|
+
- [PR#48](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/48) Support graphql-ruby 1.12 ([@DmitryTsepelev][])
|
12
|
+
|
13
|
+
## 1.4.0 (2020-12-03)
|
14
|
+
|
15
|
+
- [PR#41](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/41) Add `keep_in_context` option ([@DmitryTsepelev][])
|
16
|
+
|
17
|
+
## 1.3.0 (2020-11-25)
|
18
|
+
|
19
|
+
- [PR#39](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/39) Implement `path_cache_key` option ([@DmitryTsepelev][])
|
20
|
+
|
21
|
+
## 1.2.0 (2020-10-26)
|
22
|
+
|
23
|
+
- [PR#37](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/37) Try to use `cache_key_with_version` or `cache_key` with Rails CacheKeyBuilder ([@bbugh][])
|
24
|
+
|
5
25
|
## 1.1.0 (2020-10-26)
|
6
26
|
|
7
27
|
- [PR#38](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/38) Support caching from other places than field or resolver ([@DmitryTsepelev][])
|
@@ -69,3 +89,4 @@
|
|
69
89
|
[@palkan]: https://github.com/palkan
|
70
90
|
[@ssnickolay]: https://github.com/ssnickolay
|
71
91
|
[@reabiliti]: https://github.com/reabiliti
|
92
|
+
[@bbugh]: https://github.com/bbugh
|
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` 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
|
@@ -129,6 +129,8 @@ class QueryType < BaseObject
|
|
129
129
|
end
|
130
130
|
```
|
131
131
|
|
132
|
+
Overriding `path_cache_key` might be helpful when you resolve the same object nested in multiple places (e.g., `Post` and `Comment` both have `author`), but want to make sure cache will be invalidated when selection set is different.
|
133
|
+
|
132
134
|
Same for the option:
|
133
135
|
|
134
136
|
```ruby
|
@@ -138,6 +140,21 @@ class PostType < BaseObject
|
|
138
140
|
end
|
139
141
|
```
|
140
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
|
+
|
141
158
|
### User-provided cache key
|
142
159
|
|
143
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):
|
@@ -196,6 +213,7 @@ end
|
|
196
213
|
|
197
214
|
The way cache key part is generated for the passed argument is the following:
|
198
215
|
|
216
|
+
- Use `object_cache_key: "some_cache_key"` if passed to `cache_fragment`
|
199
217
|
- Use `#graphql_cache_key` if implemented.
|
200
218
|
- Use `#cache_key` (or `#cache_key_with_version` for modern Rails) if implemented.
|
201
219
|
- Use `self.to_s` for _primitive_ types (strings, symbols, numbers, booleans).
|
@@ -312,6 +330,24 @@ end
|
|
312
330
|
|
313
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.
|
314
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
|
+
|
315
351
|
## Limitations
|
316
352
|
|
317
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:
|
@@ -113,8 +113,13 @@ module GraphQL
|
|
113
113
|
|
114
114
|
def build
|
115
115
|
Digest::SHA1.hexdigest("#{schema_cache_key}/#{query_cache_key}").then do |base_key|
|
116
|
-
|
117
|
-
|
116
|
+
if @options[:object_cache_key]
|
117
|
+
"#{base_key}/#{@options[:object_cache_key]}"
|
118
|
+
elsif object
|
119
|
+
"#{base_key}/#{object_key(object)}"
|
120
|
+
else
|
121
|
+
base_key
|
122
|
+
end
|
118
123
|
end
|
119
124
|
end
|
120
125
|
|
@@ -141,20 +146,22 @@ module GraphQL
|
|
141
146
|
end
|
142
147
|
|
143
148
|
def path_cache_key
|
144
|
-
|
149
|
+
@options.fetch(:path_cache_key) do
|
150
|
+
lookahead = query.lookahead
|
145
151
|
|
146
|
-
|
147
|
-
|
148
|
-
|
152
|
+
path.map { |field_name|
|
153
|
+
# Handle cached fields inside collections:
|
154
|
+
next field_name if field_name.is_a?(Integer)
|
149
155
|
|
150
|
-
|
151
|
-
|
156
|
+
lookahead = lookahead.selection_with_alias(field_name)
|
157
|
+
raise "Failed to look ahead the field: #{field_name}" if lookahead.is_a?(::GraphQL::Execution::Lookahead::NullLookahead)
|
152
158
|
|
153
|
-
|
159
|
+
next lookahead.field.name if lookahead.arguments.empty?
|
154
160
|
|
155
|
-
|
156
|
-
|
157
|
-
|
161
|
+
args = lookahead.arguments.map { |_1, _2| "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")
|
162
|
+
"#{lookahead.field.name}(#{args})"
|
163
|
+
}.join("/")
|
164
|
+
end
|
158
165
|
end
|
159
166
|
|
160
167
|
def traverse_argument(argument)
|
@@ -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
|
@@ -113,8 +113,13 @@ module GraphQL
|
|
113
113
|
|
114
114
|
def build
|
115
115
|
Digest::SHA1.hexdigest("#{schema_cache_key}/#{query_cache_key}").then do |base_key|
|
116
|
-
|
117
|
-
|
116
|
+
if @options[:object_cache_key]
|
117
|
+
"#{base_key}/#{@options[:object_cache_key]}"
|
118
|
+
elsif object
|
119
|
+
"#{base_key}/#{object_key(object)}"
|
120
|
+
else
|
121
|
+
base_key
|
122
|
+
end
|
118
123
|
end
|
119
124
|
end
|
120
125
|
|
@@ -141,20 +146,22 @@ module GraphQL
|
|
141
146
|
end
|
142
147
|
|
143
148
|
def path_cache_key
|
144
|
-
|
149
|
+
@options.fetch(:path_cache_key) do
|
150
|
+
lookahead = query.lookahead
|
145
151
|
|
146
|
-
|
147
|
-
|
148
|
-
|
152
|
+
path.map { |field_name|
|
153
|
+
# Handle cached fields inside collections:
|
154
|
+
next field_name if field_name.is_a?(Integer)
|
149
155
|
|
150
|
-
|
151
|
-
|
156
|
+
lookahead = lookahead.selection_with_alias(field_name)
|
157
|
+
raise "Failed to look ahead the field: #{field_name}" if lookahead.is_a?(::GraphQL::Execution::Lookahead::NullLookahead)
|
152
158
|
|
153
|
-
|
159
|
+
next lookahead.field.name if lookahead.arguments.empty?
|
154
160
|
|
155
|
-
|
156
|
-
|
157
|
-
|
161
|
+
args = lookahead.arguments.map { "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")
|
162
|
+
"#{lookahead.field.name}(#{args})"
|
163
|
+
}.join("/")
|
164
|
+
end
|
158
165
|
end
|
159
166
|
|
160
167
|
def traverse_argument(argument)
|
@@ -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,10 +18,10 @@ 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
|
@@ -32,6 +34,20 @@ module GraphQL
|
|
32
34
|
|
33
35
|
private
|
34
36
|
|
37
|
+
def read_from_context
|
38
|
+
if (loaded_value = context.loaded_fragments[cache_key])
|
39
|
+
return loaded_value
|
40
|
+
end
|
41
|
+
|
42
|
+
yield.tap { |value| context.loaded_fragments[cache_key] = value }
|
43
|
+
end
|
44
|
+
|
45
|
+
def value_from_cache
|
46
|
+
FragmentCache.cache_store.read(cache_key).tap do |cached|
|
47
|
+
return NIL_IN_CACHE if cached.nil? && FragmentCache.cache_store.exist?(cache_key)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
35
51
|
def interpreter_context
|
36
52
|
context.namespace(:interpreter)
|
37
53
|
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
|
|
@@ -6,6 +6,8 @@ module GraphQL
|
|
6
6
|
class CacheKeyBuilder
|
7
7
|
def object_key(obj)
|
8
8
|
return obj.graphql_cache_key if obj.respond_to?(:graphql_cache_key)
|
9
|
+
return obj.cache_key_with_version if obj.respond_to?(:cache_key_with_version)
|
10
|
+
return obj.cache_key if obj.respond_to?(:cache_key)
|
9
11
|
return obj.map { |item| object_key(item) }.join("/") if obj.is_a?(Array)
|
10
12
|
return object_key(obj.to_a) if obj.respond_to?(:to_a)
|
11
13
|
|
@@ -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.5.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-02-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: graphql
|
@@ -190,7 +190,7 @@ metadata:
|
|
190
190
|
homepage_uri: https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache
|
191
191
|
source_code_uri: https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache
|
192
192
|
changelog_uri: https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/CHANGELOG.md
|
193
|
-
post_install_message:
|
193
|
+
post_install_message:
|
194
194
|
rdoc_options: []
|
195
195
|
require_paths:
|
196
196
|
- lib
|
@@ -205,8 +205,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
205
205
|
- !ruby/object:Gem::Version
|
206
206
|
version: '0'
|
207
207
|
requirements: []
|
208
|
-
rubygems_version: 3.
|
209
|
-
signing_key:
|
208
|
+
rubygems_version: 3.1.2
|
209
|
+
signing_key:
|
210
210
|
specification_version: 4
|
211
211
|
summary: Fragment cache for graphql-ruby
|
212
212
|
test_files: []
|