graphql-fragment_cache 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 385b7910c87bd9fe82d32d4d1e6ac45c9d62600b2cbcc1422445c47fca70d021
4
+ data.tar.gz: a99f8ef1ffaa3026ee2f183b74abc1dea8fab099f0a2c811de1f211adf4e176d
5
+ SHA512:
6
+ metadata.gz: 3e1936a4c8d932cf022ffaa45c79d67b3217729f7b722f3209995706a9b1c6c4346c272078c1c9404707a1833b0ef786399bde0b76431eedead4ed0cb7cfcfa0
7
+ data.tar.gz: 2a550ee8ba377ec9d199266fb75bdd9fe51bb46595c8839d5d0a9a43f9c3874b897d73b718fb66b4f2891721dbac56a436d05f66ad197f5ca31a973f6c592b41
@@ -0,0 +1,11 @@
1
+ # Change log
2
+
3
+ ## master
4
+
5
+ ## 0.1.0 (2020-04-14)
6
+
7
+ - Initial version ([@DmitryTsepelev][], [@palkan][], [@ssnickolay][])
8
+
9
+ [@DmitryTsepelev]: https://github.com/DmitryTsepelev
10
+ [@palkan]: https://github.com/palkan
11
+ [@ssnickolay]: https://github.com/ssnickolay
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 DmitryTsepelev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,283 @@
1
+ # GraphQL::FragmentCache ![CI](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/workflows/CI/badge.svg?branch=master)
2
+
3
+ `GraphQL::FragmentCache` powers up [graphql-ruby](https://graphql-ruby.org) with the ability to cache response _fragments_: you can mark any field as cached and it will never be resolved again (at least, while cache is valid). For instance, the following code caches `title` for each post:
4
+
5
+ ```ruby
6
+ class PostType < BaseObject
7
+ field :id, ID, null: false
8
+ field :title, String, null: false, cache_fragment: true
9
+ end
10
+ ```
11
+
12
+ <p align="center">
13
+ <a href="https://evilmartians.com/?utm_source=graphql-ruby-fragment_cache">
14
+ <img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54">
15
+ </a>
16
+ </p>
17
+
18
+ ## Getting started
19
+
20
+ Add the gem to your Gemfile `gem 'graphql-fragment_cache'` and add the plugin to your schema class (make sure to turn interpreter mode on with AST analysis!):
21
+
22
+ ```ruby
23
+ class GraphqSchema < GraphQL::Schema
24
+ use GraphQL::Execution::Interpreter
25
+ use GraphQL::Analysis::AST
26
+
27
+ use GraphQL::FragmentCache
28
+
29
+ query QueryType
30
+ end
31
+ ```
32
+
33
+ Include `GraphQL::FragmentCache::Object` to your base type class:
34
+
35
+ ```ruby
36
+ class BaseType < GraphQL::Schema::Object
37
+ include GraphQL::FragmentCache::Object
38
+ end
39
+ ```
40
+
41
+ Now you can add `cache_fragment:` option to your fields to turn caching on:
42
+
43
+ ```ruby
44
+ class PostType < BaseObject
45
+ field :id, ID, null: false
46
+ field :title, String, null: false, cache_fragment: true
47
+ end
48
+ ```
49
+
50
+ Alternatively, you can use `cache_fragment` method inside resolvers:
51
+
52
+ ```ruby
53
+ class QueryType < BaseObject
54
+ field :post, PostType, null: true do
55
+ argument :id, ID, required: true
56
+ end
57
+
58
+ def post(id:)
59
+ cache_fragment { Post.find(id) }
60
+ end
61
+ end
62
+ ```
63
+
64
+ ## Cache key generation
65
+
66
+ Cache keys consist of implicit and explicit (provided by user) parts.
67
+
68
+ ### Implicit cache key
69
+
70
+ Implicit part of a cache key (its prefix) contains the information about the schema and the current query. It includes:
71
+
72
+ - Hex gsdigest of the schema definition (to make sure cache is cleared when the schema changes).
73
+ - The current query fingerprint consisting of a _path_ to the field, arguments information and the selections set.
74
+
75
+ Let's take a look at the example:
76
+
77
+ ```ruby
78
+ query = <<~GQL
79
+ query {
80
+ post(id: 1) {
81
+ id
82
+ title
83
+ cachedAuthor {
84
+ id
85
+ name
86
+ }
87
+ }
88
+ }
89
+ GQL
90
+
91
+ schema_cache_key = GraphqSchema.schema_cache_key
92
+
93
+ path_cache_key = "post(id:1)/cachedAuthor"
94
+ selections_cache_key = "[#{%w[id name].join(".")}]"
95
+
96
+ query_cache_key = Digest::SHA1.hexdigest("#{path_cache_key}#{selections_cache_key}")
97
+
98
+ cache_key = "#{schema_cache_key}/#{query_cache_key}"
99
+ ```
100
+
101
+ You can override `schema_cache_key` or `query_cache_key` by passing parameters to the `cache_fragment` calls:
102
+
103
+ ```ruby
104
+ class QueryType < BaseObject
105
+ field :post, PostType, null: true do
106
+ argument :id, ID, required: true
107
+ end
108
+
109
+ def post(id:)
110
+ cache_fragment(query_cache_key: "post(#{id})") { Post.find(id) }
111
+ end
112
+ end
113
+ ```
114
+
115
+ Same for the option:
116
+
117
+ ```ruby
118
+ class PostType < BaseObject
119
+ field :id, ID, null: false
120
+ field :title, String, null: false, cache_fragment: {query_cache_key: "post_title"}
121
+ end
122
+ ```
123
+
124
+ ### User-provided cache key
125
+
126
+ 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):
127
+
128
+ ```ruby
129
+ def post(id:)
130
+ post = Post.find(id)
131
+ cache_fragment(post) { post }
132
+ end
133
+ ```
134
+
135
+ You can pass arrays as well to build a compound cache key:
136
+
137
+ ```ruby
138
+ def post(id:)
139
+ post = Post.find(id)
140
+ cache_fragment([post, current_account]) { post }
141
+ end
142
+ ```
143
+
144
+ You can omit the block if its return value is the same as the cached object:
145
+
146
+ ```ruby
147
+ # the following line
148
+ cache_fragment(post)
149
+ # is the same as
150
+ cache_fragment(post) { post }
151
+ ```
152
+
153
+ When using `cache_fragment:` option, it's only possible to use the resolved value as a cache key by setting:
154
+
155
+ ```ruby
156
+ field :post, PostType, null: true, cache_fragment: {cache_key: :object} do
157
+ argument :id, ID, required: true
158
+ end
159
+
160
+ # this is equal to
161
+ def post(id:)
162
+ cache_fragment(Post.find(id))
163
+ end
164
+ ```
165
+
166
+ Also, you can pass `:value` to the `cache_key:` argument to use the returned value to build a key:
167
+
168
+ ```ruby
169
+ field :post, PostType, null: true, cache_fragment: {cache_key: :value} do
170
+ argument :id, ID, required: true
171
+ end
172
+
173
+ # this is equal to
174
+ def post(id:)
175
+ post = Post.find(id)
176
+ cache_fragment(post) { post }
177
+ end
178
+ ```
179
+
180
+ The way cache key part is generated for the passed argument is the following:
181
+
182
+ - Use `#graphql_cache_key` if implemented.
183
+ - Use `#cache_key` (or `#cache_key_with_version` for modern Rails) if implemented.
184
+ - Use `self.to_s` for _primitive_ types (strings, symbols, numbers, booleans).
185
+ - Raise `ArgumentError` if none of the above.
186
+
187
+ ### Context cache key
188
+
189
+ By default, we do not take context into account when calculating cache keys. That's because caching is more efficient when it's _context-free_.
190
+
191
+ However, if you want some fields to be cached per context, you can do that either by passing context objects directly to the `#cache_fragment` method (see above) or by adding a `context_key` option to `cache_fragment:`.
192
+
193
+ For instance, imagine a query that allows the current user's social profiles:
194
+
195
+ ```gql
196
+ query {
197
+ socialProfiles {
198
+ provider
199
+ id
200
+ }
201
+ }
202
+ ```
203
+
204
+ You can cache the result using the context (`context[:user]`) as a cache key:
205
+
206
+ ```ruby
207
+ class QueryType < BaseObject
208
+ field :social_profiles, [SocialProfileType], null: false, cache_fragment: {context_key: :user}
209
+
210
+ def social_profiles
211
+ context[:user].social_profiles
212
+ end
213
+ end
214
+ ```
215
+
216
+ This is equal to using `#cache_fragment` the following way:
217
+
218
+ ```ruby
219
+ class QueryType < BaseObject
220
+ field :social_profiles, [SocialProfileType], null: false
221
+
222
+ def social_profiles
223
+ cache_fragment(context[:user]) { context[:user].social_profiles }
224
+ end
225
+ end
226
+ ```
227
+
228
+ ## Cache storage and options
229
+
230
+ It's up to your to decide which caching engine to use, all you need is to configure the cache store:
231
+
232
+ ```ruby
233
+ GraphQL::FragmentCache.cache_store = MyCacheStore.new
234
+ ```
235
+
236
+ Or, in Rails:
237
+
238
+ ```ruby
239
+ # config/application.rb (or config/environments/<environment>.rb)
240
+ Rails.application.configure do |config|
241
+ # arguments and options are the same as for `config.cache_store`
242
+ config.graphql_fragment_cache.store = :redis_cache_store
243
+ end
244
+ ```
245
+
246
+ ⚠️ Cache store must implement `#read(key)` and `#write(key, value, **options)` methods.
247
+
248
+ The gem provides only in-memory store out-of-the-box (`GraphQL::FragmentCache::MemoryStore`). It's used by default.
249
+
250
+ You can pass store-specific options to `#cache_fragment` or `cache_fragment:`. For example, to set expiration (assuming the store's `#write` method supports `expires_in` option):
251
+
252
+ ```ruby
253
+ class PostType < BaseObject
254
+ field :id, ID, null: false
255
+ field :title, String, null: false, cache_fragment: {expires_in: 5.minutes}
256
+ end
257
+
258
+ class QueryType < BaseObject
259
+ field :post, PostType, null: true do
260
+ argument :id, ID, required: true
261
+ end
262
+
263
+ def post(id:)
264
+ cache_fragment(expires_in: 5.minutes) { Post.find(id) }
265
+ end
266
+ end
267
+ ```
268
+
269
+ ## Limitations
270
+
271
+ - [Field aliases](https://spec.graphql.org/June2018/#sec-Field-Alias) are not currently supported (take a look at the failing spec [here](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/pull/7))
272
+
273
+ ## Credits
274
+
275
+ Based on the original [gist](https://gist.github.com/palkan/faad9f6ff1db16fcdb1c071ec50e4190) by [@palkan](https://github.com/palkan) and [@ssnickolay](https://github.com/ssnickolay).
276
+
277
+ ## Contributing
278
+
279
+ Bug reports and pull requests are welcome on GitHub at [https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache](https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache).
280
+
281
+ ## License
282
+
283
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "graphql/ruby/fragment_cache"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "digest"
5
+
6
+ using RubyNext
7
+
8
+ module GraphQL
9
+ module FragmentCache
10
+ using Ext
11
+
12
+ using(Module.new {
13
+ refine Array do
14
+ def to_selections_key
15
+ map { |val|
16
+ children = val.selections.empty? ? "" : "[#{val.selections.to_selections_key}]"
17
+ "#{val.field.name}#{children}"
18
+ }.join(".")
19
+ end
20
+ end
21
+ })
22
+
23
+ # Builds cache key for fragment
24
+ class CacheKeyBuilder
25
+ using RubyNext
26
+
27
+ class << self
28
+ def call(**options)
29
+ new(**options).build
30
+ end
31
+ end
32
+
33
+ attr_reader :query, :path, :object, :schema
34
+
35
+ def initialize(object: nil, query:, path:, **options)
36
+ @object = object
37
+ @query = query
38
+ @schema = query.schema
39
+ @path = path
40
+ @options = options
41
+ end
42
+
43
+ def build
44
+ Digest::SHA1.hexdigest("#{schema_cache_key}/#{query_cache_key}").then do |base_key|
45
+ next base_key unless object
46
+ "#{base_key}/#{object_key(object)}"
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def schema_cache_key
53
+ @options.fetch(:schema_cache_key, schema.schema_cache_key)
54
+ end
55
+
56
+ def query_cache_key
57
+ @options.fetch(:query_cache_key, "#{path_cache_key}[#{selections_cache_key}]")
58
+ end
59
+
60
+ def selections_cache_key
61
+ current_root =
62
+ path.reduce(query.lookahead) { |lkhd, name| lkhd.selection(name) }
63
+
64
+ current_root.selections.to_selections_key
65
+ end
66
+
67
+ def path_cache_key
68
+ lookahead = query.lookahead
69
+
70
+ path.map { |field_name|
71
+ lookahead = lookahead.selection(field_name)
72
+
73
+ next field_name if lookahead.arguments.empty?
74
+
75
+ args = lookahead.arguments.map { |_1, _2| "#{_1}:#{_2}" }.sort.join(",")
76
+ "#{field_name}(#{args})"
77
+ }.join("/")
78
+ end
79
+
80
+ def object_key(obj)
81
+ obj._graphql_cache_key
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module FragmentCache
5
+ using Ext
6
+
7
+ # Saves resolved fragment values to cache store
8
+ module Cacher
9
+ class << self
10
+ def call(query)
11
+ return unless query.context.fragments?
12
+
13
+ final_value = query.context.namespace(:interpreter)[:runtime].final_value
14
+
15
+ query.context.fragments.each { |_1| _1.persist(final_value) }
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module FragmentCache
5
+ module Ext
6
+ # Adds #_graphql_cache_key method to Object,
7
+ # which just call #graphql_cache_key or #cache_key.
8
+ #
9
+ # For other core classes returns string representation.
10
+ #
11
+ # Raises ArgumentError otherwise.
12
+ #
13
+ # We use a refinement to avoid case/if statements for type checking
14
+ refine Object do
15
+ def _graphql_cache_key
16
+ return graphql_cache_key if respond_to?(:graphql_cache_key)
17
+ return cache_key if respond_to?(:cache_key)
18
+ return to_a._graphql_cache_key if respond_to?(:to_a)
19
+
20
+ to_s
21
+ end
22
+ end
23
+
24
+ refine Array do
25
+ def _graphql_cache_key
26
+ map { |_1| _1._graphql_cache_key }.join("/")
27
+ end
28
+ end
29
+
30
+ refine NilClass do
31
+ def _graphql_cache_key
32
+ ""
33
+ end
34
+ end
35
+
36
+ refine TrueClass do
37
+ def _graphql_cache_key
38
+ "t"
39
+ end
40
+ end
41
+
42
+ refine FalseClass do
43
+ def _graphql_cache_key
44
+ "f"
45
+ end
46
+ end
47
+
48
+ refine String do
49
+ def _graphql_cache_key
50
+ self
51
+ end
52
+ end
53
+
54
+ refine Symbol do
55
+ def _graphql_cache_key
56
+ to_s
57
+ end
58
+ end
59
+
60
+ if RUBY_PLATFORM.match?(/java/i)
61
+ refine Integer do
62
+ def _graphql_cache_key
63
+ to_s
64
+ end
65
+ end
66
+
67
+ refine Float do
68
+ def _graphql_cache_key
69
+ to_s
70
+ end
71
+ end
72
+ else
73
+ refine Numeric do
74
+ def _graphql_cache_key
75
+ to_s
76
+ end
77
+ end
78
+ end
79
+
80
+ refine Time do
81
+ def _graphql_cache_key
82
+ to_s
83
+ end
84
+ end
85
+
86
+ refine Module do
87
+ def _graphql_cache_key
88
+ name
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,6 @@
1
+ require "ruby-next"
2
+
3
+ require "ruby-next/language/setup"
4
+ RubyNext::Language.setup_gem_load_path
5
+
6
+ require "graphql/fragment_cache"
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "graphql"
4
+
5
+ require "graphql/fragment_cache/ext/context_fragments"
6
+ require "graphql/fragment_cache/ext/graphql_cache_key"
7
+
8
+ require "graphql/fragment_cache/schema_patch"
9
+ require "graphql/fragment_cache/object"
10
+ require "graphql/fragment_cache/instrumentation"
11
+
12
+ require "graphql/fragment_cache/memory_store"
13
+
14
+ require "graphql/fragment_cache/version"
15
+ require "graphql/fragment_cache/railtie" if defined?(Rails::Railtie)
16
+
17
+ module GraphQL
18
+ # Plugin definition
19
+ module FragmentCache
20
+ class << self
21
+ attr_reader :cache_store
22
+
23
+ def use(schema_defn, options = {})
24
+ verify_interpreter!(schema_defn)
25
+
26
+ schema_defn.instrument(:query, Instrumentation)
27
+ schema_defn.extend(SchemaPatch)
28
+ end
29
+
30
+ def cache_store=(store)
31
+ unless store.respond_to?(:read)
32
+ raise ArgumentError, "Store must implement #read(key) method"
33
+ end
34
+
35
+ unless store.respond_to?(:write)
36
+ raise ArgumentError, "Store must implement #write(key, val, **options) method"
37
+ end
38
+
39
+ @cache_store = store
40
+ end
41
+
42
+ private
43
+
44
+ def verify_interpreter!(schema_defn)
45
+ unless schema_defn.interpreter?
46
+ raise StandardError,
47
+ "GraphQL::Execution::Interpreter should be enabled for fragment caching"
48
+ end
49
+
50
+ unless schema_defn.analysis_engine == GraphQL::Analysis::AST
51
+ raise StandardError,
52
+ "GraphQL::Analysis::AST should be enabled for fragment caching"
53
+ end
54
+ end
55
+ end
56
+
57
+ self.cache_store = MemoryStore.new
58
+ end
59
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "digest"
5
+
6
+ using RubyNext
7
+
8
+ module GraphQL
9
+ module FragmentCache
10
+ using Ext
11
+
12
+ using(Module.new {
13
+ refine Array do
14
+ def to_selections_key
15
+ map { |val|
16
+ children = val.selections.empty? ? "" : "[#{val.selections.to_selections_key}]"
17
+ "#{val.field.name}#{children}"
18
+ }.join(".")
19
+ end
20
+ end
21
+ })
22
+
23
+ # Builds cache key for fragment
24
+ class CacheKeyBuilder
25
+ using RubyNext
26
+
27
+ class << self
28
+ def call(**options)
29
+ new(**options).build
30
+ end
31
+ end
32
+
33
+ attr_reader :query, :path, :object, :schema
34
+
35
+ def initialize(object: nil, query:, path:, **options)
36
+ @object = object
37
+ @query = query
38
+ @schema = query.schema
39
+ @path = path
40
+ @options = options
41
+ end
42
+
43
+ def build
44
+ Digest::SHA1.hexdigest("#{schema_cache_key}/#{query_cache_key}").then do |base_key|
45
+ next base_key unless object
46
+ "#{base_key}/#{object_key(object)}"
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def schema_cache_key
53
+ @options.fetch(:schema_cache_key, schema.schema_cache_key)
54
+ end
55
+
56
+ def query_cache_key
57
+ @options.fetch(:query_cache_key, "#{path_cache_key}[#{selections_cache_key}]")
58
+ end
59
+
60
+ def selections_cache_key
61
+ current_root =
62
+ path.reduce(query.lookahead) { |lkhd, name| lkhd.selection(name) }
63
+
64
+ current_root.selections.to_selections_key
65
+ end
66
+
67
+ def path_cache_key
68
+ lookahead = query.lookahead
69
+
70
+ path.map { |field_name|
71
+ lookahead = lookahead.selection(field_name)
72
+
73
+ next field_name if lookahead.arguments.empty?
74
+
75
+ args = lookahead.arguments.map { "#{_1}:#{_2}" }.sort.join(",")
76
+ "#{field_name}(#{args})"
77
+ }.join("/")
78
+ end
79
+
80
+ def object_key(obj)
81
+ obj._graphql_cache_key
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module FragmentCache
5
+ using Ext
6
+
7
+ # Saves resolved fragment values to cache store
8
+ module Cacher
9
+ class << self
10
+ def call(query)
11
+ return unless query.context.fragments?
12
+
13
+ final_value = query.context.namespace(:interpreter)[:runtime].final_value
14
+
15
+ query.context.fragments.each { _1.persist(final_value) }
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module FragmentCache
5
+ module Ext
6
+ # Add ability to access fragments via `context.fragments`
7
+ # without dupclicating the storage logic and monkey-patching
8
+ refine GraphQL::Query::Context do
9
+ def fragments?
10
+ namespace(:fragment_cache)[:fragments]
11
+ end
12
+
13
+ def fragments
14
+ namespace(:fragment_cache)[:fragments] ||= []
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module FragmentCache
5
+ module Ext
6
+ # Adds #_graphql_cache_key method to Object,
7
+ # which just call #graphql_cache_key or #cache_key.
8
+ #
9
+ # For other core classes returns string representation.
10
+ #
11
+ # Raises ArgumentError otherwise.
12
+ #
13
+ # We use a refinement to avoid case/if statements for type checking
14
+ refine Object do
15
+ def _graphql_cache_key
16
+ return graphql_cache_key if respond_to?(:graphql_cache_key)
17
+ return cache_key if respond_to?(:cache_key)
18
+ return to_a._graphql_cache_key if respond_to?(:to_a)
19
+
20
+ to_s
21
+ end
22
+ end
23
+
24
+ refine Array do
25
+ def _graphql_cache_key
26
+ map { _1._graphql_cache_key }.join("/")
27
+ end
28
+ end
29
+
30
+ refine NilClass do
31
+ def _graphql_cache_key
32
+ ""
33
+ end
34
+ end
35
+
36
+ refine TrueClass do
37
+ def _graphql_cache_key
38
+ "t"
39
+ end
40
+ end
41
+
42
+ refine FalseClass do
43
+ def _graphql_cache_key
44
+ "f"
45
+ end
46
+ end
47
+
48
+ refine String do
49
+ def _graphql_cache_key
50
+ self
51
+ end
52
+ end
53
+
54
+ refine Symbol do
55
+ def _graphql_cache_key
56
+ to_s
57
+ end
58
+ end
59
+
60
+ if RUBY_PLATFORM.match?(/java/i)
61
+ refine Integer do
62
+ def _graphql_cache_key
63
+ to_s
64
+ end
65
+ end
66
+
67
+ refine Float do
68
+ def _graphql_cache_key
69
+ to_s
70
+ end
71
+ end
72
+ else
73
+ refine Numeric do
74
+ def _graphql_cache_key
75
+ to_s
76
+ end
77
+ end
78
+ end
79
+
80
+ refine Time do
81
+ def _graphql_cache_key
82
+ to_s
83
+ end
84
+ end
85
+
86
+ refine Module do
87
+ def _graphql_cache_key
88
+ name
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module FragmentCache
5
+ # Wraps resolver with cache method
6
+ class FieldExtension < GraphQL::Schema::FieldExtension
7
+ module Patch
8
+ def initialize(*args, **kwargs, &block)
9
+ cache_fragment = kwargs.delete(:cache_fragment)
10
+
11
+ if cache_fragment
12
+ kwargs[:extensions] ||= []
13
+ kwargs[:extensions] << build_extension(cache_fragment)
14
+ end
15
+
16
+ super
17
+ end
18
+
19
+ private
20
+
21
+ def build_extension(options)
22
+ if options.is_a?(Hash)
23
+ {FieldExtension => options}
24
+ else
25
+ FieldExtension
26
+ end
27
+ end
28
+ end
29
+
30
+ def initialize(options:, **_rest)
31
+ @cache_options = options || {}
32
+
33
+ @context_key = @cache_options.delete(:context_key)
34
+ @cache_key = @cache_options.delete(:cache_key)
35
+ end
36
+
37
+ def resolve(object:, arguments:, **_options)
38
+ resolved_value = yield(object, arguments)
39
+
40
+ object_for_key = if @context_key
41
+ Array(@context_key).map { |key| object.context[key] }
42
+ elsif @cache_key == :object
43
+ object.object
44
+ elsif @cache_key == :value
45
+ resolved_value
46
+ end
47
+
48
+ cache_fragment_options = @cache_options.merge(object: object_for_key)
49
+
50
+ object.cache_fragment(cache_fragment_options) { resolved_value }
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "graphql/fragment_cache/cache_key_builder"
4
+
5
+ module GraphQL
6
+ module FragmentCache
7
+ # Represents a single fragment to cache
8
+ class Fragment
9
+ attr_reader :options, :path, :context
10
+
11
+ def initialize(context, **options)
12
+ @context = context
13
+ @options = options
14
+ @path = context.namespace(:interpreter)[:current_path]
15
+ end
16
+
17
+ def read
18
+ FragmentCache.cache_store.read(cache_key)
19
+ end
20
+
21
+ def persist(final_value)
22
+ value = resolve(final_value)
23
+ FragmentCache.cache_store.write(cache_key, value, **options)
24
+ end
25
+
26
+ private
27
+
28
+ def cache_key
29
+ @cache_key ||= CacheKeyBuilder.call(path: path, query: context.query, **options)
30
+ end
31
+
32
+ def resolve(final_value)
33
+ final_value.dig(*path)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "graphql/fragment_cache/cacher"
4
+
5
+ module GraphQL
6
+ module FragmentCache
7
+ # Adds hook for saving cached values after query is resolved
8
+ module Instrumentation
9
+ module_function
10
+
11
+ def before_query(query)
12
+ end
13
+
14
+ def after_query(query)
15
+ return unless query.valid?
16
+
17
+ Cacher.call(query)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,56 @@
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 read(key)
27
+ key = key.to_s
28
+ storage[key]&.then do |entry|
29
+ if entry.expired?
30
+ delete(key)
31
+ next
32
+ end
33
+ entry.value
34
+ end
35
+ end
36
+
37
+ def write(key, value, expires_in: default_expires_in, **options)
38
+ key = key.to_s
39
+ @storage[key] = Entry.new(value: value, expires_at: expires_in ? Time.now + expires_in : nil)
40
+ end
41
+
42
+ def delete(key)
43
+ key = key.to_s
44
+ storage.delete(key)
45
+ end
46
+
47
+ def clear
48
+ storage.clear
49
+ end
50
+
51
+ private
52
+
53
+ attr_reader :storage
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "graphql/fragment_cache/object_helpers"
4
+ require "graphql/fragment_cache/field_extension"
5
+
6
+ module GraphQL
7
+ module FragmentCache
8
+ # Adds #cache_fragment method and kwarg option
9
+ module Object
10
+ def self.included(base)
11
+ base.include(GraphQL::FragmentCache::ObjectHelpers)
12
+ base.field_class.prepend(GraphQL::FragmentCache::FieldExtension::Patch)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "graphql/fragment_cache/fragment"
4
+
5
+ module GraphQL
6
+ module FragmentCache
7
+ using Ext
8
+
9
+ # Adds #cache_fragment method
10
+ module ObjectHelpers
11
+ def cache_fragment(object_to_cache = nil, **options, &block)
12
+ fragment = Fragment.new(context, options)
13
+
14
+ if (cached = fragment.read)
15
+ return raw_value(cached)
16
+ end
17
+
18
+ context.fragments << fragment
19
+
20
+ object_to_cache || block.call
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module FragmentCache
5
+ # Extends key builder to use .expand_cache_key in Rails
6
+ class CacheKeyBuilder
7
+ def object_key(obj)
8
+ return obj.graphql_cache_key if obj.respond_to?(:graphql_cache_key)
9
+ return obj.map { |item| object_key(item) }.join("/") if obj.is_a?(Array)
10
+ return object_key(obj.to_a) if obj.respond_to?(:to_a)
11
+
12
+ ActiveSupport::Cache.expand_cache_key(obj)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "graphql/fragment_cache/rails/cache_key_builder"
4
+
5
+ module GraphQL
6
+ module FragmentCache
7
+ class Railtie < ::Rails::Railtie # :nodoc:
8
+ # Provides Rails-specific configuration,
9
+ # accessible through `Rails.application.config.graphql_fragment_cache`
10
+ module Config
11
+ class << self
12
+ def store=(store)
13
+ # Handle both:
14
+ # store = :memory
15
+ # store = :mem_cache, ENV['MEMCACHE']
16
+ if store.is_a?(Symbol) || store.is_a?(Array)
17
+ store = ActiveSupport::Cache.lookup_store(store)
18
+ end
19
+
20
+ FragmentCache.cache_store = store
21
+ end
22
+ end
23
+ end
24
+
25
+ config.graphql_fragment_cache = Config
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest/sha1"
4
+
5
+ module GraphQL
6
+ module FragmentCache
7
+ # Patches GraphQL::Schema to support fragment cache
8
+ module SchemaPatch
9
+ def schema_cache_key
10
+ @schema_cache_key ||= Digest::SHA1.hexdigest(to_definition)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module FragmentCache
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,168 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: graphql-fragment_cache
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - DmitryTsepelev
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-04-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: graphql
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 1.10.3
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 1.10.3
27
+ - !ruby/object:Gem::Dependency
28
+ name: ruby-next-core
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.5.1
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 0.5.1
41
+ - !ruby/object:Gem::Dependency
42
+ name: combustion
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.1'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.1'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '13.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '13.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.9'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.9'
83
+ - !ruby/object:Gem::Dependency
84
+ name: timecop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: ruby-next
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0.5'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0.5'
111
+ description: Fragment cache for graphql-ruby
112
+ email:
113
+ - dmitry.a.tsepelev@gmail.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - CHANGELOG.md
119
+ - LICENSE.txt
120
+ - README.md
121
+ - bin/console
122
+ - bin/setup
123
+ - lib/.rbnext/2.7/graphql/fragment_cache/cache_key_builder.rb
124
+ - lib/.rbnext/2.7/graphql/fragment_cache/cacher.rb
125
+ - lib/.rbnext/2.7/graphql/fragment_cache/ext/graphql_cache_key.rb
126
+ - lib/graphql-fragment_cache.rb
127
+ - lib/graphql/fragment_cache.rb
128
+ - lib/graphql/fragment_cache/cache_key_builder.rb
129
+ - lib/graphql/fragment_cache/cacher.rb
130
+ - lib/graphql/fragment_cache/ext/context_fragments.rb
131
+ - lib/graphql/fragment_cache/ext/graphql_cache_key.rb
132
+ - lib/graphql/fragment_cache/field_extension.rb
133
+ - lib/graphql/fragment_cache/fragment.rb
134
+ - lib/graphql/fragment_cache/instrumentation.rb
135
+ - lib/graphql/fragment_cache/memory_store.rb
136
+ - lib/graphql/fragment_cache/object.rb
137
+ - lib/graphql/fragment_cache/object_helpers.rb
138
+ - lib/graphql/fragment_cache/rails/cache_key_builder.rb
139
+ - lib/graphql/fragment_cache/railtie.rb
140
+ - lib/graphql/fragment_cache/schema_patch.rb
141
+ - lib/graphql/fragment_cache/version.rb
142
+ homepage: https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache
143
+ licenses:
144
+ - MIT
145
+ metadata:
146
+ homepage_uri: https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache
147
+ source_code_uri: https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache
148
+ changelog_uri: https://github.com/DmitryTsepelev/graphql-ruby-fragment_cache/CHANGELOG.md
149
+ post_install_message:
150
+ rdoc_options: []
151
+ require_paths:
152
+ - lib
153
+ required_ruby_version: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - ">="
156
+ - !ruby/object:Gem::Version
157
+ version: '2.5'
158
+ required_rubygems_version: !ruby/object:Gem::Requirement
159
+ requirements:
160
+ - - ">="
161
+ - !ruby/object:Gem::Version
162
+ version: '0'
163
+ requirements: []
164
+ rubygems_version: 3.0.3
165
+ signing_key:
166
+ specification_version: 4
167
+ summary: Fragment cache for graphql-ruby
168
+ test_files: []