graphiti 1.6.4 → 1.7.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: 60dd55a9db78fcc197c66de85b88bd69e840462d77a679dbb76fcd41b040dbe1
4
- data.tar.gz: e6a2cdefe4435864ea9101c94ac122d3c11ba0e85b7284f19a08f5ec4a3cee0d
3
+ metadata.gz: 48e9df9f78bfe2d9b9303f304541894222fa4eb3d925f2ac68a01e3b0567fbb4
4
+ data.tar.gz: d8d010bca5894bc11cc0bfb76f9d88deaf6d95d7b413d937765b58d87fefe1cc
5
5
  SHA512:
6
- metadata.gz: 2264e0a1313762c00ffb96fbb80c60bbc7d6406e37fedc8eef0a9b813b6bc5d5ff9443ca165d2afcf7499c61f9746a0d851b699b9ec5f90ba5ca74c16932c428
7
- data.tar.gz: 74d51069de9047b07d213c628889766d54d8e8c43812d75a433247e70bc2b027dc7735e310bd0460a36e37083c7a9306dddbeb5d15b0160987cb8f5a65daaa93
6
+ metadata.gz: 56ec9608ea8de6b8a8be491e27a8dc0ef83b297a3178e617495d8a63098d8df2d408b944e4f314eacd98e8e87e278e01fa287b68e07961228086ffb21259d509
7
+ data.tar.gz: f2077122a2b0a4996a0326f21fcae529724dd6438303f42822eb73605b58801c1c8466d012d8f3edf5899cb82ed234373fcc989f4630b012d7e045c32e40894d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  graphiti changelog
2
2
 
3
+ # [1.7.0](https://github.com/graphiti-api/graphiti/compare/v1.6.4...v1.7.0) (2024-03-27)
4
+
5
+
6
+ ### Features
7
+
8
+ * Add support for caching renders in Graphiti, and better support using etags and stale? in the controller ([#424](https://github.com/graphiti-api/graphiti/issues/424)) ([8bae50a](https://github.com/graphiti-api/graphiti/commit/8bae50ab82559e2644d506e16a4f715effd89317))
9
+
3
10
  ## [1.6.4](https://github.com/graphiti-api/graphiti/compare/v1.6.3...v1.6.4) (2024-03-27)
4
11
 
5
12
  ## [1.6.3](https://github.com/graphiti-api/graphiti/compare/v1.6.2...v1.6.3) (2024-03-26)
@@ -20,6 +20,7 @@ module Graphiti
20
20
  attr_reader :debug, :debug_models
21
21
 
22
22
  attr_writer :schema_path
23
+ attr_writer :cache_rendering
23
24
 
24
25
  # Set defaults
25
26
  # @api private
@@ -32,6 +33,7 @@ module Graphiti
32
33
  @pagination_links = false
33
34
  @typecast_reads = true
34
35
  @raise_on_missing_sidepost = true
36
+ @cache_rendering = false
35
37
  self.debug = ENV.fetch("GRAPHITI_DEBUG", true)
36
38
  self.debug_models = ENV.fetch("GRAPHITI_DEBUG_MODELS", false)
37
39
 
@@ -52,6 +54,16 @@ module Graphiti
52
54
  end
53
55
  end
54
56
 
57
+ def cache_rendering?
58
+ use_caching = @cache_rendering && Graphiti.cache.respond_to?(:fetch)
59
+
60
+ use_caching.tap do |use|
61
+ if @cache_rendering && !Graphiti.cache&.respond_to?(:fetch)
62
+ raise "You must configure a cache store in order to use cache_rendering. Set Graphiti.cache = Rails.cache, for example."
63
+ end
64
+ end
65
+ end
66
+
55
67
  def schema_path
56
68
  @schema_path ||= raise("No schema_path defined! Set Graphiti.config.schema_path to save your schema.")
57
69
  end
@@ -98,7 +98,30 @@ module Graphiti
98
98
  took = ((stop - start) * 1000.0).round(2)
99
99
  logs << [""]
100
100
  logs << ["=== Graphiti Debug", :green, true]
101
- logs << ["Rendering:", :green, true]
101
+ if payload[:proxy]&.cached? && Graphiti.config.cache_rendering?
102
+ logs << ["Rendering (cached):", :green, true]
103
+
104
+ Graphiti::Util::CacheDebug.new(payload[:proxy]).analyze do |cache_debug|
105
+ logs << ["Cache key for #{cache_debug.name}", :blue, true]
106
+ logs << if cache_debug.volatile?
107
+ [" \\_ volatile | Request count: #{cache_debug.request_count} | Hit count: #{cache_debug.hit_count}", :red, true]
108
+ else
109
+ [" \\_ stable | Request count: #{cache_debug.request_count} | Hit count: #{cache_debug.hit_count}", :blue, true]
110
+ end
111
+
112
+ if cache_debug.changed_key?
113
+ logs << [" [x] cache key changed #{cache_debug.last_version[:etag]} -> #{cache_debug.current_version[:etag]}", :red]
114
+ logs << [" removed: #{cache_debug.removed_segments}", :red]
115
+ logs << [" added: #{cache_debug.added_segments}", :red]
116
+ elsif cache_debug.new_key?
117
+ logs << [" [+] cache key added #{cache_debug.current_version[:etag]}", :red, true]
118
+ else
119
+ logs << [" [✓] #{cache_debug.current_version[:etag]}", :green, true]
120
+ end
121
+ end
122
+ else
123
+ logs << ["Rendering:", :green, true]
124
+ end
102
125
  logs << ["Took: #{took}ms", :magenta, true]
103
126
  end
104
127
  end
@@ -1,3 +1,5 @@
1
+ require "digest"
2
+
1
3
  module Graphiti
2
4
  class Query
3
5
  attr_reader :resource, :association_name, :params, :action
@@ -232,8 +234,22 @@ module Graphiti
232
234
  ![false, "false"].include?(@params[:paginate])
233
235
  end
234
236
 
237
+ def cache_key
238
+ "args-#{query_cache_key}"
239
+ end
240
+
235
241
  private
236
242
 
243
+ def query_cache_key
244
+ attrs = {extra_fields: extra_fields,
245
+ fields: fields,
246
+ links: links?,
247
+ pagination_links: pagination_links?,
248
+ format: params[:format]}
249
+
250
+ Digest::SHA1.hexdigest(attrs.to_s)
251
+ end
252
+
237
253
  def cast_page_param(name, value)
238
254
  if [:before, :after].include?(name)
239
255
  decode_cursor(value)
@@ -68,7 +68,14 @@ module Graphiti
68
68
  options[:meta][:debug] = Debugger.to_a if debug_json?
69
69
  options[:proxy] = proxy
70
70
 
71
- renderer.render(records, options)
71
+ if proxy.cache? && Graphiti.config.cache_rendering?
72
+ Graphiti.cache.fetch("graphiti:render/#{proxy.cache_key}", version: proxy.updated_at, expires_in: proxy.cache_expires_in) do
73
+ options.delete(:cache) # ensure that we don't use JSONAPI-Resources's built-in caching logic
74
+ renderer.render(records, options)
75
+ end
76
+ else
77
+ renderer.render(records, options)
78
+ end
72
79
  end
73
80
  end
74
81
 
@@ -4,6 +4,11 @@ module Graphiti
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  class_methods do
7
+ def cache_resource(expires_in: false)
8
+ @cache_resource = true
9
+ @cache_expires_in = expires_in
10
+ end
11
+
7
12
  def all(params = {}, base_scope = nil)
8
13
  validate_request!(params)
9
14
  _all(params, {}, base_scope)
@@ -13,7 +18,7 @@ module Graphiti
13
18
  def _all(params, opts, base_scope)
14
19
  runner = Runner.new(self, params, opts.delete(:query), :all)
15
20
  opts[:params] = params
16
- runner.proxy(base_scope, opts)
21
+ runner.proxy(base_scope, opts.merge(caching_options))
17
22
  end
18
23
 
19
24
  def find(params = {}, base_scope = nil)
@@ -31,10 +36,14 @@ module Graphiti
31
36
  params[:filter][:id] = id if id
32
37
 
33
38
  runner = Runner.new(self, params, nil, :find)
34
- runner.proxy base_scope,
39
+
40
+ find_options = {
35
41
  single: true,
36
42
  raise_on_missing: true,
37
43
  bypass_required_filters: true
44
+ }.merge(caching_options)
45
+
46
+ runner.proxy base_scope, find_options
38
47
  end
39
48
 
40
49
  def build(params, base_scope = nil)
@@ -45,6 +54,10 @@ module Graphiti
45
54
 
46
55
  private
47
56
 
57
+ def caching_options
58
+ {cache: @cache_resource, cache_expires_in: @cache_expires_in}
59
+ end
60
+
48
61
  def validate_request!(params)
49
62
  return if Graphiti.context[:graphql] || !validate_endpoints?
50
63
 
@@ -2,20 +2,31 @@ module Graphiti
2
2
  class ResourceProxy
3
3
  include Enumerable
4
4
 
5
- attr_reader :resource, :query, :scope, :payload
5
+ attr_reader :resource, :query, :scope, :payload, :cache_expires_in, :cache
6
6
 
7
7
  def initialize(resource, scope, query,
8
8
  payload: nil,
9
9
  single: false,
10
- raise_on_missing: false)
10
+ raise_on_missing: false,
11
+ cache: nil,
12
+ cache_expires_in: nil)
13
+
11
14
  @resource = resource
12
15
  @scope = scope
13
16
  @query = query
14
17
  @payload = payload
15
18
  @single = single
16
19
  @raise_on_missing = raise_on_missing
20
+ @cache = cache
21
+ @cache_expires_in = cache_expires_in
22
+ end
23
+
24
+ def cache?
25
+ !!@cache
17
26
  end
18
27
 
28
+ alias_method :cached?, :cache?
29
+
19
30
  def single?
20
31
  !!@single
21
32
  end
@@ -180,6 +191,22 @@ module Graphiti
180
191
  query.debug_requested?
181
192
  end
182
193
 
194
+ def updated_at
195
+ @scope.updated_at
196
+ end
197
+
198
+ def etag
199
+ "W/#{ActiveSupport::Digest.hexdigest(cache_key_with_version.to_s)}"
200
+ end
201
+
202
+ def cache_key
203
+ ActiveSupport::Cache.expand_cache_key([@scope.cache_key, @query.cache_key])
204
+ end
205
+
206
+ def cache_key_with_version
207
+ ActiveSupport::Cache.expand_cache_key([@scope.cache_key_with_version, @query.cache_key])
208
+ end
209
+
183
210
  private
184
211
 
185
212
  def persist
@@ -71,7 +71,9 @@ module Graphiti
71
71
  query,
72
72
  payload: deserialized_payload,
73
73
  single: opts[:single],
74
- raise_on_missing: opts[:raise_on_missing]
74
+ raise_on_missing: opts[:raise_on_missing],
75
+ cache: opts[:cache],
76
+ cache_expires_in: opts[:cache_expires_in]
75
77
  end
76
78
  end
77
79
  end
@@ -67,8 +67,64 @@ module Graphiti
67
67
  end
68
68
  end
69
69
 
70
+ def parent_resource
71
+ @resource
72
+ end
73
+
74
+ def cache_key
75
+ # This is the combined cache key for the base query and the query for all sideloads
76
+ # Changing the query will yield a different cache key
77
+
78
+ cache_keys = sideload_resource_proxies.map { |proxy| proxy.try(:cache_key) }
79
+
80
+ cache_keys << @object.try(:cache_key) # this is what calls into the ORM (ActiveRecord, most likely)
81
+ ActiveSupport::Cache.expand_cache_key(cache_keys.flatten.compact)
82
+ end
83
+
84
+ def cache_key_with_version
85
+ # This is the combined and versioned cache key for the base query and the query for all sideloads
86
+ # If any returned model's updated_at changes, this key will change
87
+
88
+ cache_keys = sideload_resource_proxies.map { |proxy| proxy.try(:cache_key_with_version) }
89
+
90
+ cache_keys << @object.try(:cache_key_with_version) # this is what calls into ORM (ActiveRecord, most likely)
91
+ ActiveSupport::Cache.expand_cache_key(cache_keys.flatten.compact)
92
+ end
93
+
94
+ def updated_at
95
+ updated_ats = sideload_resource_proxies.map(&:updated_at)
96
+
97
+ begin
98
+ updated_ats << @object.maximum(:updated_at)
99
+ rescue => e
100
+ Graphiti.log("error calculating last_modified_at for #{@resource.class}")
101
+ Graphiti.log(e)
102
+ end
103
+
104
+ updated_ats.compact.max
105
+ end
106
+ alias_method :last_modified_at, :updated_at
107
+
70
108
  private
71
109
 
110
+ def sideload_resource_proxies
111
+ @sideload_resource_proxies ||= begin
112
+ @object = @resource.before_resolve(@object, @query)
113
+ results = @resource.resolve(@object)
114
+
115
+ [].tap do |proxies|
116
+ unless @query.sideloads.empty?
117
+ @query.sideloads.each_pair do |name, q|
118
+ sideload = @resource.class.sideload(name)
119
+ next if sideload.nil? || sideload.shared_remote?
120
+
121
+ proxies << sideload.build_resource_proxy(results, q, parent_resource)
122
+ end
123
+ end
124
+ end.flatten
125
+ end
126
+ end
127
+
72
128
  def broadcast_data
73
129
  opts = {
74
130
  resource: @resource,
@@ -99,7 +99,7 @@ module Graphiti
99
99
 
100
100
  def strip_relationships?
101
101
  return false unless Graphiti.config.links_on_demand
102
- params = Graphiti.context[:object].params || {}
102
+ params = Graphiti.context[:object]&.params || {}
103
103
  [false, nil, "false"].include?(params[:links])
104
104
  end
105
105
  end
@@ -209,13 +209,16 @@ module Graphiti
209
209
  end
210
210
  end
211
211
 
212
- def load(parents, query, graph_parent)
213
- params, opts, proxy = nil, nil, nil
212
+ def build_resource_proxy(parents, query, graph_parent)
213
+ params = nil
214
+ opts = nil
215
+ proxy = nil
214
216
 
215
217
  with_error_handling Errors::SideloadParamsError do
216
218
  params = load_params(parents, query)
217
219
  params_proc&.call(params, parents, context)
218
220
  return [] if blank_query?(params)
221
+
219
222
  opts = load_options(parents, query)
220
223
  opts[:sideload] = self
221
224
  opts[:parent] = graph_parent
@@ -228,7 +231,11 @@ module Graphiti
228
231
  pre_load_proc&.call(proxy, parents)
229
232
  end
230
233
 
231
- proxy.to_a
234
+ proxy
235
+ end
236
+
237
+ def load(parents, query, graph_parent)
238
+ build_resource_proxy(parents, query, graph_parent).to_a
232
239
  end
233
240
 
234
241
  # Override in subclass
@@ -0,0 +1,88 @@
1
+ module Graphiti
2
+ module Util
3
+ class CacheDebug
4
+ attr_reader :proxy
5
+
6
+ def initialize(proxy)
7
+ @proxy = proxy
8
+ end
9
+
10
+ def last_version
11
+ @last_version ||= Graphiti.cache.read(key) || {}
12
+ end
13
+
14
+ def name
15
+ "#{Graphiti.context[:object].request.method} #{Graphiti.context[:object].request.url}"
16
+ end
17
+
18
+ def key
19
+ "graphiti:debug/#{name}"
20
+ end
21
+
22
+ def current_version
23
+ @current_version ||= {
24
+ cache_key: proxy.cache_key_with_version,
25
+ version: proxy.updated_at,
26
+ expires_in: proxy.cache_expires_in,
27
+ etag: proxy.etag,
28
+ miss_count: last_version[:miss_count].to_i + (changed_key? ? 1 : 0),
29
+ hit_count: last_version[:hit_count].to_i + (!changed_key? && !new_key? ? 1 : 0),
30
+ request_count: last_version[:request_count].to_i + (last_version.present? ? 1 : 0)
31
+ }
32
+ end
33
+
34
+ def analyze
35
+ yield self
36
+ save
37
+ end
38
+
39
+ def request_count
40
+ current_version[:request_count]
41
+ end
42
+
43
+ def miss_count
44
+ current_version[:miss_count]
45
+ end
46
+
47
+ def hit_count
48
+ current_version[:hit_count]
49
+ end
50
+
51
+ def change_percentage
52
+ return 0 if request_count == 0
53
+ (miss_count.to_i / request_count.to_f * 100).round(1)
54
+ end
55
+
56
+ def volatile?
57
+ change_percentage > 50
58
+ end
59
+
60
+ def new_key?
61
+ last_version[:cache_key].blank? && proxy.cache_key_with_version
62
+ end
63
+
64
+ def changed_key?
65
+ last_version[:cache_key] != proxy.cache_key_with_version && !new_key?
66
+ end
67
+
68
+ def removed_segments
69
+ changes[1] - changes[0]
70
+ end
71
+
72
+ def added_segments
73
+ changes[0] - changes[1]
74
+ end
75
+
76
+ def changes
77
+ sub_keys_old = last_version[:cache_key]&.scan(/\w+\/query-[a-z0-9-]+\/args-[a-z0-9-]+/).to_a || []
78
+ sub_keys_new = current_version[:cache_key]&.scan(/\w+\/query-[a-z0-9-]+\/args-[a-z0-9-]+/).to_a || []
79
+
80
+ [sub_keys_old, sub_keys_new]
81
+ end
82
+
83
+ def save
84
+ Graphiti.cache.write(key, current_version)
85
+ end
86
+ end
87
+ end
88
+ end
@@ -1,3 +1,3 @@
1
1
  module Graphiti
2
- VERSION = "1.6.4"
2
+ VERSION = "1.7.0"
3
3
  end
data/lib/graphiti.rb CHANGED
@@ -106,6 +106,14 @@ module Graphiti
106
106
  r.apply_sideloads_to_serializer
107
107
  end
108
108
  end
109
+
110
+ def self.cache=(val)
111
+ @cache = val
112
+ end
113
+
114
+ def self.cache
115
+ @cache
116
+ end
109
117
  end
110
118
 
111
119
  require "graphiti/version"
@@ -177,6 +185,7 @@ require "graphiti/extensions/temp_id"
177
185
  require "graphiti/serializer"
178
186
  require "graphiti/query"
179
187
  require "graphiti/debugger"
188
+ require "graphiti/util/cache_debug"
180
189
 
181
190
  if defined?(ActiveRecord)
182
191
  require "graphiti/adapters/active_record"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphiti
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.4
4
+ version: 1.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lee Richmond
@@ -324,6 +324,7 @@ files:
324
324
  - lib/graphiti/stats/payload.rb
325
325
  - lib/graphiti/types.rb
326
326
  - lib/graphiti/util/attribute_check.rb
327
+ - lib/graphiti/util/cache_debug.rb
327
328
  - lib/graphiti/util/class.rb
328
329
  - lib/graphiti/util/field_params.rb
329
330
  - lib/graphiti/util/hash.rb