graphql-fragment_cache 1.0.2 → 1.2.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: 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