graphql-fragment_cache 1.0.4 → 1.4.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: 4a52d1b703c963cd27803c1c28ca0e8c1a66775a84ad8963f88a0e414ec7ed6c
4
- data.tar.gz: 4739e71ae750d7fbb8c9366e17da84ad9ae921409308a1b109fc60d8add0557a
3
+ metadata.gz: dab2cb1f4c27251da27c938bd8a0392f3ad0edcb5c54ba1d7d83d9e2f9c4b12b
4
+ data.tar.gz: 8e5323ed6bf476d185c993b8165e292d4d7e70df56fa1ae1ad32da2ae7bc0880
5
5
  SHA512:
6
- metadata.gz: 889510e3937c874742624f6da651914ed98c6a996842ec57fcda8121287eead193e12b974247733b38ffc7662e542eb1a82b2fb43d18ef5a121cc41607f811fb
7
- data.tar.gz: 4e9dbeb855f55bf37fbe14900b7c4b1b398155902759fd905a87c70da6dfd67850102be48c5b350d6b3126090ced6aaf0bb5b95a7f16a990dd8b070b589e865c
6
+ metadata.gz: 90eebb6973db225980926d330d93ca70a50768279bb727cf702f1bd905628cf7969e1af5025a6fefb1f9785d1fc008fd560d236fbe6315821a6dd2f92243ef56
7
+ data.tar.gz: b26090f37a4b20ce45a256880b708c17a77871d8e8f5d95c1b34f171062306332f1bf9a549ab92bce5a326a2327d5b8a4f8106e2b2b4ac338bbd257cec8e6a21
@@ -2,6 +2,26 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 1.4.0 (2020-12-03)
6
+
7
+ - [PR#41](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/41) Add `keep_in_context` option ([@DmitryTsepelev][])
8
+
9
+ ## 1.3.0 (2020-11-25)
10
+
11
+ - [PR#39](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/39) Implement `path_cache_key` option ([@DmitryTsepelev][])
12
+
13
+ ## 1.2.0 (2020-10-26)
14
+
15
+ - [PR#37](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/37) Try to use `cache_key_with_version` or `cache_key` with Rails CacheKeyBuilder ([@bbugh][])
16
+
17
+ ## 1.1.0 (2020-10-26)
18
+
19
+ - [PR#38](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/38) Support caching from other places than field or resolver ([@DmitryTsepelev][])
20
+
21
+ ## 1.0.5 (2020-10-13)
22
+
23
+ - [PR#35](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/35) Prefer using `#write_multi` on cache store when possible ([@DmitryTsepelev][])
24
+
5
25
  ## 1.0.4 (2020-10-12)
6
26
 
7
27
  - [PR#34](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/34) Avoid unneded default calculation in CacheKeyBuilder ([@DmitryTsepelev][])
@@ -61,3 +81,4 @@
61
81
  [@palkan]: https://github.com/palkan
62
82
  [@ssnickolay]: https://github.com/ssnickolay
63
83
  [@reabiliti]: https://github.com/reabiliti
84
+ [@bbugh]: https://github.com/bbugh
data/README.md CHANGED
@@ -38,6 +38,14 @@ class BaseType < GraphQL::Schema::Object
38
38
  end
39
39
  ```
40
40
 
41
+ If you're using [resolvers](https://graphql-ruby.org/fields/resolvers.html) — include the module into the base resolver as well:
42
+
43
+ ```ruby
44
+ class Resolvers::BaseResolver < GraphQL::Schema::Resolver
45
+ include GraphQL::FragmentCache::ObjectHelpers
46
+ end
47
+ ```
48
+
41
49
  Now you can add `cache_fragment:` option to your fields to turn caching on:
42
50
 
43
51
  ```ruby
@@ -47,7 +55,7 @@ class PostType < BaseObject
47
55
  end
48
56
  ```
49
57
 
50
- Alternatively, you can use `cache_fragment` method inside resolvers:
58
+ Alternatively, you can use `cache_fragment` method inside resolver methods:
51
59
 
52
60
  ```ruby
53
61
  class QueryType < BaseObject
@@ -107,7 +115,7 @@ query_cache_key = Digest::SHA1.hexdigest("#{path_cache_key}#{selections_cache_ke
107
115
  cache_key = "#{schema_cache_key}/#{query_cache_key}"
108
116
  ```
109
117
 
110
- You can override `schema_cache_key` or `query_cache_key` by passing parameters to the `cache_fragment` calls:
118
+ You can override `schema_cache_key`, `query_cache_key` or `path_cache_key` by passing parameters to the `cache_fragment` calls:
111
119
 
112
120
  ```ruby
113
121
  class QueryType < BaseObject
@@ -121,6 +129,8 @@ class QueryType < BaseObject
121
129
  end
122
130
  ```
123
131
 
132
+ Overriding `path_cache_key` might be helpful when you resolve the same object nested in multiple places (e.g., `Post` and `Comment` both have `author`), but want to make sure cache will be invalidated when selection set is different.
133
+
124
134
  Same for the option:
125
135
 
126
136
  ```ruby
@@ -252,7 +262,7 @@ Rails.application.configure do |config|
252
262
  end
253
263
  ```
254
264
 
255
- ⚠️ Cache store must implement `#read(key)`, `#write(key, value, **options)` and `#exist?(key)` methods.
265
+ ⚠️ Cache store must implement `#read(key)`, `#exist?(key)` and `#write_multi(hash, **options)` or `#write(key, value, **options)` methods.
256
266
 
257
267
  The gem provides only in-memory store out-of-the-box (`GraphQL::FragmentCache::MemoryStore`). It's used by default.
258
268
 
@@ -275,6 +285,53 @@ class QueryType < BaseObject
275
285
  end
276
286
  ```
277
287
 
288
+ ## How to use `#cache_fragment` in extensions (and other places where context is not available)
289
+
290
+ If you want to call `#cache_fragment` from places other that fields or resolvers, you'll need to pass `context` explicitly and turn on `raw_value` support. For instance, let's take a look at this extension:
291
+
292
+ ```ruby
293
+ class Types::QueryType < Types::BaseObject
294
+ class CurrentMomentExtension < GraphQL::Schema::FieldExtension
295
+ # turning on cache_fragment support
296
+ include GraphQL::FragmentCache::ObjectHelpers
297
+
298
+ def resolve(object:, arguments:, context:)
299
+ # context is passed explicitly
300
+ cache_fragment(context: context) do
301
+ result = yield(object, arguments)
302
+ "#{result} (at #{Time.now})"
303
+ end
304
+ end
305
+ end
306
+
307
+ field :event, String, null: false, extensions: [CurrentMomentExtension]
308
+
309
+ def event
310
+ "something happened"
311
+ end
312
+ end
313
+ ```
314
+
315
+ With this approach you can use `#cache_fragment` in any place you have an access to the `context`. When context is not available, the error `cannot find context, please pass it explicitly` will be thrown.
316
+
317
+ ## In–memory fragments
318
+
319
+ If you have a fragment that accessed from multiple times (e.g., if you have a list of items that belong to the same owner, and owner is cached), you can avoid multiple cache reads by using `:keep_in_context` option:
320
+
321
+ ```ruby
322
+ class QueryType < BaseObject
323
+ field :post, PostType, null: true do
324
+ argument :id, ID, required: true
325
+ end
326
+
327
+ def post(id:)
328
+ cache_fragment(keep_in_context: true, expires_in: 5.minutes) { Post.find(id) }
329
+ end
330
+ end
331
+ ```
332
+
333
+ This can reduce a number of cache calls but _increase_ memory usage, because the value returned from cache will be kept in the GraphQL context until the query is fully resolved.
334
+
278
335
  ## Limitations
279
336
 
280
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:
@@ -141,20 +141,22 @@ module GraphQL
141
141
  end
142
142
 
143
143
  def path_cache_key
144
- lookahead = query.lookahead
144
+ @options.fetch(:path_cache_key) do
145
+ lookahead = query.lookahead
145
146
 
146
- path.map { |field_name|
147
- # Handle cached fields inside collections:
148
- next field_name if field_name.is_a?(Integer)
147
+ path.map { |field_name|
148
+ # Handle cached fields inside collections:
149
+ next field_name if field_name.is_a?(Integer)
149
150
 
150
- lookahead = lookahead.selection_with_alias(field_name)
151
- raise "Failed to look ahead the field: #{field_name}" if lookahead.is_a?(::GraphQL::Execution::Lookahead::NullLookahead)
151
+ lookahead = lookahead.selection_with_alias(field_name)
152
+ raise "Failed to look ahead the field: #{field_name}" if lookahead.is_a?(::GraphQL::Execution::Lookahead::NullLookahead)
152
153
 
153
- next lookahead.field.name if lookahead.arguments.empty?
154
+ next lookahead.field.name if lookahead.arguments.empty?
154
155
 
155
- args = lookahead.arguments.map { |_1, _2| "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")
156
- "#{lookahead.field.name}(#{args})"
157
- }.join("/")
156
+ args = lookahead.arguments.map { |_1, _2| "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")
157
+ "#{lookahead.field.name}(#{args})"
158
+ }.join("/")
159
+ end
158
160
  end
159
161
 
160
162
  def traverse_argument(argument)
@@ -141,20 +141,22 @@ module GraphQL
141
141
  end
142
142
 
143
143
  def path_cache_key
144
- lookahead = query.lookahead
144
+ @options.fetch(:path_cache_key) do
145
+ lookahead = query.lookahead
145
146
 
146
- path.map { |field_name|
147
- # Handle cached fields inside collections:
148
- next field_name if field_name.is_a?(Integer)
147
+ path.map { |field_name|
148
+ # Handle cached fields inside collections:
149
+ next field_name if field_name.is_a?(Integer)
149
150
 
150
- lookahead = lookahead.selection_with_alias(field_name)
151
- raise "Failed to look ahead the field: #{field_name}" if lookahead.is_a?(::GraphQL::Execution::Lookahead::NullLookahead)
151
+ lookahead = lookahead.selection_with_alias(field_name)
152
+ raise "Failed to look ahead the field: #{field_name}" if lookahead.is_a?(::GraphQL::Execution::Lookahead::NullLookahead)
152
153
 
153
- next lookahead.field.name if lookahead.arguments.empty?
154
+ next lookahead.field.name if lookahead.arguments.empty?
154
155
 
155
- args = lookahead.arguments.map { "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")
156
- "#{lookahead.field.name}(#{args})"
157
- }.join("/")
156
+ args = lookahead.arguments.map { "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")
157
+ "#{lookahead.field.name}(#{args})"
158
+ }.join("/")
159
+ end
158
160
  end
159
161
 
160
162
  def traverse_argument(argument)
@@ -10,7 +10,26 @@ module GraphQL
10
10
  def call(query)
11
11
  return unless query.context.fragments?
12
12
 
13
- query.context.fragments.each(&:persist)
13
+ if FragmentCache.cache_store.respond_to?(:write_multi)
14
+ batched_persist(query)
15
+ else
16
+ persist(query)
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def batched_persist(query)
23
+ query.context.fragments.group_by(&:options).each do |options, group|
24
+ hash = group.map { |fragment| [fragment.cache_key, fragment.value] }.to_h
25
+ FragmentCache.cache_store.write_multi(hash, **options)
26
+ end
27
+ end
28
+
29
+ def persist(query)
30
+ query.context.fragments.each do |fragment|
31
+ FragmentCache.cache_store.write(fragment.cache_key, fragment.value, **fragment.options)
32
+ end
14
33
  end
15
34
  end
16
35
  end
@@ -13,6 +13,10 @@ module GraphQL
13
13
  def fragments
14
14
  namespace(:fragment_cache)[:fragments] ||= []
15
15
  end
16
+
17
+ def loaded_fragments
18
+ namespace(:fragment_cache)[:loaded] ||= {}
19
+ end
16
20
  end
17
21
  end
18
22
  end
@@ -49,7 +49,7 @@ module GraphQL
49
49
 
50
50
  cache_fragment_options = @cache_options.merge(object: object_for_key)
51
51
 
52
- object.cache_fragment(cache_fragment_options) do
52
+ object.cache_fragment(**cache_fragment_options) do
53
53
  resolved_value == NOT_RESOLVED ? yield(object, arguments) : resolved_value
54
54
  end
55
55
  end
@@ -4,6 +4,8 @@ require "graphql/fragment_cache/cache_key_builder"
4
4
 
5
5
  module GraphQL
6
6
  module FragmentCache
7
+ using Ext
8
+
7
9
  # Represents a single fragment to cache
8
10
  class Fragment
9
11
  attr_reader :options, :path, :context
@@ -16,21 +18,34 @@ module GraphQL
16
18
 
17
19
  NIL_IN_CACHE = Object.new
18
20
 
19
- def read
20
- FragmentCache.cache_store.read(cache_key).tap do |cached|
21
- return NIL_IN_CACHE if cached.nil? && FragmentCache.cache_store.exist?(cache_key)
22
- end
21
+ def read(keep_in_context = false)
22
+ return read_from_context { value_from_cache } if keep_in_context
23
+
24
+ value_from_cache
25
+ end
26
+
27
+ def cache_key
28
+ @cache_key ||= CacheKeyBuilder.call(path: path, query: context.query, **options)
23
29
  end
24
30
 
25
- def persist
26
- value = final_value.dig(*path)
27
- FragmentCache.cache_store.write(cache_key, value, **options)
31
+ def value
32
+ final_value.dig(*path)
28
33
  end
29
34
 
30
35
  private
31
36
 
32
- def cache_key
33
- @cache_key ||= CacheKeyBuilder.call(path: path, query: context.query, **options)
37
+ def read_from_context
38
+ if (loaded_value = context.loaded_fragments[cache_key])
39
+ return loaded_value
40
+ end
41
+
42
+ yield.tap { |value| context.loaded_fragments[cache_key] = value }
43
+ end
44
+
45
+ def value_from_cache
46
+ FragmentCache.cache_store.read(cache_key).tap do |cached|
47
+ return NIL_IN_CACHE if cached.nil? && FragmentCache.cache_store.exist?(cache_key)
48
+ end
34
49
  end
35
50
 
36
51
  def interpreter_context
@@ -10,6 +10,12 @@ module GraphQL
10
10
  module ObjectHelpers
11
11
  extend Forwardable
12
12
 
13
+ def self.included(base)
14
+ return if base < GraphQL::Execution::Interpreter::HandlesRawValue
15
+
16
+ base.include(GraphQL::Execution::Interpreter::HandlesRawValue)
17
+ end
18
+
13
19
  NO_OBJECT = Object.new
14
20
 
15
21
  def cache_fragment(object_to_cache = NO_OBJECT, **options, &block)
@@ -17,14 +23,19 @@ module GraphQL
17
23
 
18
24
  options[:object] = object_to_cache if object_to_cache != NO_OBJECT
19
25
 
20
- fragment = Fragment.new(context, options)
26
+ context_to_use = options.delete(:context)
27
+ context_to_use = context if context_to_use.nil? && respond_to?(:context)
28
+ raise ArgumentError, "cannot find context, please pass it explicitly" unless context_to_use
29
+
30
+ fragment = Fragment.new(context_to_use, **options)
21
31
 
22
- if (cached = fragment.read)
32
+ keep_in_context = options.delete(:keep_in_context)
33
+ if (cached = fragment.read(keep_in_context))
23
34
  return cached == Fragment::NIL_IN_CACHE ? nil : raw_value(cached)
24
35
  end
25
36
 
26
37
  (block_given? ? block.call : object_to_cache).tap do |resolved_value|
27
- context.fragments << fragment
38
+ context_to_use.fragments << fragment
28
39
  end
29
40
  end
30
41
  end
@@ -6,6 +6,8 @@ module GraphQL
6
6
  class CacheKeyBuilder
7
7
  def object_key(obj)
8
8
  return obj.graphql_cache_key if obj.respond_to?(:graphql_cache_key)
9
+ return obj.cache_key_with_version if obj.respond_to?(:cache_key_with_version)
10
+ return obj.cache_key if obj.respond_to?(:cache_key)
9
11
  return obj.map { |item| object_key(item) }.join("/") if obj.is_a?(Array)
10
12
  return object_key(obj.to_a) if obj.respond_to?(:to_a)
11
13
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module FragmentCache
5
- VERSION = "1.0.4"
5
+ VERSION = "1.4.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.0.4
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - DmitryTsepelev
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-10-12 00:00:00.000000000 Z
11
+ date: 2020-12-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -190,7 +190,7 @@ metadata:
190
190
  homepage_uri: https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache
191
191
  source_code_uri: https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache
192
192
  changelog_uri: https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/CHANGELOG.md
193
- post_install_message:
193
+ post_install_message:
194
194
  rdoc_options: []
195
195
  require_paths:
196
196
  - lib
@@ -205,8 +205,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
205
205
  - !ruby/object:Gem::Version
206
206
  version: '0'
207
207
  requirements: []
208
- rubygems_version: 3.0.3
209
- signing_key:
208
+ rubygems_version: 3.1.2
209
+ signing_key:
210
210
  specification_version: 4
211
211
  summary: Fragment cache for graphql-ruby
212
212
  test_files: []