graphql-fragment_cache 1.0.2 → 1.2.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: 515ec358aecf867ce2d2ec5ac82003d802dc6c31d6402d62593a626ac5bad81e
4
- data.tar.gz: 8b1b3d82c0fb808a2cd826c710047635ba141895843ff7b804a301848d266ff5
3
+ metadata.gz: 7335be135632f708b99b20d71dc28b4711e0899c68a042f0b29950f560eeb543
4
+ data.tar.gz: 30674b1fff64a0457d5de9e690b8b219545702a68eaeab186f000b0f64733e34
5
5
  SHA512:
6
- metadata.gz: 2c2dd089bf31f6d2491fc7d904d8ca356a2dfbc2cace258fe1e3b49c18dc59a64805a1cb96655bfb23eacc62ed027198365491759005b232354a2ee8ed41e605
7
- data.tar.gz: 92309ef8b7ee62a5fd85007a88be932874f996d12ee08a1e81530d90fa3138dc631f90ad6a6f41c3952338001e10cefbeb1691bcea7891ba9b2f4027543e659d
6
+ metadata.gz: 1a4e4a3bcaf2ef3d51e3bd3cd399474e6e1b655ab8ad7583d79157db20f4fc466bc2d6f99796a4025c62a0249ef77d3471798f868e4a63a5948ff8ecb9d4f3ae
7
+ data.tar.gz: 9dec0c5bb94b0b08c09854098ac2bd8ae6cbfb8ebc005925b43b862e84b1a4542d168b2e3cb2ae13b69beca39ccfe403df322f59b0239d79c055a29375214fdc
@@ -2,6 +2,27 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 1.2.0 (2020-10-26)
6
+
7
+ - [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][])
8
+
9
+ ## 1.1.0 (2020-10-26)
10
+
11
+ - [PR#38](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/38) Support caching from other places than field or resolver ([@DmitryTsepelev][])
12
+
13
+ ## 1.0.5 (2020-10-13)
14
+
15
+ - [PR#35](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/35) Prefer using `#write_multi` on cache store when possible ([@DmitryTsepelev][])
16
+
17
+ ## 1.0.4 (2020-10-12)
18
+
19
+ - [PR#34](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/34) Avoid unneded default calculation in CacheKeyBuilder ([@DmitryTsepelev][])
20
+ - [PR#31](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/31) Do not patch Connection#wrap in graphql >= 1.10.5 ([@DmitryTsepelev][])
21
+
22
+ ## 1.0.3 (2020-08-31)
23
+
24
+ - [PR#29](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/29) Cache result JSON instead of connection objects ([@DmitryTsepelev][])
25
+
5
26
  ## 1.0.2 (2020-08-19)
6
27
 
7
28
  - [PR#28](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/28) Support #keys method for GraphQL::FragmentCache::MemoryStore instance ([@reabiliti][])
@@ -52,3 +73,4 @@
52
73
  [@palkan]: https://github.com/palkan
53
74
  [@ssnickolay]: https://github.com/ssnickolay
54
75
  [@reabiliti]: https://github.com/reabiliti
76
+ [@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
@@ -252,7 +260,7 @@ Rails.application.configure do |config|
252
260
  end
253
261
  ```
254
262
 
255
- ⚠️ Cache store must implement `#read(key)`, `#write(key, value, **options)` and `#exist?(key)` methods.
263
+ ⚠️ Cache store must implement `#read(key)`, `#exist?(key)` and `#write_multi(hash, **options)` or `#write(key, value, **options)` methods.
256
264
 
257
265
  The gem provides only in-memory store out-of-the-box (`GraphQL::FragmentCache::MemoryStore`). It's used by default.
258
266
 
@@ -275,6 +283,47 @@ class QueryType < BaseObject
275
283
  end
276
284
  ```
277
285
 
286
+ ## How to use `#cache_fragment` in extensions (and other places where context is not available)
287
+
288
+ 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:
289
+
290
+ ```ruby
291
+ class Types::QueryType < Types::BaseObject
292
+ class CurrentMomentExtension < GraphQL::Schema::FieldExtension
293
+ # turning on cache_fragment support
294
+ include GraphQL::FragmentCache::ObjectHelpers
295
+
296
+ def resolve(object:, arguments:, context:)
297
+ # context is passed explicitly
298
+ cache_fragment(context: context) do
299
+ result = yield(object, arguments)
300
+ "#{result} (at #{Time.now})"
301
+ end
302
+ end
303
+ end
304
+
305
+ field :event, String, null: false, extensions: [CurrentMomentExtension]
306
+
307
+ def event
308
+ "something happened"
309
+ end
310
+ end
311
+ ```
312
+
313
+ 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
+
315
+ ## Limitations
316
+
317
+ 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:
318
+
319
+ ```ruby
320
+ field :cached_avatar_url, String, null: false
321
+
322
+ def cached_avatar_url
323
+ cache_fragment(query_cache_key: "post_avatar_url(#{object.id})") { object.avatar_url }
324
+ end
325
+ ```
326
+
278
327
  ## Credits
279
328
 
280
329
  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
@@ -6,6 +6,8 @@ require "graphql/fragment_cache/ext/context_fragments"
6
6
  require "graphql/fragment_cache/ext/graphql_cache_key"
7
7
  require "graphql/fragment_cache/object"
8
8
 
9
+ require "graphql/fragment_cache/connections/patch"
10
+
9
11
  require "graphql/fragment_cache/schema/patch"
10
12
  require "graphql/fragment_cache/schema/tracer"
11
13
  require "graphql/fragment_cache/schema/instrumentation"
@@ -26,6 +28,8 @@ module GraphQL
26
28
  schema_defn.tracer(Schema::Tracer)
27
29
  schema_defn.instrument(:query, Schema::Instrumentation)
28
30
  schema_defn.extend(Schema::Patch)
31
+
32
+ GraphQL::Pagination::Connections.prepend(Connections::Patch)
29
33
  end
30
34
 
31
35
  def cache_store=(store)
@@ -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
@@ -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
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module FragmentCache
5
+ module Connections
6
+ # Patches GraphQL::Pagination::Connections to support raw values
7
+ module Patch
8
+ if Gem::Dependency.new("graphql", "< 1.11.0").match?("graphql", GraphQL::VERSION)
9
+ def wrap(field, object, arguments, context, *options)
10
+ raw_value?(object) ? object : super
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
+ end
17
+
18
+ private
19
+
20
+ def raw_value?(value)
21
+ GraphQL::Execution::Interpreter::RawValue === value
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -8,8 +8,6 @@ module GraphQL
8
8
  class Fragment
9
9
  attr_reader :options, :path, :context
10
10
 
11
- attr_accessor :resolved_value
12
-
13
11
  def initialize(context, **options)
14
12
  @context = context
15
13
  @options = options
@@ -24,25 +22,18 @@ module GraphQL
24
22
  end
25
23
  end
26
24
 
27
- def persist
28
- # Connections are not available from the runtime object, so
29
- # we rely on Schema::Tracer to save it for us
30
- value = resolved_value || resolve_from_runtime
31
- FragmentCache.cache_store.write(cache_key, value, **options)
32
- end
33
-
34
- private
35
-
36
25
  def cache_key
37
26
  @cache_key ||= CacheKeyBuilder.call(path: path, query: context.query, **options)
38
27
  end
39
28
 
40
- def interpreter_context
41
- context.namespace(:interpreter)
29
+ def value
30
+ final_value.dig(*path)
42
31
  end
43
32
 
44
- def resolve_from_runtime
45
- final_value.dig(*path)
33
+ private
34
+
35
+ def interpreter_context
36
+ context.namespace(:interpreter)
46
37
  end
47
38
 
48
39
  def final_value
@@ -10,41 +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
15
+
16
+ base.include(GraphQL::Execution::Interpreter::HandlesRawValue)
17
+ end
14
18
 
15
- def_delegator :field, :connection?
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
- return nil if cached == Fragment::NIL_IN_CACHE
26
- return restore_cached_value(cached)
33
+ return cached == Fragment::NIL_IN_CACHE ? nil : raw_value(cached)
27
34
  end
28
35
 
29
36
  (block_given? ? block.call : object_to_cache).tap do |resolved_value|
30
- context.fragments << fragment
37
+ context_to_use.fragments << fragment
31
38
  end
32
39
  end
33
-
34
- private
35
-
36
- def restore_cached_value(cached)
37
- # If we return connection object from resolver, Interpreter stops processing it
38
- connection? ? cached : raw_value(cached)
39
- end
40
-
41
- def field
42
- interpreter_context[:current_field]
43
- end
44
-
45
- def interpreter_context
46
- @interpreter_context ||= context.namespace(:interpreter)
47
- end
48
40
  end
49
41
  end
50
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
 
@@ -10,18 +10,15 @@ module GraphQL
10
10
  class << self
11
11
  def trace(key, data)
12
12
  yield.tap do |resolved_value|
13
- next unless connection_to_cache?(key, data)
13
+ next unless connection_field?(key, data)
14
14
 
15
- # We need to attach connection object to fragment and save it later
16
- context = data[:query].context
17
- verify_connections!(context)
18
- cache_connection(resolved_value, context)
15
+ verify_connections!(data[:query].context)
19
16
  end
20
17
  end
21
18
 
22
19
  private
23
20
 
24
- def connection_to_cache?(key, data)
21
+ def connection_field?(key, data)
25
22
  key == "execute_field" && data[:field].connection?
26
23
  end
27
24
 
@@ -31,12 +28,6 @@ module GraphQL
31
28
  raise StandardError,
32
29
  "GraphQL::Pagination::Connections should be enabled for connection caching"
33
30
  end
34
-
35
- def cache_connection(resolved_value, context)
36
- current_path = context.namespace(:interpreter)[:current_path]
37
- fragment = context.fragments.find { |fragment| fragment.path == current_path }
38
- fragment.resolved_value = resolved_value if fragment
39
- end
40
31
  end
41
32
  end
42
33
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module FragmentCache
5
- VERSION = "1.0.2"
5
+ VERSION = "1.2.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.2
4
+ version: 1.2.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-19 00:00:00.000000000 Z
11
+ date: 2020-11-12 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,12 +162,14 @@ 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
168
169
  - lib/graphql/fragment_cache.rb
169
170
  - lib/graphql/fragment_cache/cache_key_builder.rb
170
171
  - lib/graphql/fragment_cache/cacher.rb
172
+ - lib/graphql/fragment_cache/connections/patch.rb
171
173
  - lib/graphql/fragment_cache/ext/context_fragments.rb
172
174
  - lib/graphql/fragment_cache/ext/graphql_cache_key.rb
173
175
  - lib/graphql/fragment_cache/field_extension.rb