iknow_view_models 3.9.2 → 3.10.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 +4 -4
- data/lib/iknow_view_models/version.rb +1 -1
- data/lib/view_model/active_record/cache.rb +37 -7
- data/lib/view_model/active_record/controller.rb +2 -47
- data/lib/view_model/controller/migration_versions.rb +52 -0
- data/test/unit/view_model/active_record/cache_test.rb +17 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 417eb3615156fa48cf438ead03e2ea33e323c5baa7d411b1e0fafcea5050acb1
|
4
|
+
data.tar.gz: cdede3ad865389a0d006725819595aec6da1ea417e9d061452ef3198a39af3d8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ada26778b54ffb60a825d408fe40f05c8a4aa93cbac7b5519b2338b5afb4cadfda0d1eb7b359b65584f88fbb9cadea4a35a7d2ba0aad9fb8e4d8ec9d5f894e03
|
7
|
+
data.tar.gz: b70d2b42d4a599c3d284dbb96c4506c8ff7b6104108ebcb7126c554fcd671eb0bad04dc4df5a1006f76af272a71c5ca46f54736bf4961821dd7f22160d46fada
|
@@ -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
|
-
|
34
|
-
|
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 =
|
185
|
-
|
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(
|
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.
|
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-
|
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
|