graphiti 1.3.9 → 1.7.5

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.
@@ -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
@@ -74,6 +85,7 @@ module Graphiti
74
85
  end
75
86
  end
76
87
  alias_method :to_a, :data
88
+ alias_method :resolve_data, :data
77
89
 
78
90
  def meta
79
91
  @meta ||= data.respond_to?(:meta) ? data.meta : {}
@@ -136,7 +148,7 @@ module Graphiti
136
148
  end
137
149
 
138
150
  def destroy
139
- data
151
+ resolve_data
140
152
  transaction_response = @resource.transaction do
141
153
  metadata = {method: :destroy}
142
154
  model = @resource.destroy(@query.filters[:id], metadata)
@@ -153,11 +165,13 @@ module Graphiti
153
165
  success
154
166
  end
155
167
 
156
- def update_attributes
157
- data
168
+ def update
169
+ resolve_data
158
170
  save(action: :update)
159
171
  end
160
172
 
173
+ alias update_attributes update # standard:disable Style/Alias
174
+
161
175
  def include_hash
162
176
  @include_hash ||= begin
163
177
  base = @payload ? @payload.include_hash : {}
@@ -177,6 +191,22 @@ module Graphiti
177
191
  query.debug_requested?
178
192
  end
179
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
+
180
210
  private
181
211
 
182
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
@@ -42,7 +42,7 @@ module Graphiti
42
42
 
43
43
  def generate_types
44
44
  {}.tap do |types|
45
- Graphiti::Types.map.each_pair do |name, config|
45
+ Graphiti::Types.map.sort.each_entry do |name, config|
46
46
  types[name] = config.slice(:kind, :description)
47
47
  end
48
48
  end
@@ -43,6 +43,7 @@ module Graphiti
43
43
  parent_resource = @resource
44
44
  graphiti_context = Graphiti.context
45
45
  resolve_sideload = -> {
46
+ Graphiti.config.before_sideload&.call(graphiti_context)
46
47
  Graphiti.context = graphiti_context
47
48
  sideload.resolve(results, q, parent_resource)
48
49
  @resource.adapter.close if concurrent
@@ -66,14 +67,72 @@ module Graphiti
66
67
  end
67
68
  end
68
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
+ updated_time || Time.now
106
+ end
107
+ alias_method :last_modified_at, :updated_at
108
+
69
109
  private
70
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
+
71
129
  def broadcast_data
72
130
  opts = {
73
131
  resource: @resource,
74
- params: @opts[:params],
132
+ params: @opts[:params] || @query.params,
75
133
  sideload: @opts[:sideload],
76
- parent: @opts[:parent]
134
+ parent: @opts[:parent],
135
+ action: @query.action
77
136
  # Set once data is resolved within block
78
137
  # results: ...
79
138
  }
@@ -193,14 +193,14 @@ module Graphiti
193
193
  # Find the quoted strings
194
194
  quotes = value.scan(/{{.*?}}/)
195
195
  # remove them from the rest
196
- quotes.each { |q| value.gsub!(q, "") }
196
+ non_quotes = quotes.inject(value) { |v, q| v.gsub(q, "") }
197
197
  # remove the quote characters from the quoted strings
198
198
  quotes.each { |q| q.gsub!("{{", "").gsub!("}}", "") }
199
199
  # merge everything back together into an array
200
200
  value = if singular_filter
201
- Array(value) + quotes
201
+ Array(non_quotes) + quotes
202
202
  else
203
- Array(value.split(",")) + quotes
203
+ Array(non_quotes.split(",")) + quotes
204
204
  end
205
205
  # remove any blanks that are left
206
206
  value.reject! { |v| v.length.zero? }
@@ -71,9 +71,9 @@ module Graphiti
71
71
  end
72
72
 
73
73
  # Allow access to resource methods
74
- def method_missing(id, *args, &blk)
74
+ def method_missing(id, ...)
75
75
  if @resource.respond_to?(id, true)
76
- @resource.send(id, *args, &blk)
76
+ @resource.send(id, ...)
77
77
  else
78
78
  super
79
79
  end
@@ -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
@@ -41,7 +41,7 @@ class Graphiti::Sideload::PolymorphicBelongsTo < Graphiti::Sideload::BelongsTo
41
41
  end
42
42
 
43
43
  def on(name, &blk)
44
- group = Group.new(name)
44
+ group = Group.new(name.to_sym)
45
45
  @groups << group
46
46
  group
47
47
  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
@@ -48,6 +48,7 @@ module Graphiti
48
48
 
49
49
  def on_demand_links(url)
50
50
  return url unless Graphiti.config.links_on_demand
51
+ return unless url
51
52
 
52
53
  url << if url.include?("?")
53
54
  "&links=true"
@@ -118,7 +118,7 @@ module Graphiti
118
118
  cache_key = :"#{@sideload.object_id}-#{action}"
119
119
  return if self.class.validated_link_cache.include?(cache_key)
120
120
  prc = Graphiti.config.context_for_endpoint
121
- unless prc.call(sideload.resource.endpoint[:full_path], action)
121
+ unless prc.call(sideload.resource.endpoint[:full_path].to_s, action)
122
122
  raise Errors::InvalidLink.new(@resource_class, sideload, action)
123
123
  end
124
124
  self.class.validated_link_cache << cache_key
@@ -1,3 +1,3 @@
1
1
  module Graphiti
2
- VERSION = "1.3.9"
2
+ VERSION = "1.7.5"
3
3
  end
data/lib/graphiti.rb CHANGED
@@ -1,12 +1,17 @@
1
1
  require "json"
2
2
  require "forwardable"
3
+ require "uri"
4
+ require "ostruct" unless defined?(::OpenStruct)
5
+
6
+ require "active_support/version"
7
+ require "active_support/deprecation"
8
+ require "active_support/deprecator" if ::ActiveSupport.version >= Gem::Version.new("7.1")
3
9
  require "active_support/core_ext/string"
4
10
  require "active_support/core_ext/enumerable"
5
11
  require "active_support/core_ext/class/attribute"
6
12
  require "active_support/core_ext/hash/conversions" # to_xml
7
13
  require "active_support/concern"
8
14
  require "active_support/time"
9
- require "active_support/deprecation"
10
15
 
11
16
  require "dry-types"
12
17
  require "graphiti_errors"
@@ -83,7 +88,12 @@ module Graphiti
83
88
  end
84
89
 
85
90
  def self.log(msg, color = :white, bold = false)
86
- colored = ActiveSupport::LogSubscriber.new.send(:color, msg, color, bold)
91
+ colored = if ::ActiveSupport.version >= Gem::Version.new("7.1")
92
+ ActiveSupport::LogSubscriber.new.send(:color, msg, color, bold: bold)
93
+ else
94
+ ActiveSupport::LogSubscriber.new.send(:color, msg, color, bold)
95
+ end
96
+
87
97
  logger.debug(colored)
88
98
  end
89
99
 
@@ -100,6 +110,14 @@ module Graphiti
100
110
  r.apply_sideloads_to_serializer
101
111
  end
102
112
  end
113
+
114
+ def self.cache=(val)
115
+ @cache = val
116
+ end
117
+
118
+ def self.cache
119
+ @cache
120
+ end
103
121
  end
104
122
 
105
123
  require "graphiti/version"
@@ -131,7 +149,6 @@ require "graphiti/resource_proxy"
131
149
  require "graphiti/request_validator"
132
150
  require "graphiti/request_validators/validator"
133
151
  require "graphiti/request_validators/update_validator"
134
- require "graphiti/query"
135
152
  require "graphiti/scope"
136
153
  require "graphiti/deserializer"
137
154
  require "graphiti/renderer"
@@ -170,7 +187,9 @@ require "graphiti/extensions/extra_attribute"
170
187
  require "graphiti/extensions/boolean_attribute"
171
188
  require "graphiti/extensions/temp_id"
172
189
  require "graphiti/serializer"
190
+ require "graphiti/query"
173
191
  require "graphiti/debugger"
192
+ require "graphiti/util/cache_debug"
174
193
 
175
194
  if defined?(ActiveRecord)
176
195
  require "graphiti/adapters/active_record"
data/package.json ADDED
@@ -0,0 +1,111 @@
1
+ {
2
+ "name": "graphiti",
3
+ "version": "1.4.0",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/graphiti-api/graphiti.git"
7
+ },
8
+ "license": "MIT",
9
+ "bugs": {
10
+ "url": "https://github.com/graphiti-api/graphiti/issues"
11
+ },
12
+ "homepage": "https://graphiti.dev",
13
+ "scripts": {
14
+ "semantic-release": "semantic-release"
15
+ },
16
+ "devDependencies": {
17
+ "semantic-release-rubygem": "^1.2.0",
18
+ "semantic-release": "^19.0.3",
19
+ "@semantic-release/changelog": "^6.0.1",
20
+ "@semantic-release/git": "^10.0.1"
21
+ },
22
+ "release": {
23
+ "branches": [
24
+ "master",
25
+ {
26
+ "name": "beta",
27
+ "prerelease": true
28
+ },
29
+ {
30
+ "name": "alpha",
31
+ "prerelease": true
32
+ }
33
+ ],
34
+ "plugins": [
35
+ [
36
+ "@semantic-release/commit-analyzer",
37
+ {
38
+ "releaseRules": [
39
+ {
40
+ "type": "*!",
41
+ "release": "major"
42
+ },
43
+ {
44
+ "type": "feat",
45
+ "release": "minor"
46
+ },
47
+ {
48
+ "type": "build",
49
+ "release": "patch"
50
+ },
51
+ {
52
+ "type": "ci",
53
+ "release": "patch"
54
+ },
55
+ {
56
+ "type": "chore",
57
+ "release": "patch"
58
+ },
59
+ {
60
+ "type": "docs",
61
+ "release": "patch"
62
+ },
63
+ {
64
+ "type": "refactor",
65
+ "release": "patch"
66
+ },
67
+ {
68
+ "type": "style",
69
+ "release": "patch"
70
+ },
71
+ {
72
+ "type": "test",
73
+ "release": "patch"
74
+ }
75
+ ],
76
+ "parserOpts": {
77
+ "noteKeywords": [
78
+ "BREAKING CHANGE",
79
+ "BREAKING CHANGES",
80
+ "BREAKING",
81
+ "BREAKING CHANGE!",
82
+ "BREAKING CHANGES!",
83
+ "BREAKING!"
84
+ ]
85
+ }
86
+ }
87
+ ],
88
+ "@semantic-release/release-notes-generator",
89
+ [
90
+ "@semantic-release/changelog",
91
+ {
92
+ "changelogTitle": "graphiti changelog",
93
+ "changelogFile": "CHANGELOG.md"
94
+ }
95
+ ],
96
+ "semantic-release-rubygem",
97
+ "@semantic-release/github",
98
+ [
99
+ "@semantic-release/git",
100
+ {
101
+ "assets": [
102
+ "CHANGELOG.md"
103
+ ],
104
+ "message": "${nextRelease.version} CHANGELOG [skip ci]\n\n${nextRelease.notes}"
105
+ }
106
+ ]
107
+ ],
108
+ "debug": true,
109
+ "dryRun": false
110
+ }
111
+ }
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.3.9
4
+ version: 1.7.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lee Richmond
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-05-25 00:00:00.000000000 Z
11
+ date: 2024-09-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jsonapi-serializable
@@ -204,7 +204,7 @@ dependencies:
204
204
  - - '='
205
205
  - !ruby/object:Gem::Version
206
206
  version: 1.0.beta.4
207
- description:
207
+ description:
208
208
  email:
209
209
  - richmolj@gmail.com
210
210
  executables:
@@ -212,7 +212,9 @@ executables:
212
212
  extensions: []
213
213
  extra_rdoc_files: []
214
214
  files:
215
+ - ".github/probots.yml"
215
216
  - ".github/workflows/ci.yml"
217
+ - ".github/workflows/release.yml"
216
218
  - ".gitignore"
217
219
  - ".rspec"
218
220
  - ".standard.yml"
@@ -251,6 +253,8 @@ files:
251
253
  - gemfiles/rails_6.gemfile
252
254
  - gemfiles/rails_6_graphiti_rails.gemfile
253
255
  - gemfiles/rails_7.gemfile
256
+ - gemfiles/rails_7_1.gemfile
257
+ - gemfiles/rails_7_1_graphiti_rails.gemfile
254
258
  - gemfiles/rails_7_graphiti_rails.gemfile
255
259
  - graphiti.gemspec
256
260
  - lib/graphiti.rb
@@ -320,6 +324,7 @@ files:
320
324
  - lib/graphiti/stats/payload.rb
321
325
  - lib/graphiti/types.rb
322
326
  - lib/graphiti/util/attribute_check.rb
327
+ - lib/graphiti/util/cache_debug.rb
323
328
  - lib/graphiti/util/class.rb
324
329
  - lib/graphiti/util/field_params.rb
325
330
  - lib/graphiti/util/hash.rb
@@ -336,11 +341,12 @@ files:
336
341
  - lib/graphiti/util/transaction_hooks_recorder.rb
337
342
  - lib/graphiti/util/validation_response.rb
338
343
  - lib/graphiti/version.rb
344
+ - package.json
339
345
  homepage: https://github.com/graphiti-api/graphiti
340
346
  licenses:
341
347
  - MIT
342
348
  metadata: {}
343
- post_install_message:
349
+ post_install_message:
344
350
  rdoc_options: []
345
351
  require_paths:
346
352
  - lib
@@ -348,15 +354,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
348
354
  requirements:
349
355
  - - ">="
350
356
  - !ruby/object:Gem::Version
351
- version: '2.6'
357
+ version: '2.7'
352
358
  required_rubygems_version: !ruby/object:Gem::Requirement
353
359
  requirements:
354
360
  - - ">="
355
361
  - !ruby/object:Gem::Version
356
362
  version: '0'
357
363
  requirements: []
358
- rubygems_version: 3.3.7
359
- signing_key:
364
+ rubygems_version: 3.3.27
365
+ signing_key:
360
366
  specification_version: 4
361
367
  summary: Easily build jsonapi.org-compatible APIs
362
368
  test_files: []