graphql-fragment_cache 1.1.0 → 1.5.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: 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: []