graphql-fragment_cache 1.4.0 → 1.7.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 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