iknow_view_models 3.2.8 → 3.2.13
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.rb +1 -0
- data/lib/view_model/active_record.rb +16 -8
- data/lib/view_model/active_record/cache.rb +8 -9
- data/lib/view_model/active_record/controller.rb +2 -2
- data/lib/view_model/garbage_collection.rb +43 -0
- data/lib/view_model/migratable_view.rb +4 -1
- data/lib/view_model/migration/no_path_error.rb +1 -0
- data/lib/view_model/migration/one_way_error.rb +1 -0
- data/lib/view_model/migration/unspecified_version_error.rb +1 -0
- data/lib/view_model/migrator.rb +17 -5
- data/test/helpers/viewmodel_spec_helpers.rb +9 -3
- data/test/unit/view_model/active_record/cache_test.rb +1 -1
- data/test/unit/view_model/active_record/has_many_through_test.rb +4 -2
- data/test/unit/view_model/active_record/migration_test.rb +213 -93
- data/test/unit/view_model/active_record_test.rb +5 -0
- data/test/unit/view_model/garbage_collection_test.rb +80 -0
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3991314cdb010347b417869ff3e2e22b0e7adb4ab590e25739cb6b12bf49a219
|
|
4
|
+
data.tar.gz: 0cd5994d07bdd9b9d311732be56b01a51d7b4f1eb71f7fa1d0217bf71911b97f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a5cd70dd6f79db33a97dbdfb05eefec460c7e332784839ad461cf5a359307ce8abedb39c046d42d04a3b7355280d1ad54948cb6cc61fdc35effa0f684ea4c821
|
|
7
|
+
data.tar.gz: 6a673b6387892588434f981b57f8131e9c7a97991cc16d3901e72f043b9429efd0fef7a8e1f44cb0ea1ee364256d63d48f198a10e270b487d3da61311780eec7
|
data/lib/view_model.rb
CHANGED
|
@@ -182,9 +182,14 @@ class ViewModel::ActiveRecord < ViewModel::Record
|
|
|
182
182
|
end
|
|
183
183
|
|
|
184
184
|
# Constructs a preload specification of the required models for
|
|
185
|
-
# serializing/deserializing this view.
|
|
186
|
-
|
|
185
|
+
# serializing/deserializing this view. Cycles in the schema will be broken
|
|
186
|
+
# after two layers of eager loading.
|
|
187
|
+
def eager_includes(serialize_context: new_serialize_context, include_referenced: true, vm_path: [])
|
|
187
188
|
association_specs = {}
|
|
189
|
+
|
|
190
|
+
return nil if vm_path.count(self) > 2
|
|
191
|
+
|
|
192
|
+
child_path = vm_path + [self]
|
|
188
193
|
_members.each do |assoc_name, association_data|
|
|
189
194
|
next unless association_data.is_a?(AssociationData)
|
|
190
195
|
next if association_data.external?
|
|
@@ -201,7 +206,7 @@ class ViewModel::ActiveRecord < ViewModel::Record
|
|
|
201
206
|
case
|
|
202
207
|
when association_data.through?
|
|
203
208
|
viewmodel = association_data.direct_viewmodel
|
|
204
|
-
children = viewmodel.eager_includes(serialize_context: child_context, include_referenced: include_referenced)
|
|
209
|
+
children = viewmodel.eager_includes(serialize_context: child_context, include_referenced: include_referenced, vm_path: child_path)
|
|
205
210
|
|
|
206
211
|
when !include_referenced && association_data.referenced?
|
|
207
212
|
children = nil # Load up to the root viewmodel, but no further
|
|
@@ -210,13 +215,13 @@ class ViewModel::ActiveRecord < ViewModel::Record
|
|
|
210
215
|
children_by_klass = {}
|
|
211
216
|
association_data.viewmodel_classes.each do |vm_class|
|
|
212
217
|
klass = vm_class.model_class.name
|
|
213
|
-
children_by_klass[klass] = vm_class.eager_includes(serialize_context: child_context, include_referenced: include_referenced)
|
|
218
|
+
children_by_klass[klass] = vm_class.eager_includes(serialize_context: child_context, include_referenced: include_referenced, vm_path: child_path)
|
|
214
219
|
end
|
|
215
220
|
children = DeepPreloader::PolymorphicSpec.new(children_by_klass)
|
|
216
221
|
|
|
217
222
|
else
|
|
218
223
|
viewmodel = association_data.viewmodel_class
|
|
219
|
-
children = viewmodel.eager_includes(serialize_context: child_context, include_referenced: include_referenced)
|
|
224
|
+
children = viewmodel.eager_includes(serialize_context: child_context, include_referenced: include_referenced, vm_path: child_path)
|
|
220
225
|
end
|
|
221
226
|
|
|
222
227
|
association_specs[association_data.direct_reflection.name.to_s] = children
|
|
@@ -340,12 +345,15 @@ class ViewModel::ActiveRecord < ViewModel::Record
|
|
|
340
345
|
|
|
341
346
|
case
|
|
342
347
|
when association_data.through?
|
|
343
|
-
# associated here are
|
|
348
|
+
# associated here are join-table models; we need to get the far side out
|
|
349
|
+
join_models = associated
|
|
350
|
+
|
|
344
351
|
if association_data.direct_viewmodel._list_member?
|
|
345
|
-
|
|
352
|
+
attr = association_data.direct_viewmodel._list_attribute_name
|
|
353
|
+
join_models = join_models.sort_by { |j| j[attr] }
|
|
346
354
|
end
|
|
347
355
|
|
|
348
|
-
|
|
356
|
+
join_models.map do |through_model|
|
|
349
357
|
model = through_model.public_send(association_data.indirect_reflection.name)
|
|
350
358
|
association_data.viewmodel_class_for_model!(model.class).new(model)
|
|
351
359
|
end
|
|
@@ -177,28 +177,27 @@ class ViewModel::ActiveRecord::Cache
|
|
|
177
177
|
ViewModel.serialize(viewmodel, json, serialize_context: serialize_context)
|
|
178
178
|
end
|
|
179
179
|
|
|
180
|
+
# viewmodels referenced from roots
|
|
180
181
|
referenced_viewmodels = serialize_context.extract_referenced_views!
|
|
181
182
|
|
|
182
183
|
if migration_versions.present?
|
|
183
184
|
migrator = ViewModel::DownMigrator.new(migration_versions)
|
|
184
185
|
|
|
185
|
-
# This migration isn't
|
|
186
|
-
#
|
|
187
|
-
#
|
|
188
|
-
#
|
|
189
|
-
#
|
|
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.
|
|
186
|
+
# This migration isn't able to affect the contents of referenced
|
|
187
|
+
# views, only their presence. The references will be themselves
|
|
188
|
+
# rendered (and migrated) independently later. We mark the dummy
|
|
189
|
+
# references provided to exclude their partial contents from being
|
|
190
|
+
# themselves migrated.
|
|
193
191
|
dummy_references = referenced_viewmodels.transform_values do |ref_vm|
|
|
194
192
|
{
|
|
195
193
|
ViewModel::TYPE_ATTRIBUTE => ref_vm.class.view_name,
|
|
196
194
|
ViewModel::VERSION_ATTRIBUTE => ref_vm.class.schema_version,
|
|
197
195
|
ViewModel::ID_ATTRIBUTE => ref_vm.id,
|
|
196
|
+
ViewModel::Migrator::EXCLUDE_FROM_MIGRATION => true,
|
|
198
197
|
}.freeze
|
|
199
198
|
end
|
|
200
199
|
|
|
201
|
-
migrator.migrate!(builder.attributes!, references
|
|
200
|
+
migrator.migrate!({ 'data' => builder.attributes!, 'references' => dummy_references })
|
|
202
201
|
|
|
203
202
|
# Removed dummy references can be removed from referenced_viewmodels.
|
|
204
203
|
referenced_viewmodels.keep_if { |k, _| dummy_references.has_key?(k) }
|
|
@@ -71,7 +71,7 @@ module ViewModel::ActiveRecord::Controller
|
|
|
71
71
|
super.tap do |update_hash, refs|
|
|
72
72
|
if migration_versions.present?
|
|
73
73
|
migrator = ViewModel::UpMigrator.new(migration_versions)
|
|
74
|
-
migrator.migrate!(
|
|
74
|
+
migrator.migrate!({ 'data' => update_hash, 'references' => refs })
|
|
75
75
|
end
|
|
76
76
|
end
|
|
77
77
|
end
|
|
@@ -84,7 +84,7 @@ module ViewModel::ActiveRecord::Controller
|
|
|
84
84
|
if migration_versions.present?
|
|
85
85
|
tree = jbuilder.attributes!
|
|
86
86
|
migrator = ViewModel::DownMigrator.new(migration_versions)
|
|
87
|
-
migrator.migrate!(tree
|
|
87
|
+
migrator.migrate!(tree)
|
|
88
88
|
end
|
|
89
89
|
end
|
|
90
90
|
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class ViewModel::GarbageCollection
|
|
4
|
+
class << self
|
|
5
|
+
def garbage_collect_references!(serialization)
|
|
6
|
+
return unless serialization.has_key?('references')
|
|
7
|
+
|
|
8
|
+
roots = serialization.except('references')
|
|
9
|
+
references = serialization['references']
|
|
10
|
+
|
|
11
|
+
worklist = Set.new(collect_references(roots))
|
|
12
|
+
visited = Set.new
|
|
13
|
+
|
|
14
|
+
while (live = worklist.first)
|
|
15
|
+
worklist.delete(live)
|
|
16
|
+
visited << live
|
|
17
|
+
collect_references(references[live]) do |ref|
|
|
18
|
+
worklist << ref unless visited.include?(ref)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
references.keep_if { |ref, _val| visited.include?(ref) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
## yield each reference encountered in tree
|
|
28
|
+
def collect_references(tree, &block)
|
|
29
|
+
return enum_for(__method__, tree) unless block_given?
|
|
30
|
+
|
|
31
|
+
case tree
|
|
32
|
+
when Hash
|
|
33
|
+
if tree.size == 1 && (ref = tree[ViewModel::REFERENCE_ATTRIBUTE])
|
|
34
|
+
block.(ref)
|
|
35
|
+
else
|
|
36
|
+
tree.each_value { |t| collect_references(t, &block) }
|
|
37
|
+
end
|
|
38
|
+
when Array
|
|
39
|
+
tree.each { |t| collect_references(t, &block) }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -52,6 +52,9 @@ module ViewModel::MigratableView
|
|
|
52
52
|
|
|
53
53
|
graph = RGL::DirectedAdjacencyGraph.new
|
|
54
54
|
|
|
55
|
+
# Add a vertex for the current version, in case no edges reach it
|
|
56
|
+
graph.add_vertex(self.schema_version)
|
|
57
|
+
|
|
55
58
|
# Add edges backwards, as we care about paths from the latest version
|
|
56
59
|
@migration_classes.each_key do |from, to|
|
|
57
60
|
graph.add_edge(to, from)
|
|
@@ -60,7 +63,7 @@ module ViewModel::MigratableView
|
|
|
60
63
|
paths = graph.dijkstra_shortest_paths(Hash.new { 1 }, self.schema_version)
|
|
61
64
|
|
|
62
65
|
paths.each do |target_version, path|
|
|
63
|
-
next if path.length == 1
|
|
66
|
+
next if path.nil? || path.length == 1
|
|
64
67
|
|
|
65
68
|
# Store the path forwards rather than backwards
|
|
66
69
|
path_migration_classes = path.reverse.each_cons(2).map do |from, to|
|
data/lib/view_model/migrator.rb
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
class ViewModel
|
|
4
4
|
class Migrator
|
|
5
|
+
EXCLUDE_FROM_MIGRATION = '_exclude_from_migration'
|
|
6
|
+
|
|
5
7
|
class << self
|
|
6
8
|
def migrated_deep_schema_version(viewmodel_class, required_versions, include_referenced: true)
|
|
7
9
|
deep_schema_version = viewmodel_class.deep_schema_version(include_referenced: include_referenced)
|
|
@@ -34,27 +36,37 @@ class ViewModel
|
|
|
34
36
|
end
|
|
35
37
|
end
|
|
36
38
|
|
|
37
|
-
def migrate!(
|
|
39
|
+
def migrate!(serialization)
|
|
40
|
+
migrate_tree!(serialization, references: serialization['references'] || {})
|
|
41
|
+
GarbageCollection.garbage_collect_references!(serialization)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def migrate_tree!(node, references:)
|
|
38
47
|
case node
|
|
39
48
|
when Hash
|
|
40
49
|
if (type = node[ViewModel::TYPE_ATTRIBUTE])
|
|
41
50
|
version = node[ViewModel::VERSION_ATTRIBUTE]
|
|
42
51
|
|
|
52
|
+
# We allow subtrees to be excluded from migration. This is used
|
|
53
|
+
# internally to permit stub references that are not a full
|
|
54
|
+
# serialization of the referenced type: see ViewModel::Cache.
|
|
55
|
+
return if node[EXCLUDE_FROM_MIGRATION]
|
|
56
|
+
|
|
43
57
|
if migrate_viewmodel!(type, version, node, references)
|
|
44
58
|
node[ViewModel::MIGRATED_ATTRIBUTE] = true
|
|
45
59
|
end
|
|
46
60
|
end
|
|
47
61
|
|
|
48
62
|
node.each_value do |child|
|
|
49
|
-
|
|
63
|
+
migrate_tree!(child, references: references)
|
|
50
64
|
end
|
|
51
65
|
when Array
|
|
52
|
-
node.each { |child|
|
|
66
|
+
node.each { |child| migrate_tree!(child, references: references) }
|
|
53
67
|
end
|
|
54
68
|
end
|
|
55
69
|
|
|
56
|
-
private
|
|
57
|
-
|
|
58
70
|
def migrate_viewmodel!(_view_name, _version, _view_hash, _references)
|
|
59
71
|
raise RuntimeError.new('abstract method')
|
|
60
72
|
end
|
|
@@ -141,10 +141,10 @@ module ViewModelSpecHelpers
|
|
|
141
141
|
end
|
|
142
142
|
end
|
|
143
143
|
|
|
144
|
-
module
|
|
144
|
+
module ParentAndChildMigrations
|
|
145
145
|
extend ActiveSupport::Concern
|
|
146
|
-
|
|
147
|
-
|
|
146
|
+
|
|
147
|
+
def model_attributes
|
|
148
148
|
super.merge(
|
|
149
149
|
schema: ->(t) { t.integer :new_field, default: 1, null: false },
|
|
150
150
|
viewmodel: ->(_v) {
|
|
@@ -206,6 +206,12 @@ module ViewModelSpecHelpers
|
|
|
206
206
|
end
|
|
207
207
|
end
|
|
208
208
|
|
|
209
|
+
module ParentAndBelongsToChildWithMigration
|
|
210
|
+
extend ActiveSupport::Concern
|
|
211
|
+
include ViewModelSpecHelpers::ParentAndBelongsToChild
|
|
212
|
+
include ViewModelSpecHelpers::ParentAndChildMigrations
|
|
213
|
+
end
|
|
214
|
+
|
|
209
215
|
module ParentAndSharedBelongsToChild
|
|
210
216
|
extend ActiveSupport::Concern
|
|
211
217
|
include ViewModelSpecHelpers::ParentAndBelongsToChild
|
|
@@ -122,7 +122,7 @@ class ViewModel::ActiveRecord
|
|
|
122
122
|
|
|
123
123
|
if migration_versions.present?
|
|
124
124
|
migrator = ViewModel::DownMigrator.new(migration_versions)
|
|
125
|
-
migrator.migrate!(
|
|
125
|
+
migrator.migrate!({ 'data' => data, 'references' => refs })
|
|
126
126
|
end
|
|
127
127
|
|
|
128
128
|
[data, refs]
|
|
@@ -101,8 +101,10 @@ class ViewModel::ActiveRecord::HasManyThroughTest < ActiveSupport::TestCase
|
|
|
101
101
|
@tag1, @tag2, @tag3 = (1..3).map { |x| Tag.create!(name: "tag#{x}") }
|
|
102
102
|
|
|
103
103
|
@parent1 = Parent.create(name: 'p1',
|
|
104
|
-
parents_tags: [
|
|
105
|
-
|
|
104
|
+
parents_tags: [
|
|
105
|
+
ParentsTag.new(tag: @tag2, position: 2.0),
|
|
106
|
+
ParentsTag.new(tag: @tag1, position: 1.0),
|
|
107
|
+
])
|
|
106
108
|
|
|
107
109
|
enable_logging!
|
|
108
110
|
end
|
|
@@ -12,147 +12,267 @@ class ViewModel::ActiveRecord::Migration < ActiveSupport::TestCase
|
|
|
12
12
|
include ARVMTestUtilities
|
|
13
13
|
extend Minitest::Spec::DSL
|
|
14
14
|
|
|
15
|
-
include ViewModelSpecHelpers::ParentAndBelongsToChildWithMigration
|
|
16
|
-
|
|
17
15
|
def new_model
|
|
18
16
|
model_class.new(name: 'm1', child: child_model_class.new(name: 'c1'))
|
|
19
17
|
end
|
|
20
18
|
|
|
21
19
|
let(:viewmodel) { create_viewmodel! }
|
|
22
20
|
|
|
23
|
-
let(:current_serialization) { ViewModel.serialize_to_hash(viewmodel) }
|
|
24
|
-
|
|
25
|
-
let(:v2_serialization) do
|
|
26
|
-
{
|
|
27
|
-
ViewModel::TYPE_ATTRIBUTE => viewmodel_class.view_name,
|
|
28
|
-
ViewModel::VERSION_ATTRIBUTE => 2,
|
|
29
|
-
ViewModel::ID_ATTRIBUTE => viewmodel.id,
|
|
30
|
-
'name' => viewmodel.name,
|
|
31
|
-
'old_field' => 1,
|
|
32
|
-
'child' => {
|
|
33
|
-
ViewModel::TYPE_ATTRIBUTE => child_viewmodel_class.view_name,
|
|
34
|
-
ViewModel::VERSION_ATTRIBUTE => 2,
|
|
35
|
-
ViewModel::ID_ATTRIBUTE => viewmodel.child.id,
|
|
36
|
-
'name' => viewmodel.child.name,
|
|
37
|
-
'former_field' => 'former_value',
|
|
38
|
-
},
|
|
39
|
-
}
|
|
40
|
-
end
|
|
41
|
-
|
|
42
21
|
let(:migration_versions) { { viewmodel_class => 2, child_viewmodel_class => 2 } }
|
|
43
22
|
|
|
44
23
|
let(:down_migrator) { ViewModel::DownMigrator.new(migration_versions) }
|
|
45
24
|
let(:up_migrator) { ViewModel::UpMigrator.new(migration_versions) }
|
|
46
25
|
|
|
47
26
|
def migrate!
|
|
48
|
-
migrator.migrate!(subject
|
|
27
|
+
migrator.migrate!(subject)
|
|
49
28
|
end
|
|
50
29
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
30
|
+
let(:current_serialization) do
|
|
31
|
+
ctx = viewmodel_class.new_serialize_context
|
|
32
|
+
view = ViewModel.serialize_to_hash(viewmodel, serialize_context: ctx)
|
|
33
|
+
refs = ctx.serialize_references_to_hash
|
|
34
|
+
{ 'data' => view, 'references' => refs }
|
|
35
|
+
end
|
|
54
36
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
37
|
+
let(:v2_serialization_data) do
|
|
38
|
+
{
|
|
39
|
+
ViewModel::TYPE_ATTRIBUTE => viewmodel_class.view_name,
|
|
40
|
+
ViewModel::VERSION_ATTRIBUTE => 2,
|
|
41
|
+
ViewModel::ID_ATTRIBUTE => viewmodel.id,
|
|
42
|
+
'name' => viewmodel.name,
|
|
43
|
+
'old_field' => 1,
|
|
44
|
+
'child' => {
|
|
45
|
+
ViewModel::TYPE_ATTRIBUTE => child_viewmodel_class.view_name,
|
|
46
|
+
ViewModel::VERSION_ATTRIBUTE => 2,
|
|
47
|
+
ViewModel::ID_ATTRIBUTE => viewmodel.child.id,
|
|
48
|
+
'name' => viewmodel.child.name,
|
|
49
|
+
'former_field' => 'former_value',
|
|
64
50
|
},
|
|
65
|
-
|
|
66
|
-
|
|
51
|
+
}
|
|
52
|
+
end
|
|
67
53
|
|
|
68
|
-
|
|
69
|
-
migrate!
|
|
54
|
+
let(:v2_serialization_references) { {} }
|
|
70
55
|
|
|
71
|
-
|
|
72
|
-
|
|
56
|
+
let(:v2_serialization) do
|
|
57
|
+
{
|
|
58
|
+
'data' => v2_serialization_data,
|
|
59
|
+
'references' => v2_serialization_references,
|
|
60
|
+
}
|
|
61
|
+
end
|
|
73
62
|
|
|
74
|
-
|
|
75
|
-
|
|
63
|
+
describe 'with defined migrations' do
|
|
64
|
+
include ViewModelSpecHelpers::ParentAndBelongsToChildWithMigration
|
|
76
65
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
66
|
+
describe 'downwards' do
|
|
67
|
+
let(:migrator) { down_migrator }
|
|
68
|
+
let(:subject) { current_serialization.deep_dup }
|
|
69
|
+
|
|
70
|
+
let(:expected_result) do
|
|
71
|
+
v2_serialization.deep_merge(
|
|
72
|
+
{
|
|
73
|
+
'data' => {
|
|
74
|
+
ViewModel::MIGRATED_ATTRIBUTE => true,
|
|
75
|
+
'old_field' => -1,
|
|
76
|
+
'child' => {
|
|
77
|
+
ViewModel::MIGRATED_ATTRIBUTE => true,
|
|
78
|
+
'former_field' => 'reconstructed',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
})
|
|
81
82
|
end
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
83
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
let(:subject) { v2_serialization.deep_dup }
|
|
84
|
+
it 'migrates' do
|
|
85
|
+
migrate!
|
|
88
86
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
ViewModel::MIGRATED_ATTRIBUTE => true,
|
|
92
|
-
'new_field' => 3,
|
|
93
|
-
'child' => {
|
|
94
|
-
ViewModel::MIGRATED_ATTRIBUTE => true,
|
|
95
|
-
},
|
|
96
|
-
)
|
|
97
|
-
end
|
|
87
|
+
assert_equal(expected_result, subject)
|
|
88
|
+
end
|
|
98
89
|
|
|
99
|
-
|
|
100
|
-
|
|
90
|
+
describe 'to an unreachable version' do
|
|
91
|
+
let(:migration_versions) { { viewmodel_class => 2, child_viewmodel_class => 1 } }
|
|
101
92
|
|
|
102
|
-
|
|
93
|
+
it 'raises' do
|
|
94
|
+
assert_raises(ViewModel::Migration::NoPathError) do
|
|
95
|
+
migrate!
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
103
99
|
end
|
|
104
100
|
|
|
105
|
-
describe '
|
|
106
|
-
let(:
|
|
107
|
-
|
|
108
|
-
|
|
101
|
+
describe 'upwards' do
|
|
102
|
+
let(:migrator) { up_migrator }
|
|
103
|
+
let(:subject_data) { v2_serialization_data.deep_dup }
|
|
104
|
+
let(:subject_references) { v2_serialization_references.deep_dup }
|
|
105
|
+
let(:subject) { { 'data' => subject_data, 'references' => subject_references } }
|
|
106
|
+
|
|
107
|
+
let(:expected_result) do
|
|
108
|
+
current_serialization.deep_merge(
|
|
109
|
+
{
|
|
110
|
+
'data' => {
|
|
111
|
+
ViewModel::MIGRATED_ATTRIBUTE => true,
|
|
112
|
+
'new_field' => 3,
|
|
113
|
+
'child' => {
|
|
114
|
+
ViewModel::MIGRATED_ATTRIBUTE => true,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
)
|
|
109
119
|
end
|
|
110
120
|
|
|
111
|
-
it '
|
|
121
|
+
it 'migrates' do
|
|
112
122
|
migrate!
|
|
123
|
+
|
|
113
124
|
assert_equal(expected_result, subject)
|
|
114
125
|
end
|
|
115
|
-
end
|
|
116
126
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
.deep_merge(ViewModel::VERSION_ATTRIBUTE => 3, 'mid_field' => 1)
|
|
122
|
-
end
|
|
127
|
+
describe 'with version unspecified' do
|
|
128
|
+
let(:subject_data) do
|
|
129
|
+
v2_serialization_data.except(ViewModel::VERSION_ATTRIBUTE)
|
|
130
|
+
end
|
|
123
131
|
|
|
124
|
-
|
|
125
|
-
assert_raises(ViewModel::Migration::UnspecifiedVersionError) do
|
|
132
|
+
it 'treats it as the requested version' do
|
|
126
133
|
migrate!
|
|
134
|
+
assert_equal(expected_result, subject)
|
|
127
135
|
end
|
|
128
136
|
end
|
|
129
|
-
end
|
|
130
137
|
|
|
131
|
-
|
|
132
|
-
|
|
138
|
+
describe 'with a version not in the specification' do
|
|
139
|
+
let(:subject_data) do
|
|
140
|
+
v2_serialization_data
|
|
141
|
+
.except('old_field')
|
|
142
|
+
.deep_merge(ViewModel::VERSION_ATTRIBUTE => 3, 'mid_field' => 1)
|
|
143
|
+
end
|
|
133
144
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
145
|
+
it 'rejects it' do
|
|
146
|
+
assert_raises(ViewModel::Migration::UnspecifiedVersionError) do
|
|
147
|
+
migrate!
|
|
148
|
+
end
|
|
149
|
+
end
|
|
138
150
|
end
|
|
139
151
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
152
|
+
describe 'from an unreachable version' do
|
|
153
|
+
let(:migration_versions) { { viewmodel_class => 2, child_viewmodel_class => 1 } }
|
|
154
|
+
|
|
155
|
+
let(:subject_data) do
|
|
156
|
+
v2_serialization_data.deep_merge(
|
|
157
|
+
'child' => { ViewModel::VERSION_ATTRIBUTE => 1 },
|
|
158
|
+
)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
it 'raises' do
|
|
162
|
+
assert_raises(ViewModel::Migration::NoPathError) do
|
|
163
|
+
migrate!
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
describe 'in an undefined direction' do
|
|
169
|
+
let(:migration_versions) { { viewmodel_class => 1, child_viewmodel_class => 2 } }
|
|
170
|
+
|
|
171
|
+
let(:subject_data) do
|
|
172
|
+
v2_serialization_data.except('old_field').merge(ViewModel::VERSION_ATTRIBUTE => 1)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it 'raises' do
|
|
176
|
+
assert_raises(ViewModel::Migration::OneWayError) do
|
|
177
|
+
migrate!
|
|
178
|
+
end
|
|
143
179
|
end
|
|
144
180
|
end
|
|
145
181
|
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
describe 'garbage collection' do
|
|
185
|
+
include ViewModelSpecHelpers::ParentAndSharedBelongsToChild
|
|
186
|
+
|
|
187
|
+
# current (v2) features the shared child, v1 did not
|
|
188
|
+
def model_attributes
|
|
189
|
+
super.merge(
|
|
190
|
+
viewmodel: ->(_v) {
|
|
191
|
+
self.schema_version = 2
|
|
192
|
+
migrates from: 1, to: 2 do
|
|
193
|
+
down do |view, refs|
|
|
194
|
+
view.delete('child')
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
})
|
|
198
|
+
end
|
|
146
199
|
|
|
147
|
-
|
|
148
|
-
|
|
200
|
+
# current (v2) refers to another child, v1 did not
|
|
201
|
+
def child_attributes
|
|
202
|
+
super.merge(
|
|
203
|
+
schema: ->(t) { t.references :child, foreign_key: true },
|
|
204
|
+
model: ->(m) {
|
|
205
|
+
belongs_to :child, inverse_of: :parent, dependent: :destroy
|
|
206
|
+
has_one :parent, inverse_of: :child, class_name: self.name
|
|
207
|
+
},
|
|
208
|
+
viewmodel: ->(_v) {
|
|
209
|
+
self.schema_version = 2
|
|
210
|
+
association :child
|
|
211
|
+
migrates from: 1, to: 2 do
|
|
212
|
+
down do |view, refs|
|
|
213
|
+
view.delete('child')
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
})
|
|
217
|
+
end
|
|
149
218
|
|
|
150
|
-
|
|
151
|
-
|
|
219
|
+
def new_model
|
|
220
|
+
model_class.new(name: 'm1',
|
|
221
|
+
child: child_model_class.new(
|
|
222
|
+
name: 'c1',
|
|
223
|
+
child: child_model_class.new(
|
|
224
|
+
name: 'c2')))
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
let(:migrator) { down_migrator }
|
|
229
|
+
let(:migration_versions) { { viewmodel_class => 1, child_viewmodel_class => 1 } }
|
|
230
|
+
|
|
231
|
+
let(:subject) { current_serialization.deep_dup }
|
|
232
|
+
|
|
233
|
+
let(:expected_result) do
|
|
234
|
+
{
|
|
235
|
+
'data' => {
|
|
236
|
+
ViewModel::TYPE_ATTRIBUTE => viewmodel_class.view_name,
|
|
237
|
+
ViewModel::VERSION_ATTRIBUTE => 1,
|
|
238
|
+
ViewModel::ID_ATTRIBUTE => viewmodel.id,
|
|
239
|
+
ViewModel::MIGRATED_ATTRIBUTE => true,
|
|
240
|
+
'name' => viewmodel.name,
|
|
241
|
+
},
|
|
242
|
+
'references' => {},
|
|
243
|
+
}
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
it 'migrates' do
|
|
247
|
+
migrate!
|
|
248
|
+
|
|
249
|
+
assert_equal(expected_result, subject)
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
describe 'without migrations' do
|
|
254
|
+
describe 'to an unreachable version' do
|
|
255
|
+
include ViewModelSpecHelpers::ParentAndBelongsToChild
|
|
256
|
+
|
|
257
|
+
def model_attributes
|
|
258
|
+
super.merge(viewmodel: ->(_v) {
|
|
259
|
+
self.schema_version = 4
|
|
260
|
+
# Define an unreachable migration to ensure that the view
|
|
261
|
+
# attempts to realize paths.
|
|
262
|
+
migrates from: 1, to: 2 do
|
|
263
|
+
end
|
|
264
|
+
})
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def child_attributes
|
|
268
|
+
super.merge(viewmodel: ->(_v) { self.schema_version = 3 })
|
|
152
269
|
end
|
|
153
270
|
|
|
154
|
-
|
|
155
|
-
|
|
271
|
+
let(:migrator) { down_migrator }
|
|
272
|
+
let(:subject) { current_serialization.deep_dup }
|
|
273
|
+
|
|
274
|
+
it 'raises no path error' do
|
|
275
|
+
assert_raises(ViewModel::Migration::NoPathError) do
|
|
156
276
|
migrate!
|
|
157
277
|
end
|
|
158
278
|
end
|
|
@@ -416,6 +416,11 @@ class ViewModel::ActiveRecordTest < ActiveSupport::TestCase
|
|
|
416
416
|
@list = List.new(child: List.new(child: nil))
|
|
417
417
|
end
|
|
418
418
|
|
|
419
|
+
def test_list_eager_includes
|
|
420
|
+
expected_includes = 3.times.inject(nil) { |x| DeepPreloader::Spec.new({ 'child' => x }) }
|
|
421
|
+
assert_equal(expected_includes, ListView.eager_includes)
|
|
422
|
+
end
|
|
423
|
+
|
|
419
424
|
def test_deserialize_context
|
|
420
425
|
view = {
|
|
421
426
|
'_type' => 'List',
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest/autorun'
|
|
4
|
+
require 'minitest/unit'
|
|
5
|
+
|
|
6
|
+
require 'view_model'
|
|
7
|
+
require 'view_model/garbage_collection'
|
|
8
|
+
|
|
9
|
+
class ViewModel::GarbageCollectionTest < ActiveSupport::TestCase
|
|
10
|
+
extend Minitest::Spec::DSL
|
|
11
|
+
|
|
12
|
+
# Generate a viewmodel-serialization alike from a minimal structure
|
|
13
|
+
# @param [Hash<Symbol, Array<Symbol>] structure mapping from id to referenced ids
|
|
14
|
+
# @param [Hash<Symbol, Array<Symbol>] data_ids list of ids of data elements
|
|
15
|
+
def mock_serialization(data_skeleton, refs_skeleton)
|
|
16
|
+
data = []
|
|
17
|
+
references = {}
|
|
18
|
+
|
|
19
|
+
generate(data_skeleton) do |id, body|
|
|
20
|
+
data << body
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
generate(refs_skeleton) do |id, body|
|
|
24
|
+
references[id] = body
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
{
|
|
28
|
+
"data" => data,
|
|
29
|
+
"references" => references,
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def generate(skeleton)
|
|
34
|
+
skeleton.each do |id, referred|
|
|
35
|
+
yield id, ({
|
|
36
|
+
ViewModel::ID_ATTRIBUTE => id,
|
|
37
|
+
:children => referred.map do |referred_id|
|
|
38
|
+
{ ViewModel::REFERENCE_ATTRIBUTE => referred_id }
|
|
39
|
+
end
|
|
40
|
+
})
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def retained_ids(data_skeleton, refs_skeleton)
|
|
45
|
+
serialization = mock_serialization(data_skeleton, refs_skeleton)
|
|
46
|
+
ViewModel::GarbageCollection.garbage_collect_references!(serialization)
|
|
47
|
+
Set.new(
|
|
48
|
+
(serialization['data'].map { |x| x[ViewModel::ID_ATTRIBUTE] }) +
|
|
49
|
+
(serialization['references'].keys),
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'keeps all roots' do
|
|
54
|
+
assert_equal(
|
|
55
|
+
Set.new([:a, :b, :c]),
|
|
56
|
+
retained_ids({ a: [], b: [], c: [] }, {})
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'keeps a list' do
|
|
61
|
+
assert_equal(
|
|
62
|
+
Set.new([:a, :b, :c, :d]),
|
|
63
|
+
retained_ids({ a: [:b], }, { b: [:c], c: [:d], d: [] }),
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'keeps a child with a removed reference' do
|
|
68
|
+
assert_equal(
|
|
69
|
+
Set.new([:a, :z]),
|
|
70
|
+
retained_ids({ a: [:z], }, { b: [:z], z: [] }),
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it 'prunes a list at the head' do
|
|
75
|
+
assert_equal(
|
|
76
|
+
Set.new([:a]),
|
|
77
|
+
retained_ids({ a: [], }, { b: [:c], c: [:d], d: [] }),
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
end
|
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.2.
|
|
4
|
+
version: 3.2.13
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- iKnow Team
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2021-02-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -414,6 +414,7 @@ files:
|
|
|
414
414
|
- lib/view_model/deserialize_context.rb
|
|
415
415
|
- lib/view_model/error.rb
|
|
416
416
|
- lib/view_model/error_view.rb
|
|
417
|
+
- lib/view_model/garbage_collection.rb
|
|
417
418
|
- lib/view_model/migratable_view.rb
|
|
418
419
|
- lib/view_model/migration.rb
|
|
419
420
|
- lib/view_model/migration/no_path_error.rb
|
|
@@ -467,6 +468,7 @@ files:
|
|
|
467
468
|
- test/unit/view_model/callbacks_test.rb
|
|
468
469
|
- test/unit/view_model/controller_test.rb
|
|
469
470
|
- test/unit/view_model/deserialization_error/unique_violation_test.rb
|
|
471
|
+
- test/unit/view_model/garbage_collection_test.rb
|
|
470
472
|
- test/unit/view_model/record_test.rb
|
|
471
473
|
- test/unit/view_model/registry_test.rb
|
|
472
474
|
- test/unit/view_model/traversal_context_test.rb
|
|
@@ -527,6 +529,7 @@ test_files:
|
|
|
527
529
|
- test/unit/view_model/callbacks_test.rb
|
|
528
530
|
- test/unit/view_model/controller_test.rb
|
|
529
531
|
- test/unit/view_model/deserialization_error/unique_violation_test.rb
|
|
532
|
+
- test/unit/view_model/garbage_collection_test.rb
|
|
530
533
|
- test/unit/view_model/record_test.rb
|
|
531
534
|
- test/unit/view_model/registry_test.rb
|
|
532
535
|
- test/unit/view_model/traversal_context_test.rb
|