graphql-fragment_cache 1.3.0 → 1.6.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: 8454e3e398cc31081e2b17fb4a9b9e971cfbd1da05bce06149e419bb0e6b6b36
4
- data.tar.gz: f654d30606764307daad38b45921533edba92fa2cd95a75556488985bd78a590
3
+ metadata.gz: a5c6796b976bd78fc44c483dd9b39684081ee07bfb8b791d3a9bf00a31893ae7
4
+ data.tar.gz: fc772a7e8c4319bd5b5bff5978ad20293334ac872be2fed170bc9084326874a0
5
5
  SHA512:
6
- metadata.gz: 3a573a48cfe5b5d1beebd665c09b8adc4fe7d7a4bb08b29cc923c2434d921c0e9eb2c46b317d4d44367e46d0afafd2a686a0186cfb68f50459eaad2707f3da72
7
- data.tar.gz: 2db445bfbf4ef1cd56d715e586a6c0b748695e022ac55d94bc8bd52171221902fa562426c8a53cadb2e7d0ee7667dd866f27106fb3901247932e1b1a2e3320b6
6
+ metadata.gz: 597046420b6342ca1c7e1429c1e44d36fa3a0eca9592d194318213614f3dc95c10eadf5ea616a13e8582f354caed9bf425b639149b15a7d9c3647e970fd3a780
7
+ data.tar.gz: 1aa900bb9840ccb836498b79ef0031859304a328bebbce86fc32399e468d43ae1279daedc1bb6444e4366450ab5b11974dec679d4e11d8688fbea9f6bdf1c4a0
data/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 1.6.0 (2021-03-13)
6
+
7
+ - [PR#54](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/54) Include arguments in selections_cache_key ([@bbugh][])
8
+
9
+ ## 1.5.1 (2021-03-10)
10
+
11
+ - [PR#53](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/53) Use thread-safe query result for final_value ([@bbugh][])
12
+ - [PR#51](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/51) Do not cache fragments without final value ([@DmitryTsepelev][])
13
+
14
+ ## 1.5.0 (2021-02-20)
15
+
16
+ - [PR#50](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/50) Add object_cache_key to CacheKeyBuilder ([@bbugh][])
17
+
18
+ ## 1.4.1 (2021-01-21)
19
+
20
+ - [PR#48](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/48) Support graphql-ruby 1.12 ([@DmitryTsepelev][])
21
+
22
+ ## 1.4.0 (2020-12-03)
23
+
24
+ - [PR#41](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/41) Add `keep_in_context` option ([@DmitryTsepelev][])
25
+
5
26
  ## 1.3.0 (2020-11-25)
6
27
 
7
28
  - [PR#39](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/39) Implement `path_cache_key` option ([@DmitryTsepelev][])
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`, `query_cache_key` or `path_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
@@ -140,6 +140,21 @@ class PostType < BaseObject
140
140
  end
141
141
  ```
142
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
+
143
158
  ### User-provided cache key
144
159
 
145
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):
@@ -198,6 +213,7 @@ end
198
213
 
199
214
  The way cache key part is generated for the passed argument is the following:
200
215
 
216
+ - Use `object_cache_key: "some_cache_key"` if passed to `cache_fragment`
201
217
  - Use `#graphql_cache_key` if implemented.
202
218
  - Use `#cache_key` (or `#cache_key_with_version` for modern Rails) if implemented.
203
219
  - Use `self.to_s` for _primitive_ types (strings, symbols, numbers, booleans).
@@ -314,6 +330,24 @@ end
314
330
 
315
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.
316
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
+
317
351
  ## Limitations
318
352
 
319
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:
@@ -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
@@ -113,8 +127,13 @@ module GraphQL
113
127
 
114
128
  def build
115
129
  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)}"
130
+ if @options[:object_cache_key]
131
+ "#{base_key}/#{@options[:object_cache_key]}"
132
+ elsif object
133
+ "#{base_key}/#{object_key(object)}"
134
+ else
135
+ base_key
136
+ end
118
137
  end
119
138
  end
120
139
 
@@ -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
@@ -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
@@ -113,8 +127,13 @@ module GraphQL
113
127
 
114
128
  def build
115
129
  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)}"
130
+ if @options[:object_cache_key]
131
+ "#{base_key}/#{@options[:object_cache_key]}"
132
+ elsif object
133
+ "#{base_key}/#{object_key(object)}"
134
+ else
135
+ base_key
136
+ end
118
137
  end
119
138
  end
120
139
 
@@ -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
@@ -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,28 +18,46 @@ 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
26
28
  @cache_key ||= CacheKeyBuilder.call(path: path, query: context.query, **options)
27
29
  end
28
30
 
31
+ def with_final_value?
32
+ !final_value.nil?
33
+ end
34
+
29
35
  def value
30
36
  final_value.dig(*path)
31
37
  end
32
38
 
33
39
  private
34
40
 
41
+ def read_from_context
42
+ if (loaded_value = context.loaded_fragments[cache_key])
43
+ return loaded_value
44
+ end
45
+
46
+ yield.tap { |value| context.loaded_fragments[cache_key] = value }
47
+ end
48
+
49
+ def value_from_cache
50
+ FragmentCache.cache_store.read(cache_key).tap do |cached|
51
+ return NIL_IN_CACHE if cached.nil? && FragmentCache.cache_store.exist?(cache_key)
52
+ end
53
+ end
54
+
35
55
  def interpreter_context
36
56
  context.namespace(:interpreter)
37
57
  end
38
58
 
39
59
  def final_value
40
- @final_value ||= interpreter_context[:runtime].final_value
60
+ @final_value ||= context.query.result["data"]
41
61
  end
42
62
  end
43
63
  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
 
@@ -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.3.0"
5
+ VERSION = "1.6.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.3.0
4
+ version: 1.6.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-11-25 00:00:00.000000000 Z
11
+ date: 2021-03-13 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
@@ -190,7 +204,7 @@ metadata:
190
204
  homepage_uri: https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache
191
205
  source_code_uri: https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache
192
206
  changelog_uri: https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/CHANGELOG.md
193
- post_install_message:
207
+ post_install_message:
194
208
  rdoc_options: []
195
209
  require_paths:
196
210
  - lib
@@ -205,8 +219,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
205
219
  - !ruby/object:Gem::Version
206
220
  version: '0'
207
221
  requirements: []
208
- rubygems_version: 3.0.3
209
- signing_key:
222
+ rubygems_version: 3.1.2
223
+ signing_key:
210
224
  specification_version: 4
211
225
  summary: Fragment cache for graphql-ruby
212
226
  test_files: []