graphql-fragment_cache 1.6.0 → 1.7.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: a5c6796b976bd78fc44c483dd9b39684081ee07bfb8b791d3a9bf00a31893ae7
4
- data.tar.gz: fc772a7e8c4319bd5b5bff5978ad20293334ac872be2fed170bc9084326874a0
3
+ metadata.gz: f193c09fd4f7682db9677d754964852e63b7541f2f2fa6d52ea81c8ee8b894fd
4
+ data.tar.gz: 328017637ba3c31c9c9af6c6beacdb63e696b13d6065c12d73b02874fb2e29d5
5
5
  SHA512:
6
- metadata.gz: 597046420b6342ca1c7e1429c1e44d36fa3a0eca9592d194318213614f3dc95c10eadf5ea616a13e8582f354caed9bf425b639149b15a7d9c3647e970fd3a780
7
- data.tar.gz: 1aa900bb9840ccb836498b79ef0031859304a328bebbce86fc32399e468d43ae1279daedc1bb6444e4366450ab5b11974dec679d4e11d8688fbea9f6bdf1c4a0
6
+ metadata.gz: 9fd90630bd8a8505e2fe1a1277b621d22e2c62a23270a87a5487b56fdde57b980859611f80bfeb956915b581e396ee89a75da11548edebddf321ba45a0640e6c
7
+ data.tar.gz: 43fa3b1709999e950b8002f7764e0e63bdacd9aa32db644ead959e6cc12c0df8d92c26423300af4aad59139a8a30290a2c82cfe0cc4ff412656c20d4c64919e4
data/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 1.7.0 (2021-04-30)
6
+
7
+ - [PR#62](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/62) Add a way to force a cache miss ([@jeromedalbert][])
8
+ - [PR#61](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/61) Add conditional caching ([@jeromedalbert][])
9
+ - [PR#64](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/64) Add a cache namespace ([@jeromedalbert][])
10
+ - [PR#63](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/63) Add a configure block notation ([@jeromedalbert][])
11
+
5
12
  ## 1.6.0 (2021-03-13)
6
13
 
7
14
  - [PR#54](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/54) Include arguments in selections_cache_key ([@bbugh][])
@@ -99,3 +106,4 @@
99
106
  [@ssnickolay]: https://github.com/ssnickolay
100
107
  [@reabiliti]: https://github.com/reabiliti
101
108
  [@bbugh]: https://github.com/bbugh
109
+ [@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.
@@ -155,7 +163,7 @@ class QueryType < BaseObject
155
163
  end
156
164
  ```
157
165
 
158
- ### User-provided cache key
166
+ ### User-provided cache key (custom key)
159
167
 
160
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):
161
169
 
@@ -184,6 +192,36 @@ cache_fragment(post)
184
192
  cache_fragment(post) { post }
185
193
  ```
186
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
+
187
225
  When using `cache_fragment:` option, it's only possible to use the resolved value as a cache key by setting:
188
226
 
189
227
  ```ruby
@@ -260,12 +298,42 @@ class QueryType < BaseObject
260
298
  end
261
299
  ```
262
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
+ ## Renewing the cache
318
+
319
+ You can force the cache to renew during query execution by adding
320
+ `renew_cache: true` to the query context:
321
+
322
+ ```ruby
323
+ MyAppSchema.execute("query { posts { title } }", context: {renew_cache: true})
324
+ ```
325
+
326
+ This will treat any cached value as missing even if it's present, and store
327
+ fresh new computed values in the cache. This can be useful for cache warmers.
328
+
263
329
  ## Cache storage and options
264
330
 
265
331
  It's up to your to decide which caching engine to use, all you need is to configure the cache store:
266
332
 
267
333
  ```ruby
268
- GraphQL::FragmentCache.cache_store = MyCacheStore.new
334
+ GraphQL::FragmentCache.configure do |config|
335
+ config.cache_store = MyCacheStore.new
336
+ end
269
337
  ```
270
338
 
271
339
  Or, in Rails:
@@ -350,7 +418,30 @@ This can reduce a number of cache calls but _increase_ memory usage, because the
350
418
 
351
419
  ## Limitations
352
420
 
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:
421
+ 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:
422
+
423
+ ```ruby
424
+ def cached_author_inside_batch
425
+ AuthorLoader.load(object).then do |author|
426
+ cache_fragment(author, context: context)
427
+ end
428
+ end
429
+ ```
430
+
431
+ 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:
432
+
433
+ ```ruby
434
+ def cached_author_inside_batch
435
+ outer_path = context.namespace(:interpreter)[:current_path]
436
+
437
+ AuthorLoader.load(object).then do |author|
438
+ context.namespace(:interpreter)[:current_path] = outer_path
439
+ cache_fragment(author, context: context)
440
+ end
441
+ end
442
+ ```
443
+
444
+ 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:
354
445
 
355
446
  ```ruby
356
447
  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
@@ -126,19 +126,19 @@ module GraphQL
126
126
  end
127
127
 
128
128
  def build
129
- Digest::SHA1.hexdigest("#{schema_cache_key}/#{query_cache_key}").then do |base_key|
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
137
- end
129
+ [
130
+ GraphQL::FragmentCache.namespace,
131
+ implicit_cache_key,
132
+ object_cache_key
133
+ ].compact.join("/")
138
134
  end
139
135
 
140
136
  private
141
137
 
138
+ def implicit_cache_key
139
+ Digest::SHA1.hexdigest("#{schema_cache_key}/#{query_cache_key}")
140
+ end
141
+
142
142
  def schema_cache_key
143
143
  @options.fetch(:schema_cache_key) { schema.schema_cache_key }
144
144
  end
@@ -184,8 +184,12 @@ module GraphQL
184
184
  "{#{argument.map { |_1, _2| "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")}}"
185
185
  end
186
186
 
187
+ def object_cache_key
188
+ @options[:object_cache_key] || object_key(object)
189
+ end
190
+
187
191
  def object_key(obj)
188
- obj._graphql_cache_key
192
+ obj&._graphql_cache_key
189
193
  end
190
194
  end
191
195
  end
@@ -21,6 +21,7 @@ module GraphQL
21
21
  module FragmentCache
22
22
  class << self
23
23
  attr_reader :cache_store
24
+ attr_accessor :namespace
24
25
 
25
26
  def use(schema_defn, options = {})
26
27
  verify_interpreter_and_analysis!(schema_defn)
@@ -32,6 +33,10 @@ module GraphQL
32
33
  GraphQL::Pagination::Connections.prepend(Connections::Patch)
33
34
  end
34
35
 
36
+ def configure
37
+ yield self
38
+ end
39
+
35
40
  def cache_store=(store)
36
41
  unless store.respond_to?(:read)
37
42
  raise ArgumentError, "Store must implement #read(key) method"
@@ -126,19 +126,19 @@ module GraphQL
126
126
  end
127
127
 
128
128
  def build
129
- Digest::SHA1.hexdigest("#{schema_cache_key}/#{query_cache_key}").then do |base_key|
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
137
- end
129
+ [
130
+ GraphQL::FragmentCache.namespace,
131
+ implicit_cache_key,
132
+ object_cache_key
133
+ ].compact.join("/")
138
134
  end
139
135
 
140
136
  private
141
137
 
138
+ def implicit_cache_key
139
+ Digest::SHA1.hexdigest("#{schema_cache_key}/#{query_cache_key}")
140
+ end
141
+
142
142
  def schema_cache_key
143
143
  @options.fetch(:schema_cache_key) { schema.schema_cache_key }
144
144
  end
@@ -184,8 +184,12 @@ module GraphQL
184
184
  "{#{argument.map { "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")}}"
185
185
  end
186
186
 
187
+ def object_cache_key
188
+ @options[:object_cache_key] || object_key(object)
189
+ end
190
+
187
191
  def object_key(obj)
188
- obj._graphql_cache_key
192
+ obj&._graphql_cache_key
189
193
  end
190
194
  end
191
195
  end
@@ -39,6 +39,13 @@ module GraphQL
39
39
  def resolve(object:, arguments:, **_options)
40
40
  resolved_value = NOT_RESOLVED
41
41
 
42
+ if @cache_options[:if].is_a?(Proc)
43
+ @cache_options[:if] = object.instance_exec(&@cache_options[:if])
44
+ end
45
+ if @cache_options[:unless].is_a?(Proc)
46
+ @cache_options[:unless] = object.instance_exec(&@cache_options[:unless])
47
+ end
48
+
42
49
  object_for_key = if @context_key
43
50
  Array(@context_key).map { |key| object.context[key] }
44
51
  elsif @cache_key == :object
@@ -46,7 +53,6 @@ module GraphQL
46
53
  elsif @cache_key == :value
47
54
  resolved_value = yield(object, arguments)
48
55
  end
49
-
50
56
  cache_fragment_options = @cache_options.merge(object: object_for_key)
51
57
 
52
58
  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
@@ -25,6 +25,13 @@ 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
+ if options.key?(:if) || options.key?(:unless)
29
+ disabled = options.key?(:if) ? !options.delete(:if) : options.delete(:unless)
30
+ if disabled
31
+ return block_given? ? block.call : object_to_cache
32
+ end
33
+ end
34
+
28
35
  options[:object] = object_to_cache if object_to_cache != NO_OBJECT
29
36
 
30
37
  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.6.0"
5
+ VERSION = "1.7.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.6.0
4
+ version: 1.7.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-03-13 00:00:00.000000000 Z
11
+ date: 2021-04-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -176,6 +176,7 @@ files:
176
176
  - README.md
177
177
  - bin/console
178
178
  - bin/setup
179
+ - lib/.rbnext/2.3/graphql/fragment_cache/cache_key_builder.rb
179
180
  - lib/.rbnext/2.3/graphql/fragment_cache/memory_store.rb
180
181
  - lib/.rbnext/2.7/graphql/fragment_cache/cache_key_builder.rb
181
182
  - lib/.rbnext/2.7/graphql/fragment_cache/ext/graphql_cache_key.rb