graphiti 1.3.9 → 1.7.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []