graphql-fragment_cache 1.0.4 → 1.4.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: 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: []