graphql-fragment_cache 1.1.0 → 1.5.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: b8dcfa21629a163b02f1097275602c294eb6e6cad5be237a11f5567c3fedce8c
4
- data.tar.gz: 812fde3ac50fcb43ebb5e00c8291817c876a9bb0c7ccaa5544319dd01fb7a143
3
+ metadata.gz: fc02c6847d8e55983814bba96e0bd303893a1639648530048f0ccf6ab1527eab
4
+ data.tar.gz: 52530f6e108ed656077a02a78a44dd2cf498e4004b9d27897572f6f77aaec353
5
5
  SHA512:
6
- metadata.gz: da34c5a7e12669352a0a095338bd52895414a49628d758cf0df6d698fc342f6b94032fdce6bf8df70b866ea6ddf2a5d6666ab7dd7b70050ab0b058448b8dcfca
7
- data.tar.gz: 937fa344650d90187acc5985c86b4f3320ed276a03aaf02531484596e1a0b20b09cc2fdb2d4596c8150d3992fdef9c48085623ff1030e16135da7f019b255e46
6
+ metadata.gz: e5b95e2b3f68011a8cdc0fc23b3a5f117f0ce9c711efcfb504230f6a84fb8c6c53a156d6671f97c8ea52cad199de597954badb2e456d33303b80e6e2e2105bec
7
+ data.tar.gz: d7275ef053d1127a17476244bd7253cb98dab81df865195b7bae7241bcd6106564af13848dfeaf39676acb96e962be73fb7782274ae4ccce2190065234b5fbf9
data/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 1.5.0 (2021-02-20)
6
+
7
+ - [PR#50](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/50) Add object_cache_key to CacheKeyBuilder ([@bbugh][])
8
+
9
+ ## 1.4.1 (2021-01-21)
10
+
11
+ - [PR#48](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/48) Support graphql-ruby 1.12 ([@DmitryTsepelev][])
12
+
13
+ ## 1.4.0 (2020-12-03)
14
+
15
+ - [PR#41](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/41) Add `keep_in_context` option ([@DmitryTsepelev][])
16
+
17
+ ## 1.3.0 (2020-11-25)
18
+
19
+ - [PR#39](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/39) Implement `path_cache_key` option ([@DmitryTsepelev][])
20
+
21
+ ## 1.2.0 (2020-10-26)
22
+
23
+ - [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][])
24
+
5
25
  ## 1.1.0 (2020-10-26)
6
26
 
7
27
  - [PR#38](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/38) Support caching from other places than field or resolver ([@DmitryTsepelev][])
@@ -69,3 +89,4 @@
69
89
  [@palkan]: https://github.com/palkan
70
90
  [@ssnickolay]: https://github.com/ssnickolay
71
91
  [@reabiliti]: https://github.com/reabiliti
92
+ [@bbugh]: https://github.com/bbugh
data/README.md CHANGED
@@ -112,10 +112,10 @@ selections_cache_key = "[#{%w[id name].join(".")}]"
112
112
 
113
113
  query_cache_key = Digest::SHA1.hexdigest("#{path_cache_key}#{selections_cache_key}")
114
114
 
115
- cache_key = "#{schema_cache_key}/#{query_cache_key}"
115
+ cache_key = "#{schema_cache_key}/#{query_cache_key}/#{object_cache_key}"
116
116
  ```
117
117
 
118
- 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`, `path_cache_key` or `object_cache_key` by passing parameters to the `cache_fragment` calls:
119
119
 
120
120
  ```ruby
121
121
  class QueryType < BaseObject
@@ -129,6 +129,8 @@ class QueryType < BaseObject
129
129
  end
130
130
  ```
131
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
+
132
134
  Same for the option:
133
135
 
134
136
  ```ruby
@@ -138,6 +140,21 @@ class PostType < BaseObject
138
140
  end
139
141
  ```
140
142
 
143
+ 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.
144
+
145
+ ```ruby
146
+ class QueryType < BaseObject
147
+ field :post, PostType, null: true do
148
+ argument :id, ID, required: true
149
+ end
150
+
151
+ def post(id:)
152
+ query = Post.where("updated_at < ?", Time.now - 1.day)
153
+ cache_fragment(object_cache_key: query.cache_key) { query.some_process }
154
+ end
155
+ end
156
+ ```
157
+
141
158
  ### User-provided cache key
142
159
 
143
160
  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):
@@ -196,6 +213,7 @@ end
196
213
 
197
214
  The way cache key part is generated for the passed argument is the following:
198
215
 
216
+ - Use `object_cache_key: "some_cache_key"` if passed to `cache_fragment`
199
217
  - Use `#graphql_cache_key` if implemented.
200
218
  - Use `#cache_key` (or `#cache_key_with_version` for modern Rails) if implemented.
201
219
  - Use `self.to_s` for _primitive_ types (strings, symbols, numbers, booleans).
@@ -312,6 +330,24 @@ end
312
330
 
313
331
  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.
314
332
 
333
+ ## In–memory fragments
334
+
335
+ 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:
336
+
337
+ ```ruby
338
+ class QueryType < BaseObject
339
+ field :post, PostType, null: true do
340
+ argument :id, ID, required: true
341
+ end
342
+
343
+ def post(id:)
344
+ cache_fragment(keep_in_context: true, expires_in: 5.minutes) { Post.find(id) }
345
+ end
346
+ end
347
+ ```
348
+
349
+ 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.
350
+
315
351
  ## Limitations
316
352
 
317
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:
@@ -113,8 +113,13 @@ module GraphQL
113
113
 
114
114
  def build
115
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)}"
116
+ if @options[:object_cache_key]
117
+ "#{base_key}/#{@options[:object_cache_key]}"
118
+ elsif object
119
+ "#{base_key}/#{object_key(object)}"
120
+ else
121
+ base_key
122
+ end
118
123
  end
119
124
  end
120
125
 
@@ -141,20 +146,22 @@ module GraphQL
141
146
  end
142
147
 
143
148
  def path_cache_key
144
- lookahead = query.lookahead
149
+ @options.fetch(:path_cache_key) do
150
+ lookahead = query.lookahead
145
151
 
146
- path.map { |field_name|
147
- # Handle cached fields inside collections:
148
- next field_name if field_name.is_a?(Integer)
152
+ path.map { |field_name|
153
+ # Handle cached fields inside collections:
154
+ next field_name if field_name.is_a?(Integer)
149
155
 
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)
156
+ lookahead = lookahead.selection_with_alias(field_name)
157
+ raise "Failed to look ahead the field: #{field_name}" if lookahead.is_a?(::GraphQL::Execution::Lookahead::NullLookahead)
152
158
 
153
- next lookahead.field.name if lookahead.arguments.empty?
159
+ next lookahead.field.name if lookahead.arguments.empty?
154
160
 
155
- args = lookahead.arguments.map { |_1, _2| "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")
156
- "#{lookahead.field.name}(#{args})"
157
- }.join("/")
161
+ args = lookahead.arguments.map { |_1, _2| "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")
162
+ "#{lookahead.field.name}(#{args})"
163
+ }.join("/")
164
+ end
158
165
  end
159
166
 
160
167
  def traverse_argument(argument)
@@ -23,7 +23,7 @@ module GraphQL
23
23
  attr_reader :cache_store
24
24
 
25
25
  def use(schema_defn, options = {})
26
- verify_interpreter!(schema_defn)
26
+ verify_interpreter_and_analysis!(schema_defn)
27
27
 
28
28
  schema_defn.tracer(Schema::Tracer)
29
29
  schema_defn.instrument(:query, Schema::Instrumentation)
@@ -44,17 +44,33 @@ module GraphQL
44
44
  @cache_store = store
45
45
  end
46
46
 
47
- private
47
+ def graphql_ruby_1_12_or_later?
48
+ Gem::Dependency.new("graphql", ">= 1.12.0").match?("graphql", GraphQL::VERSION)
49
+ end
48
50
 
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
51
+ private
54
52
 
55
- unless schema_defn.analysis_engine == GraphQL::Analysis::AST
56
- raise StandardError,
57
- "GraphQL::Analysis::AST should be enabled for fragment caching"
53
+ def verify_interpreter_and_analysis!(schema_defn)
54
+ if graphql_ruby_1_12_or_later?
55
+ unless schema_defn.interpreter?
56
+ raise StandardError,
57
+ "GraphQL::Execution::Execute should not be enabled for fragment caching"
58
+ end
59
+
60
+ unless schema_defn.analysis_engine == GraphQL::Analysis::AST
61
+ raise StandardError,
62
+ "GraphQL::Analysis should not be enabled for fragment caching"
63
+ end
64
+ else
65
+ unless schema_defn.interpreter?
66
+ raise StandardError,
67
+ "GraphQL::Execution::Interpreter should be enabled for fragment caching"
68
+ end
69
+
70
+ unless schema_defn.analysis_engine == GraphQL::Analysis::AST
71
+ raise StandardError,
72
+ "GraphQL::Analysis::AST should be enabled for fragment caching"
73
+ end
58
74
  end
59
75
  end
60
76
  end
@@ -113,8 +113,13 @@ module GraphQL
113
113
 
114
114
  def build
115
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)}"
116
+ if @options[:object_cache_key]
117
+ "#{base_key}/#{@options[:object_cache_key]}"
118
+ elsif object
119
+ "#{base_key}/#{object_key(object)}"
120
+ else
121
+ base_key
122
+ end
118
123
  end
119
124
  end
120
125
 
@@ -141,20 +146,22 @@ module GraphQL
141
146
  end
142
147
 
143
148
  def path_cache_key
144
- lookahead = query.lookahead
149
+ @options.fetch(:path_cache_key) do
150
+ lookahead = query.lookahead
145
151
 
146
- path.map { |field_name|
147
- # Handle cached fields inside collections:
148
- next field_name if field_name.is_a?(Integer)
152
+ path.map { |field_name|
153
+ # Handle cached fields inside collections:
154
+ next field_name if field_name.is_a?(Integer)
149
155
 
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)
156
+ lookahead = lookahead.selection_with_alias(field_name)
157
+ raise "Failed to look ahead the field: #{field_name}" if lookahead.is_a?(::GraphQL::Execution::Lookahead::NullLookahead)
152
158
 
153
- next lookahead.field.name if lookahead.arguments.empty?
159
+ next lookahead.field.name if lookahead.arguments.empty?
154
160
 
155
- args = lookahead.arguments.map { "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")
156
- "#{lookahead.field.name}(#{args})"
157
- }.join("/")
161
+ args = lookahead.arguments.map { "#{_1}:#{traverse_argument(_2)}" }.sort.join(",")
162
+ "#{lookahead.field.name}(#{args})"
163
+ }.join("/")
164
+ end
158
165
  end
159
166
 
160
167
  def traverse_argument(argument)
@@ -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,10 +18,10 @@ 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
23
25
  end
24
26
 
25
27
  def cache_key
@@ -32,6 +34,20 @@ module GraphQL
32
34
 
33
35
  private
34
36
 
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
49
+ end
50
+
35
51
  def interpreter_context
36
52
  context.namespace(:interpreter)
37
53
  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
@@ -27,9 +31,10 @@ module GraphQL
27
31
  context_to_use = context if context_to_use.nil? && respond_to?(:context)
28
32
  raise ArgumentError, "cannot find context, please pass it explicitly" unless context_to_use
29
33
 
30
- fragment = Fragment.new(context_to_use, options)
34
+ fragment = Fragment.new(context_to_use, **options)
31
35
 
32
- if (cached = fragment.read)
36
+ keep_in_context = options.delete(:keep_in_context)
37
+ if (cached = fragment.read(keep_in_context))
33
38
  return cached == Fragment::NIL_IN_CACHE ? nil : raw_value(cached)
34
39
  end
35
40
 
@@ -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
 
@@ -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.1.0"
5
+ VERSION = "1.5.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.1.0
4
+ version: 1.5.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-26 00:00:00.000000000 Z
11
+ date: 2021-02-20 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: []