iknow_view_models 3.10.0 → 3.11.0

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: 85aa5f3dd882b989e6a4d1fa8d5d89be347f5f3c215af86851f1ef086d583472
4
- data.tar.gz: d202810ca019d425b0de503b7a077d164d5919dc9206cf069e93b37ddf432334
3
+ metadata.gz: 40eb8cc78ae998113eac37ef9cd944ecefc1c4b0a71a492944ac88ff2bfa4077
4
+ data.tar.gz: cb7d430a809687598e365e54a0312e97fc6ee1938cfdd1c4f9f1bd924e8364a5
5
5
  SHA512:
6
- metadata.gz: 3005ecb2044fd83003ccd444fb2b22d9787b74c929f157573baa084d4d3dff1b2f24d86e30f1ee9bd1e03604a4a0f9a57ec7431cadaea28faa45eda235dbcd28
7
- data.tar.gz: 82971047b73442cbccaf9e3e80f1386dc373329b91f94260f2daeeb5794f99b3502cc47b911a62da38f7d0932b9fea49ae72319de9fb2d79f8be6035e0a34110
6
+ metadata.gz: dd1c4215f6e9bafaf68c618d4ba59541089016d4dcea45ab90892489cc6ada0183f95a6d07ae88899fc54d5a9bf37a3619e70bc62c54ed9c5b9de57eeec5f37f
7
+ data.tar.gz: e1cd93bf28d4ad96ac347effeb96d9bbbd6a1ec9a09944091d8127555b1c22ca2e97bfa7f5ce670f86cf42647b06d3e1da13a842935eb0bd39e44c303af666ab
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module IknowViewModels
4
- VERSION = '3.10.0'
4
+ VERSION = '3.11.0'
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|
@@ -56,9 +56,14 @@ module ViewModel::ActiveRecord::Controller
56
56
  end
57
57
 
58
58
  def destroy(serialize_context: new_serialize_context, deserialize_context: new_deserialize_context)
59
+ viewmodel_ids = parse_param(
60
+ :id, with: IknowParams::Serializer::ArrayOf.new(ViewmodelIdSerializer, allow_singleton: true))
61
+
59
62
  viewmodel_class.transaction do
60
- view = viewmodel_class.find(viewmodel_id, eager_include: false)
61
- view.destroy!(deserialize_context: deserialize_context)
63
+ views = viewmodel_class.find(viewmodel_ids, eager_include: false)
64
+ views.each do |view|
65
+ view.destroy!(deserialize_context: deserialize_context)
66
+ end
62
67
  end
63
68
  render_viewmodel(nil)
64
69
  end
@@ -91,8 +96,28 @@ module ViewModel::ActiveRecord::Controller
91
96
 
92
97
  private
93
98
 
99
+ # Viewmodel ids are permitted to be either integers or strings
100
+ class ViewmodelIdSerializer < IknowParams::Serializer
101
+ def initialize
102
+ super(::Object)
103
+ end
104
+
105
+ def load(val)
106
+ case val
107
+ when ::Integer, ::String
108
+ val
109
+ else
110
+ raise IknowParams::Serializer::LoadError.new(
111
+ "Incorrect type for #{self.class.name}: #{val.inspect}:#{val.class.name}")
112
+ end
113
+ end
114
+
115
+ set_singleton!
116
+ json_value!
117
+ end
118
+
94
119
  def viewmodel_id
95
- parse_param(:id)
120
+ parse_param(:id, with: ViewmodelIdSerializer)
96
121
  end
97
122
 
98
123
  def migrated_deep_schema_version
@@ -156,11 +156,13 @@ module ActionDispatch
156
156
  name_route = { as: '' } # Only one route may take the name
157
157
  post('', action: :create, **name_route.extract!(:as)) unless except.include?(:create) || !add_shallow_routes
158
158
  get('', action: :index, **name_route.extract!(:as)) unless except.include?(:index) || !add_shallow_routes
159
+ delete('', action: :destroy, as: :bulk_delete) unless except.include?(:destroy) || !add_shallow_routes
159
160
  end
160
161
  end
161
162
  else
162
163
  collection do
163
164
  get('', action: :index, as: '') unless except.include?(:index)
165
+ delete('', action: :destroy, as: :bulk_delete) unless except.include?(:destroy)
164
166
  end
165
167
  end
166
168
  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
@@ -167,6 +167,22 @@ class ViewModel::ActiveRecord::ControllerTest < ActiveSupport::TestCase
167
167
  end
168
168
 
169
169
  def test_destroy
170
+ other_parent = make_parent
171
+ parentcontroller = ParentController.new(params: { id: [@parent.id, other_parent.id] })
172
+ parentcontroller.invoke(:destroy)
173
+
174
+ assert_equal(200, parentcontroller.status)
175
+
176
+ assert(Parent.where(id: @parent.id).blank?, "record doesn't exist after delete")
177
+ assert(Parent.where(id: other_parent.id).blank?, "record doesn't exist after delete")
178
+
179
+ assert_equal({ 'data' => nil },
180
+ parentcontroller.hash_response)
181
+
182
+ assert_all_hooks_nested_inside_parent_hook(parentcontroller.hook_trace)
183
+ end
184
+
185
+ def test_batch_destroy
170
186
  parentcontroller = ParentController.new(params: { id: @parent.id })
171
187
  parentcontroller.invoke(:destroy)
172
188
 
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.10.0
4
+ version: 3.11.0
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-07-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actionpack