graphql-fragment_cache 1.4.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: dab2cb1f4c27251da27c938bd8a0392f3ad0edcb5c54ba1d7d83d9e2f9c4b12b
4
- data.tar.gz: 8e5323ed6bf476d185c993b8165e292d4d7e70df56fa1ae1ad32da2ae7bc0880
3
+ metadata.gz: f193c09fd4f7682db9677d754964852e63b7541f2f2fa6d52ea81c8ee8b894fd
4
+ data.tar.gz: 328017637ba3c31c9c9af6c6beacdb63e696b13d6065c12d73b02874fb2e29d5
5
5
  SHA512:
6
- metadata.gz: 90eebb6973db225980926d330d93ca70a50768279bb727cf702f1bd905628cf7969e1af5025a6fefb1f9785d1fc008fd560d236fbe6315821a6dd2f92243ef56
7
- data.tar.gz: b26090f37a4b20ce45a256880b708c17a77871d8e8f5d95c1b34f171062306332f1bf9a549ab92bce5a326a2327d5b8a4f8106e2b2b4ac338bbd257cec8e6a21
6
+ metadata.gz: 9fd90630bd8a8505e2fe1a1277b621d22e2c62a23270a87a5487b56fdde57b980859611f80bfeb956915b581e396ee89a75da11548edebddf321ba45a0640e6c
7
+ data.tar.gz: 43fa3b1709999e950b8002f7764e0e63bdacd9aa32db644ead959e6cc12c0df8d92c26423300af4aad59139a8a30290a2c82cfe0cc4ff412656c20d4c64919e4
data/CHANGELOG.md CHANGED
@@ -2,6 +2,30 @@
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
+
12
+ ## 1.6.0 (2021-03-13)
13
+
14
+ - [PR#54](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/54) Include arguments in selections_cache_key ([@bbugh][])
15
+
16
+ ## 1.5.1 (2021-03-10)
17
+
18
+ - [PR#53](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/53) Use thread-safe query result for final_value ([@bbugh][])
19
+ - [PR#51](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/51) Do not cache fragments without final value ([@DmitryTsepelev][])
20
+
21
+ ## 1.5.0 (2021-02-20)
22
+
23
+ - [PR#50](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/50) Add object_cache_key to CacheKeyBuilder ([@bbugh][])
24
+
25
+ ## 1.4.1 (2021-01-21)
26
+
27
+ - [PR#48](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/48) Support graphql-ruby 1.12 ([@DmitryTsepelev][])
28
+
5
29
  ## 1.4.0 (2020-12-03)
6
30
 
7
31
  - [PR#41](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/41) Add `keep_in_context` option ([@DmitryTsepelev][])
@@ -82,3 +106,4 @@
82
106
  [@ssnickolay]: https://github.com/ssnickolay
83
107
  [@reabiliti]: https://github.com/reabiliti
84
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.
@@ -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,42 @@ 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
+ ## 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
+
247
329
  ## Cache storage and options
248
330
 
249
331
  It's up to your to decide which caching engine to use, all you need is to configure the cache store:
250
332
 
251
333
  ```ruby
252
- GraphQL::FragmentCache.cache_store = MyCacheStore.new
334
+ GraphQL::FragmentCache.configure do |config|
335
+ config.cache_store = MyCacheStore.new
336
+ end
253
337
  ```
254
338
 
255
339
  Or, in Rails:
@@ -334,7 +418,30 @@ This can reduce a number of cache calls but _increase_ memory usage, because the
334
418
 
335
419
  ## Limitations
336
420
 
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:
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:
338
445
 
339
446
  ```ruby
340
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
@@ -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,9 +21,10 @@ 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
- verify_interpreter!(schema_defn)
27
+ verify_interpreter_and_analysis!(schema_defn)
27
28
 
28
29
  schema_defn.tracer(Schema::Tracer)
29
30
  schema_defn.instrument(:query, Schema::Instrumentation)
@@ -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"
@@ -44,17 +49,33 @@ module GraphQL
44
49
  @cache_store = store
45
50
  end
46
51
 
47
- private
52
+ def graphql_ruby_1_12_or_later?
53
+ Gem::Dependency.new("graphql", ">= 1.12.0").match?("graphql", GraphQL::VERSION)
54
+ end
48
55
 
49
- def verify_interpreter!(schema_defn)
50
- unless schema_defn.interpreter?
51
- raise StandardError,
52
- "GraphQL::Execution::Interpreter should be enabled for fragment caching"
53
- end
56
+ private
54
57
 
55
- unless schema_defn.analysis_engine == GraphQL::Analysis::AST
56
- raise StandardError,
57
- "GraphQL::Analysis::AST should be enabled for fragment caching"
58
+ def verify_interpreter_and_analysis!(schema_defn)
59
+ if graphql_ruby_1_12_or_later?
60
+ unless schema_defn.interpreter?
61
+ raise StandardError,
62
+ "GraphQL::Execution::Execute should not be enabled for fragment caching"
63
+ end
64
+
65
+ unless schema_defn.analysis_engine == GraphQL::Analysis::AST
66
+ raise StandardError,
67
+ "GraphQL::Analysis should not be enabled for fragment caching"
68
+ end
69
+ else
70
+ unless schema_defn.interpreter?
71
+ raise StandardError,
72
+ "GraphQL::Execution::Interpreter should be enabled for fragment caching"
73
+ end
74
+
75
+ unless schema_defn.analysis_engine == GraphQL::Analysis::AST
76
+ raise StandardError,
77
+ "GraphQL::Analysis::AST should be enabled for fragment caching"
78
+ end
58
79
  end
59
80
  end
60
81
  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
- "#{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
@@ -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
@@ -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
@@ -11,9 +11,13 @@ module GraphQL
11
11
  extend Forwardable
12
12
 
13
13
  def self.included(base)
14
- return if base < GraphQL::Execution::Interpreter::HandlesRawValue
14
+ return if base.method_defined?(:raw_value)
15
15
 
16
- base.include(GraphQL::Execution::Interpreter::HandlesRawValue)
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
@@ -21,6 +25,13 @@ module GraphQL
21
25
  def cache_fragment(object_to_cache = NO_OBJECT, **options, &block)
22
26
  raise ArgumentError, "Block or argument must be provided" unless block_given? || object_to_cache != NO_OBJECT
23
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
+
24
35
  options[:object] = object_to_cache if object_to_cache != NO_OBJECT
25
36
 
26
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)
@@ -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"
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module FragmentCache
5
- VERSION = "1.4.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.4.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: 2020-12-03 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
@@ -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