graphiti 1.6.4 → 1.7.1

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: fb08a2d097076d868284efb475a82bfb9f44b697bc322dda5a654a6d259384cf
4
+ data.tar.gz: ef60e4a509d87c76973a15a59105a50d9de7635d3084afe2621f23008ff46580
5
5
  SHA512:
6
- metadata.gz: 2264e0a1313762c00ffb96fbb80c60bbc7d6406e37fedc8eef0a9b813b6bc5d5ff9443ca165d2afcf7499c61f9746a0d851b699b9ec5f90ba5ca74c16932c428
7
- data.tar.gz: 74d51069de9047b07d213c628889766d54d8e8c43812d75a433247e70bc2b027dc7735e310bd0460a36e37083c7a9306dddbeb5d15b0160987cb8f5a65daaa93
6
+ metadata.gz: bd5489249c9c768f83e05bfcd6be59bacfef2a50826c9eb547ae6a9b12cdfc25fbd54e36d54e0a37264e77458cfdc4415e707aacdeb6377179cf44a7480850a3
7
+ data.tar.gz: 7be1d2008f1f74b3a5203053eb4dc91efe013a33e5c2cada66f409f9de1a515f720e357ef619b8459f1451d90f9fb39c3a130502304f7ce2eb408a421d29f511
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  graphiti changelog
2
2
 
3
+ ## [1.7.1](https://github.com/graphiti-api/graphiti/compare/v1.7.0...v1.7.1) (2024-04-18)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * properly display .find vs .all in debugger statements ([d2a7a03](https://github.com/graphiti-api/graphiti/commit/d2a7a038a649818979d52ccd898e68dba78b051f))
9
+ * rescue error from sideloads updated_at calculation, defaulting to the current time ([661e3b5](https://github.com/graphiti-api/graphiti/commit/661e3b5212e2649870a200067d0d5d52fa962637))
10
+
11
+ # [1.7.0](https://github.com/graphiti-api/graphiti/compare/v1.6.4...v1.7.0) (2024-03-27)
12
+
13
+
14
+ ### Features
15
+
16
+ * 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))
17
+
3
18
  ## [1.6.4](https://github.com/graphiti-api/graphiti/compare/v1.6.3...v1.6.4) (2024-03-27)
4
19
 
5
20
  ## [1.6.3](https://github.com/graphiti-api/graphiti/compare/v1.6.2...v1.6.3) (2024-03-26)
data/README.md CHANGED
@@ -1,6 +1,7 @@
1
- ![Build Status](https://travis-ci.org/graphiti-api/graphiti.svg?branch=master)
1
+ [![CI](https://github.com/graphiti-api/graphiti/actions/workflows/ci.yml/badge.svg)](https://github.com/graphiti-api/graphiti/actions/workflows/ci.yml)
2
2
  [![Gem Version](https://badge.fury.io/rb/graphiti.svg)](https://badge.fury.io/rb/graphiti)
3
3
  [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
4
+ [![semantic-release: angular](https://img.shields.io/badge/semantic--release-angular-e10079?logo=semantic-release)](https://github.com/semantic-release/semantic-release)
4
5
 
5
6
  <p align="center">
6
7
  <a href="https://www.graphiti.dev/guides">
@@ -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
@@ -36,7 +36,7 @@ module Graphiti
36
36
  json[:sideload] = sideload.name
37
37
  end
38
38
  if params
39
- query = "#{payload[:resource].class.name}.all(#{JSON.pretty_generate(params)}).data"
39
+ query = "#{payload[:resource].class.name}.#{payload[:action]}(#{JSON.pretty_generate(params)}).data"
40
40
  logs << [query, :cyan, true]
41
41
  logs << ["The error occurred when running the above query. Copy/paste it into a rake task or Rails console session to reproduce. Keep in mind you may have to set context.", :yellow, true]
42
42
  else
@@ -64,7 +64,7 @@ module Graphiti
64
64
  query = if sideload.class.scope_proc
65
65
  "#{payload[:resource].class.name}: Manual sideload via .scope"
66
66
  else
67
- "#{payload[:resource].class.name}.all(#{params.inspect})"
67
+ "#{payload[:resource].class.name}.#{payload[:action]}(#{params.inspect})"
68
68
  end
69
69
  logs << [" #{query}", :cyan, true]
70
70
  json[:query] = query
@@ -82,7 +82,7 @@ module Graphiti
82
82
  title = "Top Level Data Retrieval (+ sideloads):"
83
83
  logs << [title, :green, true]
84
84
  json[:title] = title
85
- query = "#{payload[:resource].class.name}.all(#{params.inspect})"
85
+ query = "#{payload[:resource].class.name}.#{payload[:action]}(#{params.inspect})"
86
86
  logs << [query, :cyan, true]
87
87
  json[:query] = query
88
88
  logs << ["Returned Models: #{results}"] if debug_models
@@ -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,14 +67,72 @@ 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_time = nil
96
+ begin
97
+ updated_ats = sideload_resource_proxies.map(&:updated_at)
98
+ updated_ats << @object.maximum(:updated_at)
99
+ updated_time = updated_ats.compact.max
100
+ rescue => e
101
+ Graphiti.log(["error calculating last_modified_at for #{@resource.class}", :red])
102
+ Graphiti.log(e)
103
+ end
104
+
105
+ return updated_time || Time.now
106
+ end
107
+ alias_method :last_modified_at, :updated_at
108
+
70
109
  private
71
110
 
111
+ def sideload_resource_proxies
112
+ @sideload_resource_proxies ||= begin
113
+ @object = @resource.before_resolve(@object, @query)
114
+ results = @resource.resolve(@object)
115
+
116
+ [].tap do |proxies|
117
+ unless @query.sideloads.empty?
118
+ @query.sideloads.each_pair do |name, q|
119
+ sideload = @resource.class.sideload(name)
120
+ next if sideload.nil? || sideload.shared_remote?
121
+
122
+ proxies << sideload.build_resource_proxy(results, q, parent_resource)
123
+ end
124
+ end
125
+ end.flatten
126
+ end
127
+ end
128
+
72
129
  def broadcast_data
73
130
  opts = {
74
131
  resource: @resource,
75
- params: @opts[:params],
132
+ params: @opts[:params] || @query.params,
76
133
  sideload: @opts[:sideload],
77
- parent: @opts[:parent]
134
+ parent: @opts[:parent],
135
+ action: @query.action
78
136
  # Set once data is resolved within block
79
137
  # results: ...
80
138
  }
@@ -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.1"
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,14 +1,14 @@
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.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lee Richmond
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-03-27 00:00:00.000000000 Z
11
+ date: 2024-04-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jsonapi-serializable
@@ -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