iknow_view_models 3.1.8 → 3.2.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) 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 +17 -15
  20. data/lib/view_model/active_record/association_data.rb +2 -1
  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 +0 -1
  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 +14 -2
  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 +158 -145
  86. data/test/unit/view_model/registry_test.rb +38 -0
  87. data/test/unit/view_model/traversal_context_test.rb +4 -5
  88. data/test/unit/view_model_test.rb +18 -16
  89. metadata +38 -12
  90. data/.travis.yml +0 -31
  91. data/appveyor.yml +0 -22
  92. 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'
@@ -224,7 +224,7 @@ class ViewModel::ActiveRecord < ViewModel::Record
224
224
  DeepPreloader::Spec.new(association_specs)
225
225
  end
226
226
 
227
- def dependent_viewmodels(seen = Set.new, include_referenced: true)
227
+ def dependent_viewmodels(seen = Set.new, include_referenced: true, include_external: true)
228
228
  return if seen.include?(self)
229
229
 
230
230
  seen << self
@@ -232,6 +232,8 @@ 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
+ next unless include_external || !data.external?
236
+
235
237
  data.viewmodel_classes.each do |vm|
236
238
  vm.dependent_viewmodels(seen, include_referenced: include_referenced)
237
239
  end
@@ -240,12 +242,12 @@ class ViewModel::ActiveRecord < ViewModel::Record
240
242
  seen
241
243
  end
242
244
 
243
- def deep_schema_version(include_referenced: true)
245
+ def deep_schema_version(include_referenced: true, include_external: true)
244
246
  (@deep_schema_version ||= {})[include_referenced] ||=
245
247
  begin
246
- dependent_viewmodels(include_referenced: include_referenced).each_with_object({}) do |view, h|
248
+ dependent_viewmodels(include_referenced: include_referenced, include_external: include_external).each_with_object({}) do |view, h|
247
249
  h[view.view_name] = view.schema_version
248
- end
250
+ end.freeze
249
251
  end
250
252
  end
251
253
 
@@ -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