graphql-fragment_cache 1.3.0 → 1.6.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: 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: []