iknow_view_models 3.9.2 → 3.10.1

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: 13912eaa6913049d8b8a5ac98d135d25f23abbfedf3046ba9c1a12ee92bae45d
4
- data.tar.gz: 9d8c5abafd90bac5cfa955aa152b89d03cd0657f3923edb5e7ef5e534cd5e4a1
3
+ metadata.gz: 417eb3615156fa48cf438ead03e2ea33e323c5baa7d411b1e0fafcea5050acb1
4
+ data.tar.gz: cdede3ad865389a0d006725819595aec6da1ea417e9d061452ef3198a39af3d8
5
5
  SHA512:
6
- metadata.gz: 3072dd149b35d236ca79f0a2a22232bf30274a05a141ce77f74358a1c2ac9b72f7c877879bbff1508c71fae477d0ebdff4daf2c35347bf71e07cc3811e4227c2
7
- data.tar.gz: d42c834cf4e569b8685a9e787fc9ea01de1bc656f5f5cfd6255228806556823368f2685913414b68c84d23474b19d33329803d89c47525c91194f1b2a88d2b26
6
+ metadata.gz: ada26778b54ffb60a825d408fe40f05c8a4aa93cbac7b5519b2338b5afb4cadfda0d1eb7b359b65584f88fbb9cadea4a35a7d2ba0aad9fb8e4d8ec9d5f894e03
7
+ data.tar.gz: b70d2b42d4a599c3d284dbb96c4506c8ff7b6104108ebcb7126c554fcd671eb0bad04dc4df5a1006f76af272a71c5ca46f54736bf4961821dd7f22160d46fada
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module IknowViewModels
4
- VERSION = '3.9.2'
4
+ VERSION = '3.10.1'
5
5
  end
@@ -30,8 +30,21 @@ class ViewModel::ActiveRecord::Cache
30
30
  end
31
31
 
32
32
  def render_from_cache(viewmodel_class, ids, initial_viewmodels: nil, migration_versions: {}, locked: false, serialize_context: viewmodel_class.new_serialize_context)
33
- worker = CacheWorker.new(migration_versions: migration_versions, serialize_context: serialize_context)
34
- worker.render_from_cache(viewmodel_class, ids, initial_viewmodels: initial_viewmodels, locked: locked)
33
+ ignore_existing = false
34
+ begin
35
+ worker = CacheWorker.new(migration_versions: migration_versions, serialize_context: serialize_context, ignore_existing: ignore_existing)
36
+ worker.render_from_cache(viewmodel_class, ids, initial_viewmodels: initial_viewmodels, locked: locked)
37
+ rescue StaleCachedReference
38
+ # If the cache contents contained a unresolvable stale reference, retry
39
+ # while ignoring existing cached values (thereby updating the cache). This
40
+ # will have the side-effect of duplicate Before/AfterVisit callbacks.
41
+ if ignore_existing
42
+ raise
43
+ else
44
+ ignore_existing = true
45
+ retry
46
+ end
47
+ end
35
48
  end
36
49
  end
37
50
 
@@ -76,18 +89,25 @@ class ViewModel::ActiveRecord::Cache
76
89
  migration_versions: migration_versions, serialize_context: serialize_context)
77
90
  end
78
91
 
92
+ class StaleCachedReference < StandardError
93
+ def initialize(error)
94
+ super("Cached value contained stale reference: #{error.message}")
95
+ end
96
+ end
97
+
79
98
  class CacheWorker
80
99
  SENTINEL = Object.new
81
100
  WorklistEntry = Struct.new(:ref_name, :viewmodel)
82
101
 
83
102
  attr_reader :migration_versions, :serialize_context, :resolved_references
84
103
 
85
- def initialize(migration_versions:, serialize_context:)
104
+ def initialize(migration_versions:, serialize_context:, ignore_existing: false)
86
105
  @worklist = {} # Hash[type_name, Hash[id, WorklistEntry]]
87
106
  @resolved_references = {} # Hash[refname, json]
88
107
  @migration_versions = migration_versions
89
108
  @migrated_cache_versions = {}
90
109
  @serialize_context = serialize_context
110
+ @ignore_existing = ignore_existing
91
111
  end
92
112
 
93
113
  def render_from_cache(viewmodel_class, ids, initial_viewmodels: nil, locked: false)
@@ -104,7 +124,7 @@ class ViewModel::ActiveRecord::Cache
104
124
 
105
125
  ids_to_render = ids.to_set
106
126
 
107
- if viewmodel_class < CacheableView
127
+ if viewmodel_class < CacheableView && !@ignore_existing
108
128
  # Load existing serializations from the cache
109
129
  cached_serializations = load_from_cache(viewmodel_class.viewmodel_cache, ids)
110
130
  cached_serializations.each do |id, data|
@@ -168,7 +188,7 @@ class ViewModel::ActiveRecord::Cache
168
188
  @resolved_references[entry.ref_name] = SENTINEL
169
189
  end
170
190
 
171
- if viewmodel_class < CacheableView
191
+ if viewmodel_class < CacheableView && !@ignore_existing
172
192
  cached_serializations = load_from_cache(viewmodel_class.viewmodel_cache, required_entries.keys)
173
193
  cached_serializations.each do |id, data|
174
194
  ref_name = required_entries.delete(id).ref_name
@@ -181,8 +201,18 @@ class ViewModel::ActiveRecord::Cache
181
201
  h[id] = entry.viewmodel if entry.viewmodel
182
202
  end
183
203
 
184
- viewmodels = find_and_preload_viewmodels(viewmodel_class, required_entries.keys,
185
- available_viewmodels: available_viewmodels)
204
+ viewmodels =
205
+ begin
206
+ find_and_preload_viewmodels(viewmodel_class, required_entries.keys,
207
+ available_viewmodels: available_viewmodels)
208
+ rescue ViewModel::DeserializationError::NotFound => e
209
+ # We encountered a reference to an entity that does not exist.
210
+ # If this reference was potentially found in cached data, it
211
+ # could be stale: we can retry without using the cache.
212
+ # If the reference was obtained directly, it indicates invalid
213
+ # data such as an invalid foreign key, and we cannot recover.
214
+ raise StaleCachedReference.new(e)
215
+ end
186
216
 
187
217
  loaded_serializations = serialize_and_cache(viewmodels)
188
218
  loaded_serializations.each do |id, data|
@@ -3,6 +3,7 @@
3
3
  require 'view_model/active_record/controller_base'
4
4
  require 'view_model/active_record/collection_nested_controller'
5
5
  require 'view_model/active_record/singular_nested_controller'
6
+ require 'view_model/controller/migration_versions'
6
7
 
7
8
  # Controller for accessing an ViewModel::ActiveRecord
8
9
  # Provides for the following routes:
@@ -16,8 +17,7 @@ module ViewModel::ActiveRecord::Controller
16
17
  include ViewModel::ActiveRecord::ControllerBase
17
18
  include ViewModel::ActiveRecord::CollectionNestedController
18
19
  include ViewModel::ActiveRecord::SingularNestedController
19
-
20
- MIGRATION_VERSION_HEADER = 'X-ViewModel-Versions'
20
+ include ViewModel::Controller::MigrationVersions
21
21
 
22
22
  def show(scope: nil, viewmodel_class: self.viewmodel_class, serialize_context: new_serialize_context(viewmodel_class: viewmodel_class))
23
23
  view = nil
@@ -95,51 +95,6 @@ module ViewModel::ActiveRecord::Controller
95
95
  parse_param(:id)
96
96
  end
97
97
 
98
- def migration_versions
99
- @migration_versions ||=
100
- begin
101
- specified_migration_versions.reject do |viewmodel_class, required_version|
102
- viewmodel_class.schema_version == required_version
103
- end.freeze
104
- end
105
- end
106
-
107
- def specified_migration_versions
108
- @specified_migration_versions ||=
109
- begin
110
- version_spec =
111
- if params.include?(:versions)
112
- params[:versions]
113
- elsif request.headers.include?(MIGRATION_VERSION_HEADER)
114
- begin
115
- JSON.parse(request.headers[MIGRATION_VERSION_HEADER])
116
- rescue JSON::ParserError
117
- raise ViewModel::Error.new(status: 400, detail: "Invalid JSON in #{MIGRATION_VERSION_HEADER}")
118
- end
119
- else
120
- {}
121
- end
122
-
123
- versions =
124
- IknowParams::Parser.parse_value(
125
- version_spec,
126
- with: IknowParams::Serializer::HashOf.new(
127
- IknowParams::Serializer::String, IknowParams::Serializer::Integer))
128
-
129
- migration_versions = {}
130
-
131
- versions.each do |view_name, required_version|
132
- viewmodel_class = ViewModel::Registry.for_view_name(view_name)
133
- migration_versions[viewmodel_class] = required_version
134
- rescue ViewModel::DeserializationError::UnknownView
135
- # Ignore requests to migrate types that no longer exist
136
- next
137
- end
138
-
139
- migration_versions.freeze
140
- end
141
- end
142
-
143
98
  def migrated_deep_schema_version
144
99
  ViewModel::Migrator.migrated_deep_schema_version(viewmodel_class, migration_versions, include_referenced: true)
145
100
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ViewModel::Controller::MigrationVersions
4
+ extend ActiveSupport::Concern
5
+
6
+ MIGRATION_VERSION_HEADER = 'X-ViewModel-Versions'
7
+
8
+ def migration_versions
9
+ @migration_versions ||=
10
+ begin
11
+ specified_migration_versions.reject do |viewmodel_class, required_version|
12
+ viewmodel_class.schema_version == required_version
13
+ end.freeze
14
+ end
15
+ end
16
+
17
+ def specified_migration_versions
18
+ @specified_migration_versions ||=
19
+ begin
20
+ version_spec =
21
+ if params.include?(:versions)
22
+ params[:versions]
23
+ elsif request.headers.include?(MIGRATION_VERSION_HEADER)
24
+ begin
25
+ JSON.parse(request.headers[MIGRATION_VERSION_HEADER])
26
+ rescue JSON::ParserError
27
+ raise ViewModel::Error.new(status: 400, detail: "Invalid JSON in #{MIGRATION_VERSION_HEADER}")
28
+ end
29
+ else
30
+ {}
31
+ end
32
+
33
+ versions =
34
+ IknowParams::Parser.parse_value(
35
+ version_spec,
36
+ with: IknowParams::Serializer::HashOf.new(
37
+ IknowParams::Serializer::String, IknowParams::Serializer::Integer))
38
+
39
+ migration_versions = {}
40
+
41
+ versions.each do |view_name, required_version|
42
+ viewmodel_class = ViewModel::Registry.for_view_name(view_name)
43
+ migration_versions[viewmodel_class] = required_version
44
+ rescue ViewModel::DeserializationError::UnknownView
45
+ # Ignore requests to migrate types that no longer exist
46
+ next
47
+ end
48
+
49
+ migration_versions.freeze
50
+ end
51
+ end
52
+ end
@@ -209,11 +209,27 @@ class ViewModel::ActiveRecord
209
209
  end
210
210
 
211
211
  it 'returns the right serialization with provided locked initial viewmodel' do
212
- locked_root_view = viewmodel_class.new(model_class.lock("FOR SHARE").find(root.id))
212
+ locked_root_view = viewmodel_class.new(model_class.lock('FOR SHARE').find(root.id))
213
213
  fetched_result = parse_result(fetch_with_cache(initial_viewmodels: [locked_root_view], locked: true))
214
214
 
215
215
  value(fetched_result).must_equal(serialize_from_database)
216
216
  end
217
+
218
+ it 'handles a stale cached reference' do
219
+ # Fetch to populate the cache
220
+ initial_result = parse_result(fetch_with_cache)
221
+ value(initial_result).must_equal(serialize_from_database)
222
+
223
+ # Destroy the shared child and its cache without invalidating the cached parent cache
224
+ root.update_columns(shared_id: nil)
225
+ shared.destroy!
226
+ shared_viewmodel_class.viewmodel_cache.clear
227
+
228
+ fetched_result = parse_result(fetch_with_cache)
229
+ value(fetched_result).must_equal(serialize_from_database)
230
+ fetched_view = fetched_result.first.first
231
+ value(fetched_view['shared']).must_be_nil
232
+ end
217
233
  end
218
234
 
219
235
  describe 'with migrations' do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: iknow_view_models
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.9.2
4
+ version: 3.10.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - iKnow Team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-04-23 00:00:00.000000000 Z
11
+ date: 2024-05-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actionpack
@@ -428,6 +428,7 @@ files:
428
428
  - lib/view_model/changes.rb
429
429
  - lib/view_model/config.rb
430
430
  - lib/view_model/controller.rb
431
+ - lib/view_model/controller/migration_versions.rb
431
432
  - lib/view_model/deserialization_error.rb
432
433
  - lib/view_model/deserialize_context.rb
433
434
  - lib/view_model/error.rb