graphiti 1.6.4 → 1.7.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: 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