graphql-fragment_cache 1.4.1 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 89da5923396f256876f648b15e23b77b14a731b4e5b642fc67074d50fcefc5c3
4
- data.tar.gz: b4c5a2fe21477726e41835168a8768e8adba9d03d3bf3f618705a31e0ee391a1
3
+ metadata.gz: 7ad8755b7ad0579ac5b5a663a9c680fd5132ef818f39881b4df5d2a9c09a3d4b
4
+ data.tar.gz: b2132f313316e1f413b2fd07120b3c2a8c0465f66cc6183f32661a8f606074a4
5
5
  SHA512:
6
- metadata.gz: 470dc39abc207c3d3db874d1a954d665fadf105c31b3e62ef18bfe0d60ca002e505e7861a52ee2ba652a4789f2c3566c2b7e2134fe22d09ef62b6fe17cf95a7d
7
- data.tar.gz: ceb3ec4abdf657527aac1e2ca5ab441bb3551a33aefe2fd38c2e1f6cb31e9bbea52b19c9aa30edf10a9e0071593697d4c6cd7f3b9254ad3682a995796375abc4
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 (provided by user) parts.
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 (its prefix) contains the information about the schema and the current query. It includes:
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 `path_cache_key` by passing parameters to the `cache_fragment` calls:
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
- ### User-provided cache key
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.cache_store = MyCacheStore.new
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
- 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:
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
- "#{val.field.name}#{children}"
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
- Digest::SHA1.hexdigest("#{schema_cache_key}/#{query_cache_key}").then do |base_key|
116
- next base_key unless object
117
- "#{base_key}/#{object_key(object)}"
118
- end
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._graphql_cache_key
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
- "#{val.field.name}#{children}"
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
- Digest::SHA1.hexdigest("#{schema_cache_key}/#{query_cache_key}").then do |base_key|
116
- next base_key unless object
117
- "#{base_key}/#{object_key(object)}"
118
- end
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._graphql_cache_key
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.context.fragments.group_by(&:options).each do |options, group|
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.context.fragments.each do |fragment|
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 ||= interpreter_context[:runtime].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)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module FragmentCache
5
- VERSION = "1.4.1"
5
+ VERSION = "1.8.0"
6
6
  end
7
7
  end
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.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-01-21 00:00:00.000000000 Z
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