iknow_view_models 3.1.6 → 3.2.2

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.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +6 -6
  3. data/.rubocop.yml +18 -0
  4. data/Appraisals +6 -6
  5. data/Gemfile +6 -2
  6. data/Rakefile +5 -5
  7. data/gemfiles/rails_5_2.gemfile +5 -5
  8. data/gemfiles/rails_6_0.gemfile +9 -0
  9. data/iknow_view_models.gemspec +40 -38
  10. data/lib/iknow_view_models.rb +9 -7
  11. data/lib/iknow_view_models/version.rb +1 -1
  12. data/lib/view_model.rb +31 -17
  13. data/lib/view_model/access_control.rb +5 -2
  14. data/lib/view_model/access_control/composed.rb +10 -9
  15. data/lib/view_model/access_control/open.rb +2 -0
  16. data/lib/view_model/access_control/read_only.rb +2 -0
  17. data/lib/view_model/access_control/tree.rb +11 -6
  18. data/lib/view_model/access_control_error.rb +4 -1
  19. data/lib/view_model/active_record.rb +13 -12
  20. data/lib/view_model/active_record/association_data.rb +3 -2
  21. data/lib/view_model/active_record/association_manipulation.rb +6 -4
  22. data/lib/view_model/active_record/cache.rb +114 -34
  23. data/lib/view_model/active_record/cache/cacheable_view.rb +2 -2
  24. data/lib/view_model/active_record/collection_nested_controller.rb +3 -3
  25. data/lib/view_model/active_record/controller.rb +68 -1
  26. data/lib/view_model/active_record/controller_base.rb +4 -1
  27. data/lib/view_model/active_record/nested_controller_base.rb +1 -0
  28. data/lib/view_model/active_record/update_context.rb +8 -6
  29. data/lib/view_model/active_record/update_data.rb +32 -30
  30. data/lib/view_model/active_record/update_operation.rb +17 -13
  31. data/lib/view_model/active_record/visitor.rb +0 -1
  32. data/lib/view_model/after_transaction_runner.rb +2 -2
  33. data/lib/view_model/callbacks.rb +3 -1
  34. data/lib/view_model/controller.rb +13 -3
  35. data/lib/view_model/deserialization_error.rb +15 -12
  36. data/lib/view_model/error.rb +12 -10
  37. data/lib/view_model/error_view.rb +3 -1
  38. data/lib/view_model/migratable_view.rb +78 -0
  39. data/lib/view_model/migration.rb +48 -0
  40. data/lib/view_model/migration/no_path_error.rb +26 -0
  41. data/lib/view_model/migration/one_way_error.rb +24 -0
  42. data/lib/view_model/migration/unspecified_version_error.rb +24 -0
  43. data/lib/view_model/migrator.rb +108 -0
  44. data/lib/view_model/record.rb +15 -14
  45. data/lib/view_model/reference.rb +3 -1
  46. data/lib/view_model/references.rb +8 -5
  47. data/lib/view_model/registry.rb +1 -1
  48. data/lib/view_model/schemas.rb +9 -4
  49. data/lib/view_model/serialization_error.rb +4 -1
  50. data/lib/view_model/serialize_context.rb +4 -4
  51. data/lib/view_model/test_helpers.rb +8 -3
  52. data/lib/view_model/test_helpers/arvm_builder.rb +21 -15
  53. data/lib/view_model/traversal_context.rb +2 -1
  54. data/nix/dependencies.nix +5 -0
  55. data/nix/gem/generate.rb +2 -1
  56. data/shell.nix +8 -3
  57. data/test/.rubocop.yml +14 -0
  58. data/test/helpers/arvm_test_models.rb +12 -9
  59. data/test/helpers/arvm_test_utilities.rb +5 -3
  60. data/test/helpers/controller_test_helpers.rb +55 -32
  61. data/test/helpers/match_enumerator.rb +1 -0
  62. data/test/helpers/query_logging.rb +2 -1
  63. data/test/helpers/test_access_control.rb +5 -3
  64. data/test/helpers/viewmodel_spec_helpers.rb +88 -22
  65. data/test/unit/view_model/access_control_test.rb +144 -144
  66. data/test/unit/view_model/active_record/alias_test.rb +15 -13
  67. data/test/unit/view_model/active_record/belongs_to_test.rb +40 -39
  68. data/test/unit/view_model/active_record/cache_test.rb +68 -31
  69. data/test/unit/view_model/active_record/cloner_test.rb +67 -63
  70. data/test/unit/view_model/active_record/controller_test.rb +113 -65
  71. data/test/unit/view_model/active_record/counter_test.rb +10 -9
  72. data/test/unit/view_model/active_record/customization_test.rb +59 -58
  73. data/test/unit/view_model/active_record/has_many_test.rb +112 -111
  74. data/test/unit/view_model/active_record/has_many_through_poly_test.rb +15 -14
  75. data/test/unit/view_model/active_record/has_many_through_test.rb +33 -38
  76. data/test/unit/view_model/active_record/has_one_test.rb +37 -36
  77. data/test/unit/view_model/active_record/migration_test.rb +161 -0
  78. data/test/unit/view_model/active_record/namespacing_test.rb +19 -17
  79. data/test/unit/view_model/active_record/poly_test.rb +44 -45
  80. data/test/unit/view_model/active_record/shared_test.rb +30 -28
  81. data/test/unit/view_model/active_record/version_test.rb +9 -7
  82. data/test/unit/view_model/active_record_test.rb +72 -72
  83. data/test/unit/view_model/callbacks_test.rb +19 -15
  84. data/test/unit/view_model/controller_test.rb +4 -2
  85. data/test/unit/view_model/record_test.rb +92 -97
  86. data/test/unit/view_model/traversal_context_test.rb +4 -5
  87. data/test/unit/view_model_test.rb +18 -16
  88. metadata +36 -12
  89. data/.travis.yml +0 -31
  90. data/appveyor.yml +0 -22
  91. data/gemfiles/rails_6_0_beta.gemfile +0 -9
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class ViewModel::AccessControl::Open < ViewModel::AccessControl
2
4
  def visible_check(_traversal_env)
3
5
  ViewModel::AccessControl::Result::PERMIT
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class ViewModel::AccessControl::ReadOnly < ViewModel::AccessControl
2
4
  def visible_check(_traversal_env)
3
5
  ViewModel::AccessControl::Result::PERMIT
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  ## Defines an access control discipline for a given action against a tree of
2
4
  ## viewmodels.
3
5
  ##
@@ -78,7 +80,7 @@ class ViewModel::AccessControl::Tree < ViewModel::AccessControl
78
80
  def initialize
79
81
  super()
80
82
  @always_policy_instance = self.class::AlwaysPolicy.new(self)
81
- @view_policy_instances = self.class.view_policies.each_with_object({}) { |(name, policy), h| h[name] = policy.new(self) }
83
+ @view_policy_instances = self.class.view_policies.transform_values { |policy| policy.new(self) }
82
84
  @root_visibility_store = {}
83
85
  @root_editability_store = {}
84
86
  end
@@ -98,27 +100,29 @@ class ViewModel::AccessControl::Tree < ViewModel::AccessControl
98
100
 
99
101
  def store_descendent_editability(view, descendent_editability)
100
102
  if @root_editability_store.has_key?(view.object_id)
101
- raise RuntimeError.new("Root access control data already saved for root")
103
+ raise RuntimeError.new('Root access control data already saved for root')
102
104
  end
105
+
103
106
  @root_editability_store[view.object_id] = descendent_editability
104
107
  end
105
108
 
106
109
  def fetch_descendent_editability(view)
107
110
  @root_editability_store.fetch(view.object_id) do
108
- raise RuntimeError.new("No root access control data recorded for root")
111
+ raise RuntimeError.new('No root access control data recorded for root')
109
112
  end
110
113
  end
111
114
 
112
115
  def store_descendent_visibility(view, descendent_visibility)
113
116
  if @root_visibility_store.has_key?(view.object_id)
114
- raise RuntimeError.new("Root access control data already saved for root")
117
+ raise RuntimeError.new('Root access control data already saved for root')
115
118
  end
119
+
116
120
  @root_visibility_store[view.object_id] = descendent_visibility
117
121
  end
118
122
 
119
123
  def fetch_descendent_visibility(view)
120
124
  @root_visibility_store.fetch(view.object_id) do
121
- raise RuntimeError.new("No root access control data recorded for root")
125
+ raise RuntimeError.new('No root access control data recorded for root')
122
126
  end
123
127
  end
124
128
 
@@ -152,6 +156,7 @@ class ViewModel::AccessControl::Tree < ViewModel::AccessControl
152
156
 
153
157
  def initialize_as_node
154
158
  @root = false
159
+
155
160
  @root_children_editable_ifs = []
156
161
  @root_children_editable_unlesses = []
157
162
  @root_children_visible_ifs = []
@@ -187,7 +192,7 @@ class ViewModel::AccessControl::Tree < ViewModel::AccessControl
187
192
  def inspect_checks
188
193
  checks = super
189
194
  if root?
190
- checks << "no root checks"
195
+ checks << 'no root checks'
191
196
  else
192
197
  checks << "root_children_visible_if: #{root_children_visible_ifs.map(&:reason)}" if root_children_visible_ifs.present?
193
198
  checks << "root_children_visible_unless: #{root_children_visible_unlesses.map(&:reason)}" if root_children_visible_unlesses.present?
@@ -1,7 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class ViewModel::AccessControlError < ViewModel::AbstractErrorWithBlame
2
4
  attr_reader :detail
5
+
3
6
  status 403
4
- code "AccessControl.Forbidden"
7
+ code 'AccessControl.Forbidden'
5
8
 
6
9
  def initialize(detail, nodes = [])
7
10
  @detail = detail
@@ -1,23 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support"
4
- require "active_record"
3
+ require 'active_support'
4
+ require 'active_record'
5
5
 
6
- require "view_model"
7
- require "view_model/record"
6
+ require 'view_model'
7
+ require 'view_model/record'
8
8
 
9
- require "lazily"
10
- require "concurrent"
9
+ require 'lazily'
10
+ require 'concurrent'
11
11
 
12
12
  class ViewModel::ActiveRecord < ViewModel::Record
13
13
  # Defined before requiring components so components can refer to them at parse time
14
14
 
15
15
  # for functional updates
16
- FUNCTIONAL_UPDATE_TYPE = "_update"
17
- ACTIONS_ATTRIBUTE = "actions"
18
- VALUES_ATTRIBUTE = "values"
19
- BEFORE_ATTRIBUTE = "before"
20
- AFTER_ATTRIBUTE = "after"
16
+ FUNCTIONAL_UPDATE_TYPE = '_update'
17
+ ACTIONS_ATTRIBUTE = 'actions'
18
+ VALUES_ATTRIBUTE = 'values'
19
+ BEFORE_ATTRIBUTE = 'before'
20
+ AFTER_ATTRIBUTE = 'after'
21
21
 
22
22
  require 'view_model/utils/collections'
23
23
  require 'view_model/active_record/association_data'
@@ -232,6 +232,7 @@ class ViewModel::ActiveRecord < ViewModel::Record
232
232
  _members.each_value do |data|
233
233
  next unless data.is_a?(AssociationData)
234
234
  next unless include_referenced || !data.referenced?
235
+
235
236
  data.viewmodel_classes.each do |vm|
236
237
  vm.dependent_viewmodels(seen, include_referenced: include_referenced)
237
238
  end
@@ -245,7 +246,7 @@ class ViewModel::ActiveRecord < ViewModel::Record
245
246
  begin
246
247
  dependent_viewmodels(include_referenced: include_referenced).each_with_object({}) do |view, h|
247
248
  h[view.view_name] = view.schema_version
248
- end
249
+ end.freeze
249
250
  end
250
251
  end
251
252
 
@@ -17,7 +17,7 @@ class ViewModel::ActiveRecord::AssociationData
17
17
 
18
18
  @direct_reflection = owner.model_class.reflect_on_association(direct_association_name)
19
19
  if @direct_reflection.nil?
20
- raise InvalidAssociation.new("Association '#{direct_association_name}' not found in model '#{model_class.name}'")
20
+ raise InvalidAssociation.new("Association '#{direct_association_name}' not found in model '#{owner.model_class.name}'")
21
21
  end
22
22
 
23
23
  @indirect_association_name = indirect_association_name
@@ -42,7 +42,7 @@ class ViewModel::ActiveRecord::AssociationData
42
42
  @indirect_reflection = load_indirect_reflection(intermediate_model, @indirect_association_name)
43
43
  target_reflection = @indirect_reflection
44
44
  else
45
- target_reflection = @direct_reflection
45
+ target_reflection = @direct_reflection
46
46
  end
47
47
 
48
48
  @viewmodel_classes =
@@ -202,6 +202,7 @@ class ViewModel::ActiveRecord::AssociationData
202
202
 
203
203
  def direct_viewmodel
204
204
  raise ArgumentError.new('not a through association') unless through?
205
+
205
206
  lazy_initialize! unless @initialized
206
207
  @direct_viewmodel
207
208
  end
@@ -11,7 +11,8 @@ module ViewModel::ActiveRecord::AssociationManipulation
11
11
  association_scope = association.scope
12
12
 
13
13
  if association_data.through?
14
- raise ArgumentError.new("Polymorphic through relationships not supported yet") if association_data.polymorphic?
14
+ raise ArgumentError.new('Polymorphic through relationships not supported yet') if association_data.polymorphic?
15
+
15
16
  associated_viewmodel = association_data.viewmodel_class
16
17
  direct_viewmodel = association_data.direct_viewmodel
17
18
  else
@@ -44,6 +45,7 @@ module ViewModel::ActiveRecord::AssociationManipulation
44
45
  if vms.size > 1
45
46
  raise ViewModel::DeserializationError::Internal.new("Internal error: encountered multiple records for single association #{association_name}", self.blame_reference)
46
47
  end
48
+
47
49
  vms.first
48
50
  end
49
51
  end
@@ -112,7 +114,7 @@ module ViewModel::ActiveRecord::AssociationManipulation
112
114
  deserialize_context.run_callback(ViewModel::Callbacks::Hook::BeforeValidate, self)
113
115
 
114
116
  if association_data.through?
115
- raise ArgumentError.new("Polymorphic through relationships not supported yet") if association_data.polymorphic?
117
+ raise ArgumentError.new('Polymorphic through relationships not supported yet') if association_data.polymorphic?
116
118
 
117
119
  direct_viewmodel_class = association_data.direct_viewmodel
118
120
  root_update_data, referenced_update_data = construct_indirect_append_updates(association_data, subtree_hashes, references)
@@ -317,7 +319,7 @@ module ViewModel::ActiveRecord::AssociationManipulation
317
319
  # TODO: this won't handle polymorphic associations! In the case of polymorphism,
318
320
  # need to join on (type, id) pairs instead.
319
321
  if association_data.polymorphic?
320
- raise ArgumentError.new("Internal error: append_association is not yet supported for polymorphic indirect associations")
322
+ raise ArgumentError.new('Internal error: append_association is not yet supported for polymorphic indirect associations')
321
323
  end
322
324
 
323
325
  existing_indirect_associates = indirect_update_data.map { |upd| upd.id unless upd.new? }.compact
@@ -340,7 +342,7 @@ module ViewModel::ActiveRecord::AssociationManipulation
340
342
  ViewModel::ActiveRecord::UpdateData.new(
341
343
  direct_viewmodel_class,
342
344
  metadata,
343
- { indirect_reflection.name.to_s => { ViewModel::REFERENCE_ATTRIBUTE => ref_name }},
345
+ { indirect_reflection.name.to_s => { ViewModel::REFERENCE_ATTRIBUTE => ref_name } },
344
346
  [ref_name])
345
347
  end
346
348
 
@@ -5,6 +5,7 @@ require 'iknow_cache'
5
5
  # Cache for ViewModels that wrap ActiveRecord models.
6
6
  class ViewModel::ActiveRecord::Cache
7
7
  require 'view_model/active_record/cache/cacheable_view'
8
+ require 'view_model/migrator'
8
9
 
9
10
  class UncacheableViewModelError < RuntimeError; end
10
11
 
@@ -13,13 +14,20 @@ class ViewModel::ActiveRecord::Cache
13
14
  # If cache_group: is specified, it must be a group of a single key: `:id`
14
15
  def initialize(viewmodel_class, cache_group: nil)
15
16
  @viewmodel_class = viewmodel_class
16
- @cache_group = cache_group || create_default_cache_group # requires @viewmodel_class
17
+
18
+ @cache_group = cache_group || create_default_cache_group
19
+ @migrated_cache_group = @cache_group.register_child_group(:migrated, :version)
20
+
21
+ # /viewname/:id/viewname-currentversion
17
22
  @cache = @cache_group.register_cache(cache_name)
23
+
24
+ # /viewname/:id/migrated/:oldversion/viewname-currentversion
25
+ @migrated_cache = @migrated_cache_group.register_cache(cache_name)
18
26
  end
19
27
 
20
28
  def delete(*ids)
21
29
  ids.each do |id|
22
- @cache_group.delete_all(key_for(id))
30
+ @cache_group.delete_all(@cache.key.new(id))
23
31
  end
24
32
  end
25
33
 
@@ -27,14 +35,14 @@ class ViewModel::ActiveRecord::Cache
27
35
  @cache_group.invalidate_cache_group
28
36
  end
29
37
 
30
- def fetch_by_viewmodel(viewmodels, locked: false, serialize_context: @viewmodel_class.new_serialize_context)
38
+ def fetch_by_viewmodel(viewmodels, migration_versions: {}, locked: false, serialize_context: @viewmodel_class.new_serialize_context)
31
39
  ids = viewmodels.map(&:id)
32
- fetch(ids, initial_viewmodels: viewmodels, locked: false, serialize_context: serialize_context)
40
+ fetch(ids, initial_viewmodels: viewmodels, migration_versions: migration_versions, locked: locked, serialize_context: serialize_context)
33
41
  end
34
42
 
35
- def fetch(ids, initial_viewmodels: nil, locked: false, serialize_context: @viewmodel_class.new_serialize_context)
43
+ def fetch(ids, initial_viewmodels: nil, migration_versions: {}, locked: false, serialize_context: @viewmodel_class.new_serialize_context)
36
44
  data_serializations = Array.new(ids.size)
37
- worker = CacheWorker.new(serialize_context: serialize_context)
45
+ worker = CacheWorker.new(migration_versions: migration_versions, serialize_context: serialize_context)
38
46
 
39
47
  # If initial root viewmodels were provided, visit them to ensure that they
40
48
  # are visible. Other than this, no traversal callbacks are performed, as a
@@ -98,12 +106,14 @@ class ViewModel::ActiveRecord::Cache
98
106
  SENTINEL = Object.new
99
107
  WorklistEntry = Struct.new(:ref_name, :viewmodel)
100
108
 
101
- attr_reader :serialize_context, :resolved_references
109
+ attr_reader :migration_versions, :serialize_context, :resolved_references
102
110
 
103
- def initialize(serialize_context:)
104
- @worklist = {}
105
- @resolved_references = {}
106
- @serialize_context = serialize_context
111
+ def initialize(migration_versions:, serialize_context:)
112
+ @worklist = {}
113
+ @resolved_references = {}
114
+ @migration_versions = migration_versions
115
+ @migrated_cache_versions = {}
116
+ @serialize_context = serialize_context
107
117
  end
108
118
 
109
119
  def resolve_references!
@@ -141,11 +151,15 @@ class ViewModel::ActiveRecord::Cache
141
151
  end
142
152
  end
143
153
 
154
+ def migrated_cache_version(viewmodel_cache)
155
+ @migrated_cache_versions[viewmodel_cache] ||= viewmodel_cache.migrated_cache_version(migration_versions)
156
+ end
157
+
144
158
  # Loads the specified entities from the cache and returns a hash of
145
159
  # {id=>serialized_view}. Any references encountered are added to the
146
160
  # worklist.
147
161
  def load_from_cache(viewmodel_cache, ids)
148
- cached_serializations = viewmodel_cache.load(ids, serialize_context: serialize_context)
162
+ cached_serializations = viewmodel_cache.load(ids, migrated_cache_version(viewmodel_cache), serialize_context: serialize_context)
149
163
 
150
164
  cached_serializations.each_with_object({}) do |(id, cached_serialization), result|
151
165
  add_refs_to_worklist(cached_serialization[:ref_cache])
@@ -159,17 +173,54 @@ class ViewModel::ActiveRecord::Cache
159
173
  # added to the worklist.
160
174
  def serialize_and_cache(viewmodels)
161
175
  viewmodels.each_with_object({}) do |viewmodel, result|
162
- data_serialization = Jbuilder.encode do |json|
176
+ builder = Jbuilder.new do |json|
163
177
  ViewModel.serialize(viewmodel, json, serialize_context: serialize_context)
164
178
  end
165
179
 
166
180
  referenced_viewmodels = serialize_context.extract_referenced_views!
181
+
182
+ if migration_versions.present?
183
+ migrator = ViewModel::DownMigrator.new(migration_versions)
184
+
185
+ # This migration isn't given the chance to inspect/alter the contents
186
+ # of referenced views, only their presence: it's strictly less
187
+ # powerful than migrations on a fully serialized tree, as the only
188
+ # possible action on a referenced child is to delete it. The effect of
189
+ # this is that for sufficiently complex migrations where a parent view
190
+ # must introduce children or alter the contents of its referenced
191
+ # children, we may have to avoid caching while the migration is in
192
+ # use.
193
+ dummy_references = referenced_viewmodels.transform_values do |ref_vm|
194
+ {
195
+ ViewModel::TYPE_ATTRIBUTE => ref_vm.class.view_name,
196
+ ViewModel::VERSION_ATTRIBUTE => ref_vm.class.schema_version,
197
+ ViewModel::ID_ATTRIBUTE => ref_vm.id,
198
+ }.freeze
199
+ end
200
+
201
+ migrator.migrate!(builder.attributes!, references: dummy_references)
202
+
203
+ # Removed dummy references can be removed from referenced_viewmodels.
204
+ referenced_viewmodels.keep_if { |k, _| dummy_references.has_key?(k) }
205
+
206
+ # Introduced dummy references cannot be handled.
207
+ if dummy_references.keys != referenced_viewmodels.keys
208
+ version = migration_versions[viewmodel.class]
209
+ raise ViewModel::Error.new(
210
+ status: 500,
211
+ detail: "Down-migration for cacheable view #{viewmodel.class} to v#{version} must not introduce new shared references")
212
+ end
213
+ end
214
+
215
+ data_serialization = builder.target!
216
+
167
217
  add_viewmodels_to_worklist(referenced_viewmodels)
168
218
 
169
219
  if viewmodel.class < CacheableView
170
220
  cacheable_references = referenced_viewmodels.transform_values { |vm| cacheable_reference(vm) }
171
- viewmodel.class.viewmodel_cache.store(viewmodel.id, data_serialization, cacheable_references,
172
- serialize_context: serialize_context)
221
+ target_cache = viewmodel.class.viewmodel_cache
222
+ target_cache.store(viewmodel.id, migrated_cache_version(target_cache), data_serialization, cacheable_references,
223
+ serialize_context: serialize_context)
173
224
  end
174
225
 
175
226
  result[viewmodel.id] = data_serialization
@@ -192,14 +243,14 @@ class ViewModel::ActiveRecord::Cache
192
243
  if ids.present?
193
244
  found = viewmodel_class.find(ids,
194
245
  eager_include: false,
195
- lock: "FOR SHARE",
246
+ lock: 'FOR SHARE',
196
247
  serialize_context: serialize_context)
197
248
  viewmodels.concat(found)
198
249
  end
199
250
 
200
251
  ViewModel.preload_for_serialization(viewmodels,
201
252
  include_referenced: false,
202
- lock: "FOR SHARE",
253
+ lock: 'FOR SHARE',
203
254
  serialize_context: serialize_context)
204
255
 
205
256
  viewmodels
@@ -213,6 +264,7 @@ class ViewModel::ActiveRecord::Cache
213
264
  def add_refs_to_worklist(cacheable_references)
214
265
  cacheable_references.each do |ref_name, (type, id)|
215
266
  next if resolved_references.has_key?(ref_name)
267
+
216
268
  (@worklist[type] ||= {})[id] = WorklistEntry.new(ref_name, nil)
217
269
  end
218
270
  end
@@ -220,13 +272,26 @@ class ViewModel::ActiveRecord::Cache
220
272
  def add_viewmodels_to_worklist(referenced_viewmodels)
221
273
  referenced_viewmodels.each do |ref_name, viewmodel|
222
274
  next if resolved_references.has_key?(ref_name)
275
+
223
276
  (@worklist[viewmodel.class.view_name] ||= {})[viewmodel.id] = WorklistEntry.new(ref_name, viewmodel)
224
277
  end
225
278
  end
226
279
  end
227
280
 
228
- def key_for(id)
229
- cache.key.new(id)
281
+ def cache_for(migration_version)
282
+ if migration_version
283
+ @migrated_cache
284
+ else
285
+ @cache
286
+ end
287
+ end
288
+
289
+ def key_for(id, migration_version)
290
+ if migration_version
291
+ @migrated_cache.key.new(id, migration_version)
292
+ else
293
+ @cache.key.new(id)
294
+ end
230
295
  end
231
296
 
232
297
  def id_for(key)
@@ -234,32 +299,47 @@ class ViewModel::ActiveRecord::Cache
234
299
  end
235
300
 
236
301
  # Save the provided serialization and reference data in the cache
237
- def store(id, data_serialization, ref_cache, serialize_context:)
238
- cache.write(key_for(id), { data: data_serialization, ref_cache: ref_cache })
302
+ def store(id, migration_version, data_serialization, ref_cache, serialize_context:)
303
+ key = key_for(id, migration_version)
304
+ cache_for(migration_version).write(key, { data: data_serialization, ref_cache: ref_cache })
239
305
  end
240
306
 
241
- def load(ids, serialize_context:)
242
- keys = ids.map { |id| key_for(id) }
243
- results = cache.read_multi(keys)
307
+ def load(ids, migration_version, serialize_context:)
308
+ keys = ids.map { |id| key_for(id, migration_version) }
309
+ results = cache_for(migration_version).read_multi(keys)
244
310
  results.transform_keys! { |key| id_for(key) }
245
311
  end
246
312
 
247
- private
313
+ def cache_version
314
+ @cache_version ||=
315
+ begin
316
+ versions = @viewmodel_class.deep_schema_version(include_referenced: false)
317
+ ViewModel.schema_hash(versions)
318
+ end
319
+ end
248
320
 
249
- attr_reader :cache
321
+ def migrated_cache_version(migration_versions)
322
+ versions = ViewModel::Migrator.migrated_deep_schema_version(viewmodel_class, migration_versions, include_referenced: false)
323
+ version_hash = ViewModel.schema_hash(versions)
324
+
325
+ if version_hash == cache_version
326
+ # no migrations affect this view
327
+ nil
328
+ else
329
+ version_hash
330
+ end
331
+ end
332
+
333
+ private
250
334
 
251
335
  def create_default_cache_group
252
336
  IknowCache.register_group(@viewmodel_class.name, :id)
253
337
  end
254
338
 
255
- # Statically version the terminal cache based on the deep schema versions of
256
- # the constituent viewmodels, so that viewmodel changes force invalidation.
339
+ # Statically version the cache name based on the (current) deep schema
340
+ # versions of the constituent viewmodels, so that viewmodel changes force
341
+ # invalidation.
257
342
  def cache_name
258
- "#{@viewmodel_class.name}_#{cache_version}"
259
- end
260
-
261
- def cache_version
262
- version_string = @viewmodel_class.deep_schema_version(include_referenced: false).to_a.sort.join(',')
263
- Base64.urlsafe_encode64(Digest::MD5.digest(version_string))
343
+ "vmcache_#{cache_version}"
264
344
  end
265
345
  end