iknow_view_models 3.1.8 → 3.2.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: 5fc6b7d1f2e4c48c65f3f603d8e322afe7f26d52e08bb418dc746f9c5b6bd6c0
4
- data.tar.gz: 39eab5e4ae3423805b3c3d931504d08c623d8fbf602b58e4d978aeb7202aee71
3
+ metadata.gz: 8031b0e3cf50931cfdc65ee1c530efc4210566f2951ab5d6187f80d75848ac2a
4
+ data.tar.gz: bad21c087fff5def9362bd93576e0f9f0aeb8ec8ddc4f5ff6eb6849e879168c7
5
5
  SHA512:
6
- metadata.gz: 13f7e88bc34527984aea0d73518acb2e2fa8e2a6cc4cbb408f325afd39bfedd738be907f0922efcfacebe1802002e8f8d1c86ca9c37fc9a810df767f923d4455
7
- data.tar.gz: 71b8b322ce29db0b042c28b40c94820433d96beade6d991c5759a7828a7ce8101fcccc47fea3545538e522355fe949fd5a03493e26b22601c22e8bc31c8b2d92
6
+ metadata.gz: 17edcc7ab94fa8fa3a9b34e209a3d2feebb1c31fc1d3d619c36d3d9f602312e9ff13c1e0bc0974c2df070084b8b3fa7de3a2f7c74c38a3dbd04976ee166854aa
7
+ data.tar.gz: e176a0df288c029bea2d083c03492b6a5370e78a4c35164eaadf284b66de9120dfe60eda3dc6bca5e2845b71a865f4996fd17124462d06a70156249fd709a066
@@ -102,15 +102,15 @@ workflows:
102
102
  build:
103
103
  jobs:
104
104
  - test:
105
- name: 'ruby 2.6 rails 5.2 pg 11'
105
+ name: 'ruby 2.6 rails 5.2 pg 12'
106
106
  ruby-version: "2.6"
107
- pg-version: "11"
107
+ pg-version: "12"
108
108
  gemfile: gemfiles/rails_5_2.gemfile
109
109
  - test:
110
- name: 'ruby 2.6 rails 6.0 pg 11'
111
- ruby-version: "2.6"
112
- pg-version: "11"
113
- gemfile: gemfiles/rails_6_0_beta.gemfile
110
+ name: 'ruby 2.7 rails 6.0 pg 12'
111
+ ruby-version: "2.7"
112
+ pg-version: "12"
113
+ gemfile: gemfiles/rails_6_0.gemfile
114
114
  - publish:
115
115
  filters:
116
116
  branches:
@@ -0,0 +1,5 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.7
3
+
4
+ inherit_gem:
5
+ rubocop-iknow: rubocop.yml
data/Appraisals CHANGED
@@ -3,7 +3,7 @@ appraise "rails-5-2" do
3
3
  gem "activesupport", "~> 5.2.0"
4
4
  end
5
5
 
6
- appraise "rails-6-0-beta" do
7
- gem "activerecord", "~> 6.0.0.beta"
8
- gem "activesupport", "~> 6.0.0.beta"
6
+ appraise "rails-6-0" do
7
+ gem "activerecord", "~> 6.0.0"
8
+ gem "activesupport", "~> 6.0.0"
9
9
  end
data/Gemfile CHANGED
@@ -3,9 +3,13 @@ source 'https://rubygems.org'
3
3
  # Specify your gem's dependencies in cerego_view_models.gemspec
4
4
  gemspec
5
5
 
6
+ # Add our linter rules as a development dependency
7
+ gem 'rubocop'
8
+ gem 'rubocop-iknow'
9
+
6
10
  # Test metadata collection for circleci
7
11
  gem 'minitest-ci'
8
12
 
9
13
  # Override gemspec for development version preferences
10
- gem 'activerecord', '~> 5.2.0'
11
- gem 'activesupport', '~> 5.2.0'
14
+ gem 'activerecord', '~> 6.0.0'
15
+ gem 'activesupport', '~> 6.0.0'
@@ -3,7 +3,7 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "minitest-ci"
6
- gem "activerecord", "~> 6.0.0.beta"
7
- gem "activesupport", "~> 6.0.0.beta"
6
+ gem "activerecord", "~> 6.0.0"
7
+ gem "activesupport", "~> 6.0.0"
8
8
 
9
9
  gemspec path: "../"
@@ -36,13 +36,14 @@ Gem::Specification.new do |spec|
36
36
  spec.add_dependency "lazily"
37
37
  spec.add_dependency "renum"
38
38
  spec.add_dependency "oj"
39
+ spec.add_dependency "rgl"
39
40
 
40
41
  spec.add_development_dependency "appraisal"
41
42
  spec.add_development_dependency "bundler"
42
43
  spec.add_development_dependency "byebug"
43
44
  spec.add_development_dependency "method_source"
44
45
  spec.add_development_dependency "minitest-hooks"
45
- spec.add_development_dependency "pg", '~> 0.18' # As of 5.1.4, Rails runtime check excludes pg 1.x, see #31669
46
+ spec.add_development_dependency "pg"
46
47
  spec.add_development_dependency "pry"
47
48
  spec.add_development_dependency "rake"
48
49
  spec.add_development_dependency "rspec-expectations"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module IknowViewModels
4
- VERSION = '3.1.8'
4
+ VERSION = '3.2.0'
5
5
  end
@@ -12,7 +12,12 @@ class ViewModel
12
12
  VERSION_ATTRIBUTE = "_version"
13
13
  NEW_ATTRIBUTE = "_new"
14
14
 
15
- Metadata = Struct.new(:id, :view_name, :schema_version, :new) do
15
+ # Migrations leave a metadata attribute _migrated on any views that they
16
+ # alter. This attribute is accessible as metadata when deserializing migrated
17
+ # input, and is included in the output serialization sent to clients.
18
+ MIGRATED_ATTRIBUTE = "_migrated"
19
+
20
+ Metadata = Struct.new(:id, :view_name, :schema_version, :new, :migrated) do
16
21
  alias :new? :new
17
22
  end
18
23
 
@@ -99,8 +104,9 @@ class ViewModel
99
104
  type_name = hash.delete(ViewModel::TYPE_ATTRIBUTE)
100
105
  schema_version = hash.delete(ViewModel::VERSION_ATTRIBUTE)
101
106
  new = hash.delete(ViewModel::NEW_ATTRIBUTE) { false }
107
+ migrated = hash.delete(ViewModel::MIGRATED_ATTRIBUTE) { false }
102
108
 
103
- Metadata.new(id, type_name, schema_version, new)
109
+ Metadata.new(id, type_name, schema_version, new, migrated)
104
110
  end
105
111
 
106
112
  def extract_reference_only_metadata(hash)
@@ -108,7 +114,7 @@ class ViewModel
108
114
  id = hash.delete(ViewModel::ID_ATTRIBUTE)
109
115
  type_name = hash.delete(ViewModel::TYPE_ATTRIBUTE)
110
116
 
111
- Metadata.new(id, type_name, nil, false)
117
+ Metadata.new(id, type_name, nil, false, false)
112
118
  end
113
119
 
114
120
  def extract_reference_metadata(hash)
@@ -234,6 +240,11 @@ class ViewModel
234
240
  schema_version == self.schema_version
235
241
  end
236
242
 
243
+ def schema_hash(schema_versions)
244
+ version_string = schema_versions.to_a.sort.join(',')
245
+ Base64.urlsafe_encode64(Digest::MD5.digest(version_string))
246
+ end
247
+
237
248
  def preload_for_serialization(viewmodels, serialize_context: new_serialize_context, include_referenced: true, lock: nil)
238
249
  Array.wrap(viewmodels).group_by(&:class).each do |type, views|
239
250
  DeepPreloader.preload(views.map(&:model),
@@ -245,7 +245,7 @@ class ViewModel::ActiveRecord < ViewModel::Record
245
245
  begin
246
246
  dependent_viewmodels(include_referenced: include_referenced).each_with_object({}) do |view, h|
247
247
  h[view.view_name] = view.schema_version
248
- end
248
+ end.freeze
249
249
  end
250
250
  end
251
251
 
@@ -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
@@ -225,8 +276,20 @@ class ViewModel::ActiveRecord::Cache
225
276
  end
226
277
  end
227
278
 
228
- def key_for(id)
229
- cache.key.new(id)
279
+ def cache_for(migration_version)
280
+ if migration_version
281
+ @migrated_cache
282
+ else
283
+ @cache
284
+ end
285
+ end
286
+
287
+ def key_for(id, migration_version)
288
+ if migration_version
289
+ @migrated_cache.key.new(id, migration_version)
290
+ else
291
+ @cache.key.new(id)
292
+ end
230
293
  end
231
294
 
232
295
  def id_for(key)
@@ -234,32 +297,47 @@ class ViewModel::ActiveRecord::Cache
234
297
  end
235
298
 
236
299
  # 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 })
300
+ def store(id, migration_version, data_serialization, ref_cache, serialize_context:)
301
+ key = key_for(id, migration_version)
302
+ cache_for(migration_version).write(key, { data: data_serialization, ref_cache: ref_cache })
239
303
  end
240
304
 
241
- def load(ids, serialize_context:)
242
- keys = ids.map { |id| key_for(id) }
243
- results = cache.read_multi(keys)
305
+ def load(ids, migration_version, serialize_context:)
306
+ keys = ids.map { |id| key_for(id, migration_version) }
307
+ results = cache_for(migration_version).read_multi(keys)
244
308
  results.transform_keys! { |key| id_for(key) }
245
309
  end
246
310
 
247
- private
311
+ def cache_version
312
+ @cache_version ||=
313
+ begin
314
+ versions = @viewmodel_class.deep_schema_version(include_referenced: false)
315
+ ViewModel.schema_hash(versions)
316
+ end
317
+ end
248
318
 
249
- attr_reader :cache
319
+ def migrated_cache_version(migration_versions)
320
+ versions = ViewModel::Migrator.migrated_deep_schema_version(viewmodel_class, migration_versions, include_referenced: false)
321
+ version_hash = ViewModel.schema_hash(versions)
322
+
323
+ if version_hash == cache_version
324
+ # no migrations affect this view
325
+ nil
326
+ else
327
+ version_hash
328
+ end
329
+ end
330
+
331
+ private
250
332
 
251
333
  def create_default_cache_group
252
334
  IknowCache.register_group(@viewmodel_class.name, :id)
253
335
  end
254
336
 
255
- # Statically version the terminal cache based on the deep schema versions of
256
- # the constituent viewmodels, so that viewmodel changes force invalidation.
337
+ # Statically version the cache name based on the (current) deep schema
338
+ # versions of the constituent viewmodels, so that viewmodel changes force
339
+ # invalidation.
257
340
  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))
341
+ "vmcache_#{cache_version}"
264
342
  end
265
343
  end
@@ -40,10 +40,10 @@ module ViewModel::ActiveRecord::Cache::CacheableView
40
40
  @viewmodel_cache
41
41
  end
42
42
 
43
- def serialize_from_cache(views, serialize_context:)
43
+ def serialize_from_cache(views, migration_versions: {}, locked: false, serialize_context:)
44
44
  plural = views.is_a?(Array)
45
45
  views = Array.wrap(views)
46
- json_views, json_refs = viewmodel_cache.fetch_by_viewmodel(views, serialize_context: serialize_context)
46
+ json_views, json_refs = viewmodel_cache.fetch_by_viewmodel(views, locked: locked, migration_versions: migration_versions, serialize_context: serialize_context)
47
47
  json_views = json_views.first unless plural
48
48
  return json_views, json_refs
49
49
  end
@@ -62,7 +62,29 @@ module ViewModel::ActiveRecord::Controller
62
62
  end
63
63
 
64
64
  included do
65
- etag { self.viewmodel_class.deep_schema_version }
65
+ etag { migrated_deep_schema_version }
66
+ end
67
+
68
+ def parse_viewmodel_updates
69
+ super.tap do |update_hash, refs|
70
+ if migration_versions.present?
71
+ migrator = ViewModel::UpMigrator.new(migration_versions)
72
+ migrator.migrate!([update_hash, refs], references: refs)
73
+ end
74
+ end
75
+ end
76
+
77
+ def prerender_viewmodel(*)
78
+ super do |jbuilder|
79
+ yield(jbuilder) if block_given?
80
+
81
+ # migrate the resulting structure before it's serialized to a json string
82
+ if migration_versions.present?
83
+ tree = jbuilder.attributes!
84
+ migrator = ViewModel::DownMigrator.new(migration_versions)
85
+ migrator.migrate!(tree, references: tree['references'])
86
+ end
87
+ end
66
88
  end
67
89
 
68
90
  private
@@ -70,4 +92,34 @@ module ViewModel::ActiveRecord::Controller
70
92
  def viewmodel_id
71
93
  parse_param(:id)
72
94
  end
95
+
96
+ def migration_versions
97
+ @migration_versions ||=
98
+ begin
99
+ versions = parse_param(
100
+ :versions,
101
+ default: {},
102
+ with: IknowParams::Serializer::HashOf.new(
103
+ IknowParams::Serializer::String, IknowParams::Serializer::Integer))
104
+
105
+ migration_versions = {}
106
+
107
+ versions.each do |view_name, required_version|
108
+ viewmodel_class = ViewModel::Registry.for_view_name(view_name)
109
+
110
+ if viewmodel_class.schema_version != required_version
111
+ migration_versions[viewmodel_class] = required_version
112
+ end
113
+ rescue ViewModel::DeserializationError::UnknownView
114
+ # Ignore requests to migrate types that no longer exist
115
+ next
116
+ end
117
+
118
+ migration_versions.freeze
119
+ end
120
+ end
121
+
122
+ def migrated_deep_schema_version
123
+ ViewModel::Migrator.migrated_deep_schema_version(viewmodel_class, migration_versions, include_referenced: true)
124
+ end
73
125
  end