graphql-fragment_cache 1.0.3 → 1.3.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: 8fd575d2efddb52ef6d0f878b40823baae40cedec5aa683ba5f6a1b39b98bffe
4
- data.tar.gz: eba1c06d3650d395575bd96927f831fc206f797b47afa9ebfcfff9adbf2ecf1d
3
+ metadata.gz: 8454e3e398cc31081e2b17fb4a9b9e971cfbd1da05bce06149e419bb0e6b6b36
4
+ data.tar.gz: f654d30606764307daad38b45921533edba92fa2cd95a75556488985bd78a590
5
5
  SHA512:
6
- metadata.gz: 5fe016ab01f8b8076b1cc6680f8a09ad9bf1ab9d7f5c798a308465fd0d6d80dd6da24ef5855aa35660d9b8cce0529ab59cb6a45b7b971c58d4f8a9e761b80ff9
7
- data.tar.gz: 0430b991c22524be88e3741f700d5c02ac65186f4cf40e7f06440b737c345bba8e44748a9e8d6ef1293356e21ee7560595b5e1563c28da7bb5f88008f7174360
6
+ metadata.gz: 3a573a48cfe5b5d1beebd665c09b8adc4fe7d7a4bb08b29cc923c2434d921c0e9eb2c46b317d4d44367e46d0afafd2a686a0186cfb68f50459eaad2707f3da72
7
+ data.tar.gz: 2db445bfbf4ef1cd56d715e586a6c0b748695e022ac55d94bc8bd52171221902fa562426c8a53cadb2e7d0ee7667dd866f27106fb3901247932e1b1a2e3320b6
@@ -2,6 +2,27 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 1.3.0 (2020-11-25)
6
+
7
+ - [PR#39](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/39) Implement `path_cache_key` option ([@DmitryTsepelev][])
8
+
9
+ ## 1.2.0 (2020-10-26)
10
+
11
+ - [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][])
12
+
13
+ ## 1.1.0 (2020-10-26)
14
+
15
+ - [PR#38](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/38) Support caching from other places than field or resolver ([@DmitryTsepelev][])
16
+
17
+ ## 1.0.5 (2020-10-13)
18
+
19
+ - [PR#35](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/35) Prefer using `#write_multi` on cache store when possible ([@DmitryTsepelev][])
20
+
21
+ ## 1.0.4 (2020-10-12)
22
+
23
+ - [PR#34](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/34) Avoid unneded default calculation in CacheKeyBuilder ([@DmitryTsepelev][])
24
+ - [PR#31](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/31) Do not patch Connection#wrap in graphql >= 1.10.5 ([@DmitryTsepelev][])
25
+
5
26
  ## 1.0.3 (2020-08-31)
6
27
 
7
28
  - [PR#29](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/29) Cache result JSON instead of connection objects ([@DmitryTsepelev][])
@@ -56,3 +77,4 @@
56
77
  [@palkan]: https://github.com/palkan
57
78
  [@ssnickolay]: https://github.com/ssnickolay
58
79
  [@reabiliti]: https://github.com/reabiliti
80
+ [@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,47 @@ 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
+ ## Limitations
318
+
319
+ 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:
320
+
321
+ ```ruby
322
+ field :cached_avatar_url, String, null: false
323
+
324
+ def cached_avatar_url
325
+ cache_fragment(query_cache_key: "post_avatar_url(#{object.id})") { object.avatar_url }
326
+ end
327
+ ```
328
+
278
329
  ## Credits
279
330
 
280
331
  Based on the original [gist](https://gist.github.com/palkan/faad9f6ff1db16fcdb1c071ec50e4190) by [@palkan](https://github.com/palkan) and [@ssnickolay](https://github.com/ssnickolay).
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ using RubyNext
4
+
5
+ module GraphQL
6
+ module FragmentCache
7
+ # Memory adapter for storing cached fragments
8
+ class MemoryStore
9
+ using RubyNext
10
+
11
+ class Entry < Struct.new(:value, :expires_at, keyword_init: true)
12
+ def expired?
13
+ expires_at && expires_at < Time.now
14
+ end
15
+ end
16
+
17
+ attr_reader :default_expires_in
18
+
19
+ def initialize(expires_in: nil, **other)
20
+ raise ArgumentError, "Unsupported options: #{other.keys.join(",")}" unless other.empty?
21
+
22
+ @default_expires_in = expires_in
23
+ @storage = {}
24
+ end
25
+
26
+ def keys
27
+ storage.keys
28
+ end
29
+
30
+ def exist?(key)
31
+ storage.key?(key)
32
+ end
33
+
34
+ def read(key)
35
+ key = key.to_s
36
+ ((!storage[key].nil?) || nil) && storage[key].then do |entry|
37
+ if entry.expired?
38
+ delete(key)
39
+ next
40
+ end
41
+ entry.value
42
+ end
43
+ end
44
+
45
+ def write(key, value, expires_in: default_expires_in, **options)
46
+ key = key.to_s
47
+ @storage[key] = Entry.new(value: value, expires_at: expires_in ? Time.now + expires_in : nil)
48
+ end
49
+
50
+ def delete(key)
51
+ key = key.to_s
52
+ storage.delete(key)
53
+ end
54
+
55
+ def clear
56
+ storage.clear
57
+ end
58
+
59
+ private
60
+
61
+ attr_reader :storage
62
+ end
63
+ end
64
+ end
@@ -121,11 +121,11 @@ module GraphQL
121
121
  private
122
122
 
123
123
  def schema_cache_key
124
- @options.fetch(:schema_cache_key, schema.schema_cache_key)
124
+ @options.fetch(:schema_cache_key) { schema.schema_cache_key }
125
125
  end
126
126
 
127
127
  def query_cache_key
128
- @options.fetch(:query_cache_key, "#{path_cache_key}[#{selections_cache_key}]")
128
+ @options.fetch(:query_cache_key) { "#{path_cache_key}[#{selections_cache_key}]" }
129
129
  end
130
130
 
131
131
  def selections_cache_key
@@ -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)
@@ -121,11 +121,11 @@ module GraphQL
121
121
  private
122
122
 
123
123
  def schema_cache_key
124
- @options.fetch(:schema_cache_key, schema.schema_cache_key)
124
+ @options.fetch(:schema_cache_key) { schema.schema_cache_key }
125
125
  end
126
126
 
127
127
  def query_cache_key
128
- @options.fetch(:query_cache_key, "#{path_cache_key}[#{selections_cache_key}]")
128
+ @options.fetch(:query_cache_key) { "#{path_cache_key}[#{selections_cache_key}]" }
129
129
  end
130
130
 
131
131
  def selections_cache_key
@@ -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
@@ -5,14 +5,14 @@ module GraphQL
5
5
  module Connections
6
6
  # Patches GraphQL::Pagination::Connections to support raw values
7
7
  module Patch
8
- if Gem::Dependency.new("graphql", ">= 1.11.0").match?("graphql", GraphQL::VERSION)
9
- def wrap(field, parent, items, arguments, context, *options)
10
- raw_value?(items) ? items : super
11
- end
12
- else
8
+ if Gem::Dependency.new("graphql", "< 1.11.0").match?("graphql", GraphQL::VERSION)
13
9
  def wrap(field, object, arguments, context, *options)
14
10
  raw_value?(object) ? object : super
15
11
  end
12
+ elsif Gem::Dependency.new("graphql", "< 1.11.5").match?("graphql", GraphQL::VERSION)
13
+ def wrap(field, parent, items, arguments, context, *options)
14
+ raw_value?(items) ? items : super
15
+ end
16
16
  end
17
17
 
18
18
  private
@@ -22,17 +22,16 @@ module GraphQL
22
22
  end
23
23
  end
24
24
 
25
- def persist
26
- value = final_value.dig(*path)
27
- FragmentCache.cache_store.write(cache_key, value, **options)
28
- end
29
-
30
- private
31
-
32
25
  def cache_key
33
26
  @cache_key ||= CacheKeyBuilder.call(path: path, query: context.query, **options)
34
27
  end
35
28
 
29
+ def value
30
+ final_value.dig(*path)
31
+ end
32
+
33
+ private
34
+
36
35
  def interpreter_context
37
36
  context.namespace(:interpreter)
38
37
  end
@@ -10,35 +10,33 @@ module GraphQL
10
10
  module ObjectHelpers
11
11
  extend Forwardable
12
12
 
13
- NO_OBJECT = Object.new
13
+ def self.included(base)
14
+ return if base < GraphQL::Execution::Interpreter::HandlesRawValue
14
15
 
15
- def_delegator :field, :connection?
16
+ base.include(GraphQL::Execution::Interpreter::HandlesRawValue)
17
+ end
18
+
19
+ NO_OBJECT = Object.new
16
20
 
17
21
  def cache_fragment(object_to_cache = NO_OBJECT, **options, &block)
18
22
  raise ArgumentError, "Block or argument must be provided" unless block_given? || object_to_cache != NO_OBJECT
19
23
 
20
24
  options[:object] = object_to_cache if object_to_cache != NO_OBJECT
21
25
 
22
- 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)
23
31
 
24
32
  if (cached = fragment.read)
25
33
  return cached == Fragment::NIL_IN_CACHE ? nil : raw_value(cached)
26
34
  end
27
35
 
28
36
  (block_given? ? block.call : object_to_cache).tap do |resolved_value|
29
- context.fragments << fragment
37
+ context_to_use.fragments << fragment
30
38
  end
31
39
  end
32
-
33
- private
34
-
35
- def field
36
- interpreter_context[:current_field]
37
- end
38
-
39
- def interpreter_context
40
- @interpreter_context ||= context.namespace(:interpreter)
41
- end
42
40
  end
43
41
  end
44
42
  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.3"
5
+ VERSION = "1.3.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.3
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - DmitryTsepelev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-08-31 00:00:00.000000000 Z
11
+ date: 2020-11-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: graphql
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 0.7.0
33
+ version: 0.10.0
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: 0.7.0
40
+ version: 0.10.0
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: combustion
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -128,28 +128,28 @@ dependencies:
128
128
  requirements:
129
129
  - - ">="
130
130
  - !ruby/object:Gem::Version
131
- version: '0.6'
131
+ version: '0.10'
132
132
  type: :development
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
136
  - - ">="
137
137
  - !ruby/object:Gem::Version
138
- version: '0.6'
138
+ version: '0.10'
139
139
  - !ruby/object:Gem::Dependency
140
- name: ruby-next-parser
140
+ name: unparser
141
141
  requirement: !ruby/object:Gem::Requirement
142
142
  requirements:
143
143
  - - '='
144
144
  - !ruby/object:Gem::Version
145
- version: 2.8.0.7
145
+ version: 0.4.9
146
146
  type: :development
147
147
  prerelease: false
148
148
  version_requirements: !ruby/object:Gem::Requirement
149
149
  requirements:
150
150
  - - '='
151
151
  - !ruby/object:Gem::Version
152
- version: 2.8.0.7
152
+ version: 0.4.9
153
153
  description: Fragment cache for graphql-ruby
154
154
  email:
155
155
  - dmitry.a.tsepelev@gmail.com
@@ -162,6 +162,7 @@ files:
162
162
  - README.md
163
163
  - bin/console
164
164
  - bin/setup
165
+ - lib/.rbnext/2.3/graphql/fragment_cache/memory_store.rb
165
166
  - lib/.rbnext/2.7/graphql/fragment_cache/cache_key_builder.rb
166
167
  - lib/.rbnext/2.7/graphql/fragment_cache/ext/graphql_cache_key.rb
167
168
  - lib/graphql-fragment_cache.rb