iknow_view_models 2.8.4
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 +7 -0
- data/.circleci/config.yml +115 -0
- data/.gitignore +36 -0
- data/.travis.yml +31 -0
- data/Appraisals +9 -0
- data/Gemfile +19 -0
- data/LICENSE.txt +22 -0
- data/README.md +19 -0
- data/Rakefile +21 -0
- data/appveyor.yml +22 -0
- data/gemfiles/rails_5_2.gemfile +15 -0
- data/gemfiles/rails_6_0_beta.gemfile +15 -0
- data/iknow_view_models.gemspec +49 -0
- data/lib/iknow_view_models.rb +12 -0
- data/lib/iknow_view_models/railtie.rb +8 -0
- data/lib/iknow_view_models/version.rb +5 -0
- data/lib/view_model.rb +333 -0
- data/lib/view_model/access_control.rb +154 -0
- data/lib/view_model/access_control/composed.rb +216 -0
- data/lib/view_model/access_control/open.rb +13 -0
- data/lib/view_model/access_control/read_only.rb +13 -0
- data/lib/view_model/access_control/tree.rb +264 -0
- data/lib/view_model/access_control_error.rb +10 -0
- data/lib/view_model/active_record.rb +383 -0
- data/lib/view_model/active_record/association_data.rb +178 -0
- data/lib/view_model/active_record/association_manipulation.rb +389 -0
- data/lib/view_model/active_record/cache.rb +265 -0
- data/lib/view_model/active_record/cache/cacheable_view.rb +51 -0
- data/lib/view_model/active_record/cloner.rb +113 -0
- data/lib/view_model/active_record/collection_nested_controller.rb +100 -0
- data/lib/view_model/active_record/controller.rb +77 -0
- data/lib/view_model/active_record/controller_base.rb +185 -0
- data/lib/view_model/active_record/nested_controller_base.rb +93 -0
- data/lib/view_model/active_record/singular_nested_controller.rb +34 -0
- data/lib/view_model/active_record/update_context.rb +252 -0
- data/lib/view_model/active_record/update_data.rb +749 -0
- data/lib/view_model/active_record/update_operation.rb +810 -0
- data/lib/view_model/active_record/visitor.rb +77 -0
- data/lib/view_model/after_transaction_runner.rb +29 -0
- data/lib/view_model/callbacks.rb +219 -0
- data/lib/view_model/changes.rb +62 -0
- data/lib/view_model/config.rb +29 -0
- data/lib/view_model/controller.rb +142 -0
- data/lib/view_model/deserialization_error.rb +437 -0
- data/lib/view_model/deserialize_context.rb +16 -0
- data/lib/view_model/error.rb +191 -0
- data/lib/view_model/error_view.rb +35 -0
- data/lib/view_model/record.rb +367 -0
- data/lib/view_model/record/attribute_data.rb +48 -0
- data/lib/view_model/reference.rb +31 -0
- data/lib/view_model/references.rb +48 -0
- data/lib/view_model/registry.rb +73 -0
- data/lib/view_model/schemas.rb +45 -0
- data/lib/view_model/serialization_error.rb +10 -0
- data/lib/view_model/serialize_context.rb +118 -0
- data/lib/view_model/test_helpers.rb +103 -0
- data/lib/view_model/test_helpers/arvm_builder.rb +111 -0
- data/lib/view_model/traversal_context.rb +126 -0
- data/lib/view_model/utils.rb +24 -0
- data/lib/view_model/utils/collections.rb +49 -0
- data/test/helpers/arvm_test_models.rb +59 -0
- data/test/helpers/arvm_test_utilities.rb +187 -0
- data/test/helpers/callback_tracer.rb +27 -0
- data/test/helpers/controller_test_helpers.rb +270 -0
- data/test/helpers/match_enumerator.rb +58 -0
- data/test/helpers/query_logging.rb +71 -0
- data/test/helpers/test_access_control.rb +56 -0
- data/test/helpers/viewmodel_spec_helpers.rb +326 -0
- data/test/unit/view_model/access_control_test.rb +769 -0
- data/test/unit/view_model/active_record/alias_test.rb +35 -0
- data/test/unit/view_model/active_record/belongs_to_test.rb +376 -0
- data/test/unit/view_model/active_record/cache_test.rb +351 -0
- data/test/unit/view_model/active_record/cloner_test.rb +313 -0
- data/test/unit/view_model/active_record/controller_test.rb +561 -0
- data/test/unit/view_model/active_record/counter_test.rb +80 -0
- data/test/unit/view_model/active_record/customization_test.rb +388 -0
- data/test/unit/view_model/active_record/has_many_test.rb +957 -0
- data/test/unit/view_model/active_record/has_many_through_poly_test.rb +269 -0
- data/test/unit/view_model/active_record/has_many_through_test.rb +736 -0
- data/test/unit/view_model/active_record/has_one_test.rb +334 -0
- data/test/unit/view_model/active_record/namespacing_test.rb +75 -0
- data/test/unit/view_model/active_record/optional_attribute_view_test.rb +58 -0
- data/test/unit/view_model/active_record/poly_test.rb +320 -0
- data/test/unit/view_model/active_record/shared_test.rb +285 -0
- data/test/unit/view_model/active_record/version_test.rb +121 -0
- data/test/unit/view_model/active_record_test.rb +542 -0
- data/test/unit/view_model/callbacks_test.rb +582 -0
- data/test/unit/view_model/deserialization_error/unique_violation_test.rb +73 -0
- data/test/unit/view_model/record_test.rb +524 -0
- data/test/unit/view_model/traversal_context_test.rb +371 -0
- data/test/unit/view_model_test.rb +62 -0
- metadata +490 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
require_relative "../../../helpers/arvm_test_utilities.rb"
|
|
2
|
+
require_relative "../../../helpers/arvm_test_models.rb"
|
|
3
|
+
|
|
4
|
+
require "minitest/autorun"
|
|
5
|
+
|
|
6
|
+
require "view_model/active_record"
|
|
7
|
+
|
|
8
|
+
class ViewModel::ActiveRecord::HasManyThroughPolyTest < ActiveSupport::TestCase
|
|
9
|
+
include ARVMTestUtilities
|
|
10
|
+
|
|
11
|
+
def self.build_tag_a(arvm_test_case)
|
|
12
|
+
arvm_test_case.build_viewmodel(:TagA) do
|
|
13
|
+
define_schema do |t|
|
|
14
|
+
t.string :name
|
|
15
|
+
t.string :tag_b_desc
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
define_model do
|
|
19
|
+
has_many :parents_tag, dependent: :destroy, inverse_of: :tag
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
define_viewmodel do
|
|
23
|
+
attributes :name
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.build_tag_b(arvm_test_case)
|
|
29
|
+
arvm_test_case.build_viewmodel(:TagB) do
|
|
30
|
+
define_schema do |t|
|
|
31
|
+
t.string :name
|
|
32
|
+
t.string :tag_b_desc
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
define_model do
|
|
36
|
+
has_many :parents_tag, dependent: :destroy, inverse_of: :tag
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
define_viewmodel do
|
|
40
|
+
attributes :name
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.build_parent(arvm_test_case)
|
|
46
|
+
arvm_test_case.build_viewmodel(:Parent) do
|
|
47
|
+
define_schema do |t|
|
|
48
|
+
t.string :name
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
define_model do
|
|
52
|
+
has_many :parents_tags, dependent: :destroy, inverse_of: :parent
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
define_viewmodel do
|
|
56
|
+
attributes :name
|
|
57
|
+
association :tags, shared: true, through: :parents_tags, through_order_attr: :position, viewmodels: [TagAView, TagBView]
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.build_parent_tag_join_model(arvm_test_case)
|
|
63
|
+
arvm_test_case.build_viewmodel(:ParentsTag) do
|
|
64
|
+
define_schema do |t|
|
|
65
|
+
t.references :parent, foreign_key: true
|
|
66
|
+
t.references :tag
|
|
67
|
+
t.string :tag_type
|
|
68
|
+
t.float :position
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
define_model do
|
|
72
|
+
belongs_to :parent
|
|
73
|
+
belongs_to :tag, polymorphic: true
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
no_viewmodel
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def before_all
|
|
81
|
+
super
|
|
82
|
+
|
|
83
|
+
self.class.build_tag_a(self)
|
|
84
|
+
self.class.build_tag_b(self)
|
|
85
|
+
self.class.build_parent(self)
|
|
86
|
+
self.class.build_parent_tag_join_model(self)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private def context_with(*args)
|
|
90
|
+
ParentView.new_serialize_context(include: args)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def setup
|
|
94
|
+
super
|
|
95
|
+
|
|
96
|
+
@tag_a1, @tag_a2 = (1..2).map { |x| TagA.create(name: "tag A#{x}") }
|
|
97
|
+
@tag_b1, @tag_b2 = (1..2).map { |x| TagB.create(name: "tag B#{x}") }
|
|
98
|
+
|
|
99
|
+
@parent1 = Parent.create(name: 'p1',
|
|
100
|
+
parents_tags: [ParentsTag.new(tag: @tag_a1, position: 1.0),
|
|
101
|
+
ParentsTag.new(tag: @tag_a2, position: 2.0),
|
|
102
|
+
ParentsTag.new(tag: @tag_b1, position: 3.0),
|
|
103
|
+
ParentsTag.new(tag: @tag_b2, position: 4.0)])
|
|
104
|
+
|
|
105
|
+
enable_logging!
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def test_roundtrip
|
|
109
|
+
# Objects are serialized to a view and deserialized, and should not be different when complete.
|
|
110
|
+
|
|
111
|
+
alter_by_view!(ParentView, @parent1, serialize_context: context_with(:tags)) {}
|
|
112
|
+
assert_equal('p1', @parent1.name)
|
|
113
|
+
assert_equal([@tag_a1, @tag_a2, @tag_b1, @tag_b2],
|
|
114
|
+
@parent1.parents_tags.order(:position).map(&:tag))
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def test_loading_batching
|
|
118
|
+
context = context_with(:tags)
|
|
119
|
+
log_queries do
|
|
120
|
+
parent_views = ParentView.load(serialize_context: context)
|
|
121
|
+
serialize(parent_views, serialize_context: context)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
assert_equal(['Parent Load', 'ParentsTag Load', 'TagA Load', 'TagB Load'],
|
|
125
|
+
logged_load_queries)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def test_eager_includes
|
|
129
|
+
includes = ParentView.eager_includes(serialize_context: context_with(:tags))
|
|
130
|
+
assert_equal(DeepPreloader::Spec.new(
|
|
131
|
+
'parents_tags' => DeepPreloader::Spec.new(
|
|
132
|
+
'tag' => DeepPreloader::PolymorphicSpec.new(
|
|
133
|
+
'TagA' => DeepPreloader::Spec.new,
|
|
134
|
+
'TagB' => DeepPreloader::Spec.new))),
|
|
135
|
+
includes)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def test_preload_dependencies
|
|
139
|
+
# TODO not part of ARVM; but depends on the particular context from #before_all
|
|
140
|
+
# If we refactor out the contexts from their tests, this should go in another test file.
|
|
141
|
+
|
|
142
|
+
root_updates, _ref_updates = ViewModel::ActiveRecord::UpdateData.parse_hashes([{ '_type' => 'Parent' }])
|
|
143
|
+
assert_equal(DeepPreloader::Spec.new,
|
|
144
|
+
root_updates.first.preload_dependencies,
|
|
145
|
+
'nothing loaded by default')
|
|
146
|
+
|
|
147
|
+
root_updates, _ref_updates = ViewModel::ActiveRecord::UpdateData.parse_hashes([{ '_type' => 'Parent',
|
|
148
|
+
'tags' => [{ '_ref' => 'r1' }] }],
|
|
149
|
+
{ 'r1' => { '_type' => 'TagB' } })
|
|
150
|
+
|
|
151
|
+
assert_equal(DeepPreloader::Spec.new(
|
|
152
|
+
'parents_tags' => DeepPreloader::Spec.new(
|
|
153
|
+
'tag' => DeepPreloader::PolymorphicSpec.new)),
|
|
154
|
+
root_updates.first.preload_dependencies,
|
|
155
|
+
'mentioning tags causes through association loading, excluding shared')
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_serialize
|
|
160
|
+
view, refs = serialize_with_references(ParentView.new(@parent1),
|
|
161
|
+
serialize_context: context_with(:tags))
|
|
162
|
+
|
|
163
|
+
tag_data = view['tags'].map { |hash| refs[hash['_ref']] }
|
|
164
|
+
assert_equal([{ 'id' => @tag_a1.id, '_type' => 'TagA', '_version' => 1, 'name' => 'tag A1' },
|
|
165
|
+
{ 'id' => @tag_a2.id, '_type' => 'TagA', '_version' => 1, 'name' => 'tag A2' },
|
|
166
|
+
{ 'id' => @tag_b1.id, '_type' => 'TagB', '_version' => 1, 'name' => 'tag B1' },
|
|
167
|
+
{ 'id' => @tag_b2.id, '_type' => 'TagB', '_version' => 1, 'name' => 'tag B2' }],
|
|
168
|
+
tag_data)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def test_create_has_many_through
|
|
172
|
+
alter_by_view!(ParentView, @parent1) do |view, refs|
|
|
173
|
+
view['tags'].each { |tr| refs.delete(tr['_ref']) }
|
|
174
|
+
|
|
175
|
+
view['tags'] = [{ '_ref' => 't1' }, { '_ref' => 't2' }]
|
|
176
|
+
refs['t1'] = { '_type' => 'TagA', 'name' => 'new tagA' }
|
|
177
|
+
refs['t2'] = { '_type' => 'TagB', 'name' => 'new tagB' }
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
new_tag_a = TagA.find_by_name('new tagA')
|
|
181
|
+
new_tag_b = TagB.find_by_name('new tagB')
|
|
182
|
+
|
|
183
|
+
refute_nil(new_tag_a, 'new tag A created')
|
|
184
|
+
refute_nil(new_tag_b, 'new tag B created')
|
|
185
|
+
|
|
186
|
+
assert_equal([new_tag_a, new_tag_b],
|
|
187
|
+
@parent1.parents_tags.order(:position).map(&:tag))
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def test_reordering_swap_type
|
|
191
|
+
alter_by_view!(ParentView, @parent1, serialize_context: context_with(:tags)) do |view, refs|
|
|
192
|
+
t1, t2, t3, t4 = view['tags']
|
|
193
|
+
view['tags'] = [t3, t2, t1, t4]
|
|
194
|
+
end
|
|
195
|
+
assert_equal([@tag_b1, @tag_a2, @tag_a1, @tag_b2],
|
|
196
|
+
@parent1.parents_tags.order(:position).map(&:tag))
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def test_delete
|
|
200
|
+
alter_by_view!(ParentView, @parent1) do |view, refs|
|
|
201
|
+
refs.clear
|
|
202
|
+
view['tags'] = []
|
|
203
|
+
end
|
|
204
|
+
assert_equal([], @parent1.parents_tags)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
class RenameTest < ActiveSupport::TestCase
|
|
208
|
+
include ARVMTestUtilities
|
|
209
|
+
|
|
210
|
+
def before_all
|
|
211
|
+
super
|
|
212
|
+
|
|
213
|
+
ViewModel::ActiveRecord::HasManyThroughPolyTest.build_tag_a(self)
|
|
214
|
+
ViewModel::ActiveRecord::HasManyThroughPolyTest.build_tag_b(self)
|
|
215
|
+
|
|
216
|
+
build_viewmodel(:Parent) do
|
|
217
|
+
define_schema do |t|
|
|
218
|
+
t.string :name
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
define_model do
|
|
222
|
+
has_many :parents_tags, dependent: :destroy, inverse_of: :parent
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
define_viewmodel do
|
|
226
|
+
attributes :name
|
|
227
|
+
association :tags, shared: true, through: :parents_tags, through_order_attr: :position, viewmodels: [TagAView, TagBView], as: :something_else
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
ViewModel::ActiveRecord::HasManyThroughPolyTest.build_parent_tag_join_model(self)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def setup
|
|
235
|
+
super
|
|
236
|
+
|
|
237
|
+
@parent = Parent.create(parents_tags: [ParentsTag.new(tag: TagA.new(name: 'tag A name'))])
|
|
238
|
+
|
|
239
|
+
enable_logging!
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def test_dependencies
|
|
243
|
+
root_updates, _ref_updates = ViewModel::ActiveRecord::UpdateData.parse_hashes([{ '_type' => 'Parent', 'something_else' => [] }])
|
|
244
|
+
# Compare to non-polymorphic, which will also load the tags
|
|
245
|
+
deps = root_updates.first.preload_dependencies
|
|
246
|
+
assert_equal(DeepPreloader::Spec.new('parents_tags' => DeepPreloader::Spec.new('tag' => DeepPreloader::PolymorphicSpec.new)), deps)
|
|
247
|
+
assert_equal({ 'something_else' => {} }, root_updates.first.updated_associations)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def test_renamed_roundtrip
|
|
252
|
+
context = ParentView.new_serialize_context(include: :something_else)
|
|
253
|
+
alter_by_view!(ParentView, @parent, serialize_context: context) do |view, refs|
|
|
254
|
+
assert_equal({refs.keys.first => { 'id' => @parent.parents_tags.first.tag.id,
|
|
255
|
+
'_type' => 'TagA',
|
|
256
|
+
'_version' => 1,
|
|
257
|
+
'name' => 'tag A name' }}, refs)
|
|
258
|
+
assert_equal([{ '_ref' => refs.keys.first }],
|
|
259
|
+
view['something_else'])
|
|
260
|
+
|
|
261
|
+
refs.clear
|
|
262
|
+
refs['new'] = {'_type' => 'TagB', 'name' => 'tag B name'}
|
|
263
|
+
view['something_else'] = [{'_ref' => 'new'}]
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
assert_equal('tag B name', @parent.parents_tags.first.tag.name)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
require_relative "../../../helpers/arvm_test_utilities.rb"
|
|
2
|
+
require_relative "../../../helpers/arvm_test_models.rb"
|
|
3
|
+
|
|
4
|
+
require "minitest/autorun"
|
|
5
|
+
|
|
6
|
+
require "view_model/active_record"
|
|
7
|
+
|
|
8
|
+
class ViewModel::ActiveRecord::HasManyThroughTest < ActiveSupport::TestCase
|
|
9
|
+
include ARVMTestUtilities
|
|
10
|
+
|
|
11
|
+
def self.build_parent(arvm_test_case)
|
|
12
|
+
arvm_test_case.build_viewmodel(:Parent) do
|
|
13
|
+
define_schema do |t|
|
|
14
|
+
t.string :name
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
define_model do
|
|
18
|
+
has_many :parents_tags, dependent: :destroy, inverse_of: :parent
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
define_viewmodel do
|
|
22
|
+
attributes :name
|
|
23
|
+
association :tags, shared: true, through: :parents_tags, through_order_attr: :position
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.build_tag(arvm_test_case, with: [])
|
|
29
|
+
use_childtag = with.include?(:ChildTag)
|
|
30
|
+
arvm_test_case.build_viewmodel(:Tag) do
|
|
31
|
+
define_schema do |t|
|
|
32
|
+
t.string :name
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
define_model do
|
|
36
|
+
has_many :parents_tags, dependent: :destroy, inverse_of: :tag
|
|
37
|
+
if use_childtag
|
|
38
|
+
has_many :child_tags, dependent: :destroy, inverse_of: :tag
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
define_viewmodel do
|
|
43
|
+
attributes :name
|
|
44
|
+
if use_childtag
|
|
45
|
+
associations :child_tags
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.build_childtag(arvm_test_case)
|
|
52
|
+
arvm_test_case.build_viewmodel(:ChildTag) do
|
|
53
|
+
define_schema do |t|
|
|
54
|
+
t.string :name
|
|
55
|
+
t.references :tag, foreign_key: true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
define_model do
|
|
59
|
+
belongs_to :tag, dependent: :destroy, inverse_of: :child_tag
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
define_viewmodel do
|
|
63
|
+
attributes :name
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.build_join_table_model(arvm_test_case)
|
|
69
|
+
arvm_test_case.build_viewmodel(:ParentsTag) do
|
|
70
|
+
define_schema do |t|
|
|
71
|
+
t.references :parent, foreign_key: true
|
|
72
|
+
t.references :tag, foreign_key: true
|
|
73
|
+
t.float :position
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
define_model do
|
|
77
|
+
belongs_to :parent
|
|
78
|
+
belongs_to :tag
|
|
79
|
+
# TODO list membership?
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
no_viewmodel
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def before_all
|
|
87
|
+
super
|
|
88
|
+
|
|
89
|
+
self.class.build_parent(self)
|
|
90
|
+
self.class.build_tag(self)
|
|
91
|
+
self.class.build_join_table_model(self)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private def context_with(*args)
|
|
95
|
+
ParentView.new_serialize_context(include: args)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def setup
|
|
99
|
+
super
|
|
100
|
+
|
|
101
|
+
@tag1, @tag2, @tag3 = (1..3).map { |x| Tag.create!(name: "tag#{x}") }
|
|
102
|
+
|
|
103
|
+
@parent1 = Parent.create(name: 'p1',
|
|
104
|
+
parents_tags: [ParentsTag.new(tag: @tag1, position: 1.0),
|
|
105
|
+
ParentsTag.new(tag: @tag2, position: 2.0)])
|
|
106
|
+
|
|
107
|
+
enable_logging!
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def test_loading_batching
|
|
111
|
+
context = context_with(:tags)
|
|
112
|
+
log_queries do
|
|
113
|
+
parent_views = ParentView.load(serialize_context: context)
|
|
114
|
+
serialize(parent_views, serialize_context: context)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
assert_equal(['Parent Load', 'ParentsTag Load', 'Tag Load'],
|
|
118
|
+
logged_load_queries)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def test_roundtrip
|
|
122
|
+
# Objects are serialized to a view and deserialized, and should not be different when complete.
|
|
123
|
+
|
|
124
|
+
alter_by_view!(ParentView, @parent1, serialize_context: context_with(:tags)) {}
|
|
125
|
+
assert_equal('p1', @parent1.name)
|
|
126
|
+
assert_equal([@tag1, @tag2], @parent1.parents_tags.order(:position).map(&:tag))
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def test_eager_includes
|
|
130
|
+
includes = ParentView.eager_includes(serialize_context: context_with(:tags))
|
|
131
|
+
assert_equal(DeepPreloader::Spec.new('parents_tags' => DeepPreloader::Spec.new('tag' => DeepPreloader::Spec.new)), includes)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def test_preload_dependencies
|
|
135
|
+
# TODO not part of ARVM; but depends on the particular context from #before_all
|
|
136
|
+
# If we refactor out the contexts from their tests, this should go in another test file.
|
|
137
|
+
|
|
138
|
+
root_updates, _ref_updates = ViewModel::ActiveRecord::UpdateData.parse_hashes([{ '_type' => 'Parent' }])
|
|
139
|
+
assert_equal(DeepPreloader::Spec.new,
|
|
140
|
+
root_updates.first.preload_dependencies,
|
|
141
|
+
'nothing loaded by default')
|
|
142
|
+
|
|
143
|
+
root_updates, _ref_updates = ViewModel::ActiveRecord::UpdateData.parse_hashes(
|
|
144
|
+
[{ '_type' => 'Parent',
|
|
145
|
+
'tags' => [{ '_ref' => 'r1' }] }],
|
|
146
|
+
{ 'r1' => { '_type' => 'Tag' } })
|
|
147
|
+
|
|
148
|
+
assert_equal(DeepPreloader::Spec.new('parents_tags' => DeepPreloader::Spec.new('tag' => DeepPreloader::Spec.new)),
|
|
149
|
+
root_updates.first.preload_dependencies,
|
|
150
|
+
'mentioning tags and child_tags causes through association loading')
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def test_updated_associations
|
|
154
|
+
root_updates, _ref_updates = ViewModel::ActiveRecord::UpdateData.parse_hashes(
|
|
155
|
+
[{ '_type' => 'Parent',
|
|
156
|
+
'tags' => [{ '_ref' => 'r1' }] }],
|
|
157
|
+
{ 'r1' => { '_type' => 'Tag', } })
|
|
158
|
+
|
|
159
|
+
assert_equal({ 'tags' => {} },
|
|
160
|
+
root_updates.first.updated_associations,
|
|
161
|
+
'mentioning tags causes through association loading')
|
|
162
|
+
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def test_serialize
|
|
166
|
+
view, refs = serialize_with_references(ParentView.new(@parent1),
|
|
167
|
+
serialize_context: context_with(:tags))
|
|
168
|
+
|
|
169
|
+
tag_data = view['tags'].map { |hash| refs[hash['_ref']] }
|
|
170
|
+
assert_equal([{ 'id' => @tag1.id, '_type' => 'Tag', '_version' => 1, 'name' => 'tag1' },
|
|
171
|
+
{ 'id' => @tag2.id, '_type' => 'Tag', '_version' => 1, 'name' => 'tag2' }],
|
|
172
|
+
tag_data)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def test_create_has_many_through
|
|
176
|
+
alter_by_view!(ParentView, @parent1) do |view, refs|
|
|
177
|
+
refs.delete_if { |_, ref_hash| ref_hash['_type'] == 'Tag' }
|
|
178
|
+
refs['t1'] = { '_type' => 'Tag', 'name' => 'new tag1' }
|
|
179
|
+
refs['t2'] = { '_type' => 'Tag', 'name' => 'new tag2' }
|
|
180
|
+
view['tags'] = [{ '_ref' => 't1' }, { '_ref' => 't2' }]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
new_tag1, new_tag2 = Tag.where(name: ['new tag1', 'new tag2'])
|
|
184
|
+
|
|
185
|
+
refute_nil(new_tag1, 'new tag 1 created')
|
|
186
|
+
refute_nil(new_tag2, 'new tag 2 created')
|
|
187
|
+
|
|
188
|
+
assert_equal([new_tag1, new_tag2], @parent1.parents_tags.order(:position).map(&:tag),
|
|
189
|
+
'database state updated')
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def test_delete
|
|
193
|
+
alter_by_view!(ParentView, @parent1) do |view, refs|
|
|
194
|
+
refs.clear
|
|
195
|
+
view['tags'] = []
|
|
196
|
+
end
|
|
197
|
+
assert_equal([], @parent1.parents_tags)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def test_reordering
|
|
201
|
+
pv, ctx = alter_by_view!(ParentView, @parent1, serialize_context: context_with(:tags)) do |view, _refs|
|
|
202
|
+
view['tags'].reverse!
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
assert_equal([@tag2, @tag1],
|
|
206
|
+
@parent1.parents_tags.order(:position).map(&:tag))
|
|
207
|
+
|
|
208
|
+
expected_edit_checks = [pv.to_reference]
|
|
209
|
+
assert_contains_exactly(expected_edit_checks, ctx.valid_edit_refs)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def test_child_edit_doesnt_editcheck_parent
|
|
213
|
+
# editing child doesn't edit check parent
|
|
214
|
+
pv, d_context = alter_by_view!(ParentView, @parent1, serialize_context: context_with(:tags)) do |view, refs|
|
|
215
|
+
refs[view['tags'][0]["_ref"]]["name"] = "changed"
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
nc = pv.tags.detect { |t| t.name == 'changed' }
|
|
219
|
+
|
|
220
|
+
expected_edit_checks = [nc.to_reference]
|
|
221
|
+
assert_contains_exactly(expected_edit_checks,
|
|
222
|
+
d_context.valid_edit_refs)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def test_child_reordering_editchecks_parent
|
|
226
|
+
pv, d_context = alter_by_view!(ParentView, @parent1, serialize_context: context_with(:tags)) do |view, _refs|
|
|
227
|
+
view['tags'].reverse!
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
assert_contains_exactly([pv.to_reference],
|
|
231
|
+
d_context.valid_edit_refs)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def test_child_deletion_editchecks_parent
|
|
235
|
+
pv, d_context = alter_by_view!(ParentView, @parent1, serialize_context: context_with(:tags)) do |view, refs|
|
|
236
|
+
removed = view['tags'].pop['_ref']
|
|
237
|
+
refs.delete(removed)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
assert_contains_exactly([pv.to_reference],
|
|
241
|
+
d_context.valid_edit_refs)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def test_child_addition_editchecks_parent
|
|
245
|
+
pv, d_context = alter_by_view!(ParentView, @parent1, serialize_context: context_with(:tags)) do |view, refs|
|
|
246
|
+
view['tags'] << { '_ref' => 't_new' }
|
|
247
|
+
refs['t_new'] = { '_type' => 'Tag', 'name' => 'newest tag' }
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
nc = pv.tags.detect { |t| t.name == 'newest tag' }
|
|
251
|
+
|
|
252
|
+
expected_edit_checks = [pv.to_reference, nc.to_reference]
|
|
253
|
+
|
|
254
|
+
assert_contains_exactly(expected_edit_checks,
|
|
255
|
+
d_context.valid_edit_refs)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def tags(parent)
|
|
259
|
+
parent.parents_tags.order(:position).includes(:tag).map(&:tag)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def fupdate_tags(parent)
|
|
263
|
+
tags = self.tags(parent)
|
|
264
|
+
fupdate, refs = yield(tags).values_at(:fupdate, :refs)
|
|
265
|
+
op_view = { '_type' => 'Parent',
|
|
266
|
+
'id' => parent.id,
|
|
267
|
+
'tags' => fupdate }
|
|
268
|
+
ParentView.deserialize_from_view(op_view, references: refs || {})
|
|
269
|
+
parent.reload
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def test_functional_update_append
|
|
273
|
+
c1 = c2 = nil
|
|
274
|
+
fupdate_tags(@parent1) do |tags|
|
|
275
|
+
c1, c2 = tags
|
|
276
|
+
{ :fupdate => build_fupdate { append([{ '_ref' => 'new_tag' }]) },
|
|
277
|
+
:refs => { 'new_tag' => { '_type' => 'Tag', 'name' => 'new tag' } }
|
|
278
|
+
}
|
|
279
|
+
end
|
|
280
|
+
assert_equal([c1.name, c2.name, 'new tag'],
|
|
281
|
+
tags(@parent1).map(&:name))
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def test_functional_update_append_before_mid
|
|
285
|
+
c1 = c2 = nil
|
|
286
|
+
fupdate_tags(@parent1) do |tags|
|
|
287
|
+
c1, c2 = tags
|
|
288
|
+
{ :fupdate => build_fupdate { append([{ '_ref' => 'new_tag' }],
|
|
289
|
+
before: { '_type' => 'Tag', 'id' => c2.id }) },
|
|
290
|
+
:refs => { 'new_tag' => { '_type' => 'Tag', 'name' => 'new tag' } }
|
|
291
|
+
}
|
|
292
|
+
end
|
|
293
|
+
assert_equal([c1.name, 'new tag', c2.name],
|
|
294
|
+
tags(@parent1).map(&:name))
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def test_functional_update_append_before_beginning
|
|
298
|
+
c1 = c2 = nil
|
|
299
|
+
fupdate_tags(@parent1) do |tags|
|
|
300
|
+
c1, c2 = tags
|
|
301
|
+
{ :fupdate => build_fupdate { append([{ '_ref' => 'new_tag' }],
|
|
302
|
+
before: { '_type' => 'Tag', 'id' => c1.id }) },
|
|
303
|
+
:refs => { 'new_tag' => { '_type' => 'Tag', 'name' => 'new tag' } }
|
|
304
|
+
}
|
|
305
|
+
end
|
|
306
|
+
assert_equal(['new tag', c1.name, c2.name],
|
|
307
|
+
tags(@parent1).map(&:name))
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def test_functional_update_append_before_reorder
|
|
311
|
+
c1 = c2 = nil
|
|
312
|
+
fupdate_tags(@parent1) do |tags|
|
|
313
|
+
c1, c2 = tags
|
|
314
|
+
{ :fupdate => build_fupdate { append([{ '_ref' => 'c2' }],
|
|
315
|
+
before: { '_type' => 'Tag', 'id' => c1.id }) },
|
|
316
|
+
:refs => { 'c2' => { '_type' => 'Tag', 'id' => c2.id } }
|
|
317
|
+
}
|
|
318
|
+
end
|
|
319
|
+
assert_equal([c2.name, c1.name],
|
|
320
|
+
tags(@parent1).map(&:name))
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def test_functional_update_append_after_mid
|
|
325
|
+
c1 = c2 = nil
|
|
326
|
+
fupdate_tags(@parent1) do |tags|
|
|
327
|
+
c1, c2 = tags
|
|
328
|
+
{ :fupdate => build_fupdate { append([{ '_ref' => 'new_tag' }],
|
|
329
|
+
after: { '_type' => 'Tag', 'id' => c1.id }) },
|
|
330
|
+
:refs => { 'new_tag' => { '_type' => 'Tag', 'name' => 'new tag' } }
|
|
331
|
+
}
|
|
332
|
+
end
|
|
333
|
+
assert_equal([c1.name, 'new tag', c2.name],
|
|
334
|
+
tags(@parent1).map(&:name))
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def test_functional_update_append_after_end
|
|
338
|
+
c1 = c2 = nil
|
|
339
|
+
fupdate_tags(@parent1) do |tags|
|
|
340
|
+
c1, c2 = tags
|
|
341
|
+
{ :fupdate => build_fupdate { append([{ '_ref' => 'new_tag' }],
|
|
342
|
+
after: { '_type' => 'Tag', 'id' => c2.id }) },
|
|
343
|
+
:refs => { 'new_tag' => { '_type' => 'Tag', 'name' => 'new tag' } }
|
|
344
|
+
}
|
|
345
|
+
end
|
|
346
|
+
assert_equal([c1.name, c2.name, 'new tag'],
|
|
347
|
+
tags(@parent1).map(&:name))
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def test_functional_update_append_after_reorder
|
|
351
|
+
c1 = c2 = nil
|
|
352
|
+
fupdate_tags(@parent1) do |tags|
|
|
353
|
+
c1, c2 = tags
|
|
354
|
+
{ :fupdate => build_fupdate { append([{ '_ref' => 'c1' }],
|
|
355
|
+
after: { '_type' => 'Tag', 'id' => c2.id }) },
|
|
356
|
+
:refs => { 'c1' => { '_type' => 'Tag', 'id' => c1.id } }
|
|
357
|
+
}
|
|
358
|
+
end
|
|
359
|
+
assert_equal([c2.name, c1.name],
|
|
360
|
+
tags(@parent1).map(&:name))
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def test_functional_update_remove_success
|
|
364
|
+
c1 = c2 = nil
|
|
365
|
+
fupdate_tags(@parent1) do |tags|
|
|
366
|
+
c1, c2 = tags
|
|
367
|
+
{ :fupdate => build_fupdate { remove([{ '_type' => 'Tag', 'id' => c1.id }]) } }
|
|
368
|
+
end
|
|
369
|
+
assert_equal([c2.name],
|
|
370
|
+
tags(@parent1).map(&:name))
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def test_functional_update_remove_stale
|
|
374
|
+
# remove an entity that's no longer part of the collection
|
|
375
|
+
ex = assert_raises(ViewModel::DeserializationError::AssociatedNotFound) do
|
|
376
|
+
fupdate_tags(@parent1) do |tags|
|
|
377
|
+
_c1, c2 = tags
|
|
378
|
+
@parent1.parents_tags.where(tag_id: c2.id).destroy_all
|
|
379
|
+
{ :fupdate => build_fupdate { remove([{ '_type' => 'Tag', 'id' => c2.id }]) } }
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
assert_equal('tags', ex.association)
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def test_functional_update_append_after_corpse
|
|
386
|
+
# append after something that no longer exists
|
|
387
|
+
ex = assert_raises(ViewModel::DeserializationError::AssociatedNotFound) do
|
|
388
|
+
fupdate_tags(@parent1) do |tags|
|
|
389
|
+
_c1, c2 = tags
|
|
390
|
+
@parent1.parents_tags.where(tag_id: c2.id).destroy_all
|
|
391
|
+
{ :fupdate => build_fupdate { append([{ '_ref' => 'new_tag' }],
|
|
392
|
+
after: { '_type' => 'Tag', 'id' => c2.id }) },
|
|
393
|
+
:refs => { 'new_tag' => { '_type' => 'Tag', 'name' => 'new tag name' } }
|
|
394
|
+
}
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
assert_equal('tags', ex.association)
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def test_functional_update_update_success
|
|
401
|
+
# refer to a shared entity with edits, no collection add/remove
|
|
402
|
+
c1 = c2 = nil
|
|
403
|
+
fupdate_tags(@parent1) do |tags|
|
|
404
|
+
c1, c2 = tags
|
|
405
|
+
{ :fupdate => build_fupdate { update([{ '_ref' => 'c1' }])},
|
|
406
|
+
:refs => { 'c1' => { '_type' => 'Tag', 'id' => c1.id, 'name' => 'c1 new name' } }
|
|
407
|
+
}
|
|
408
|
+
end
|
|
409
|
+
assert_equal(['c1 new name', c2.name],
|
|
410
|
+
tags(@parent1).map(&:name))
|
|
411
|
+
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def test_functional_update_update_stale
|
|
415
|
+
_c1, c2 = tags(@parent1)
|
|
416
|
+
|
|
417
|
+
# update for a shared entity that's no longer present in the association
|
|
418
|
+
c2.parents_tags.destroy_all
|
|
419
|
+
|
|
420
|
+
ex = assert_raises(ViewModel::DeserializationError::AssociatedNotFound) do
|
|
421
|
+
fupdate_tags(@parent1) do |tags|
|
|
422
|
+
# @parent1.parents_tags.where(tag_id: c2.id).destroy_all
|
|
423
|
+
{ :fupdate => build_fupdate { update([{ '_ref' => 'c2' }]) },
|
|
424
|
+
:refs => { 'c2' => { '_type' => 'Tag', 'id' => c2.id, 'name' => 'c2 new name' } }
|
|
425
|
+
}
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
assert_equal("tags", ex.association)
|
|
429
|
+
assert_equal([ViewModel::Reference.new(TagView, c2.id)], ex.missing_nodes)
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def test_functional_update_edit_checks
|
|
433
|
+
fupdate = build_fupdate do
|
|
434
|
+
append([{ '_ref' => 't_new' }])
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
view = { '_type' => 'Parent',
|
|
438
|
+
'id' => @parent1.id,
|
|
439
|
+
'tags' => fupdate }
|
|
440
|
+
|
|
441
|
+
refs = { 't_new' => { '_type' => 'Tag', 'name' => 'newest tag' } }
|
|
442
|
+
|
|
443
|
+
d_context = ParentView.new_deserialize_context
|
|
444
|
+
pv = ParentView.deserialize_from_view(view, references: refs, deserialize_context: d_context)
|
|
445
|
+
new_tag = pv.tags.detect { |t| t.name == 'newest tag' }
|
|
446
|
+
|
|
447
|
+
expected_edit_checks = [pv.to_reference, new_tag.to_reference]
|
|
448
|
+
assert_contains_exactly(expected_edit_checks, d_context.valid_edit_refs)
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def test_replace_associated
|
|
452
|
+
pv = ParentView.new(@parent1)
|
|
453
|
+
context = ParentView.new_deserialize_context
|
|
454
|
+
|
|
455
|
+
nc = pv.replace_associated(:tags,
|
|
456
|
+
[{ '_type' => 'Tag', 'name' => 'new_tag' }],
|
|
457
|
+
deserialize_context: context)
|
|
458
|
+
|
|
459
|
+
expected_edit_checks = [pv.to_reference,
|
|
460
|
+
*nc.map(&:to_reference)]
|
|
461
|
+
|
|
462
|
+
assert_contains_exactly(expected_edit_checks,
|
|
463
|
+
context.valid_edit_refs)
|
|
464
|
+
|
|
465
|
+
assert_equal(1, nc.size)
|
|
466
|
+
assert(nc[0].is_a?(TagView))
|
|
467
|
+
assert_equal('new_tag', nc[0].name)
|
|
468
|
+
|
|
469
|
+
@parent1.reload
|
|
470
|
+
assert_equal(['new_tag'], tags(@parent1).map(&:name))
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# Test that each of the functional updates actions work through
|
|
474
|
+
# replace_associated. The main tests for functional updates are
|
|
475
|
+
# earlier in this file.
|
|
476
|
+
def test_replace_associated_functional
|
|
477
|
+
pv = ParentView.new(@parent1)
|
|
478
|
+
context = ParentView.new_deserialize_context
|
|
479
|
+
|
|
480
|
+
tag1 = @tag1
|
|
481
|
+
tag2 = @tag2
|
|
482
|
+
|
|
483
|
+
update = build_fupdate do
|
|
484
|
+
append([{ '_type' => 'Tag', 'name' => 'new_tag' }])
|
|
485
|
+
remove([{ '_type' => 'Tag', 'id' => tag2.id }])
|
|
486
|
+
update([{ '_type' => 'Tag', 'id' => tag1.id, 'name' => 'renamed tag1' }])
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
nc = pv.replace_associated(:tags, update, deserialize_context: context)
|
|
490
|
+
new_tag = nc.detect { |t| t.name == 'new_tag' }
|
|
491
|
+
|
|
492
|
+
expected_edit_checks = [ViewModel::Reference.new(ParentView, @parent1.id),
|
|
493
|
+
ViewModel::Reference.new(TagView, @tag1.id),
|
|
494
|
+
ViewModel::Reference.new(TagView, new_tag.id)]
|
|
495
|
+
|
|
496
|
+
assert_contains_exactly(expected_edit_checks,
|
|
497
|
+
context.valid_edit_refs)
|
|
498
|
+
|
|
499
|
+
assert_equal(2, nc.size)
|
|
500
|
+
|
|
501
|
+
@parent1.reload
|
|
502
|
+
assert_equal(['renamed tag1', 'new_tag'], tags(@parent1).map(&:name))
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
def test_delete_associated_has_many
|
|
506
|
+
t1, t2 = tags(@parent1)
|
|
507
|
+
|
|
508
|
+
pv = ParentView.new(@parent1)
|
|
509
|
+
context = ParentView.new_deserialize_context
|
|
510
|
+
|
|
511
|
+
pv.delete_associated(:tags, t1.id,
|
|
512
|
+
deserialize_context: context)
|
|
513
|
+
|
|
514
|
+
expected_edit_checks = [ViewModel::Reference.new(ParentView, @parent1.id)].to_set
|
|
515
|
+
|
|
516
|
+
assert_equal(expected_edit_checks,
|
|
517
|
+
context.valid_edit_refs.to_set)
|
|
518
|
+
|
|
519
|
+
@parent1.reload
|
|
520
|
+
assert_equal([t2], tags(@parent1))
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
def test_append_associated_move_has_many
|
|
524
|
+
pv = ParentView.new(@parent1)
|
|
525
|
+
|
|
526
|
+
expected_edit_checks = [ViewModel::Reference.new(ParentView, @parent1.id)].to_set
|
|
527
|
+
|
|
528
|
+
# insert before
|
|
529
|
+
pv.append_associated(:tags,
|
|
530
|
+
{ '_type' => 'Tag', 'id' => @tag2.id },
|
|
531
|
+
before: ViewModel::Reference.new(TagView, @tag1.id),
|
|
532
|
+
deserialize_context: (context = ParentView.new_deserialize_context))
|
|
533
|
+
|
|
534
|
+
assert_equal(expected_edit_checks, context.valid_edit_refs.to_set)
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
assert_equal([@tag2, @tag1],
|
|
538
|
+
tags(@parent1))
|
|
539
|
+
|
|
540
|
+
# insert after
|
|
541
|
+
pv.append_associated(:tags,
|
|
542
|
+
{ '_type' => 'Tag', 'id' => @tag2.id },
|
|
543
|
+
after: ViewModel::Reference.new(TagView, @tag1.id),
|
|
544
|
+
deserialize_context: (context = ParentView.new_deserialize_context))
|
|
545
|
+
|
|
546
|
+
assert_equal(expected_edit_checks, context.valid_edit_refs.to_set)
|
|
547
|
+
|
|
548
|
+
assert_equal([@tag1, @tag2],
|
|
549
|
+
tags(@parent1))
|
|
550
|
+
|
|
551
|
+
# append
|
|
552
|
+
pv.append_associated(:tags,
|
|
553
|
+
{ '_type' => 'Tag', 'id' => @tag1.id },
|
|
554
|
+
deserialize_context: (context = ParentView.new_deserialize_context))
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
assert_equal([@tag2, @tag1],
|
|
558
|
+
tags(@parent1))
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def test_append_associated_insert_has_many
|
|
562
|
+
pv = ParentView.new(@parent1)
|
|
563
|
+
|
|
564
|
+
expected_edit_checks = [ViewModel::Reference.new(ParentView, @parent1.id)].to_set
|
|
565
|
+
|
|
566
|
+
# insert before
|
|
567
|
+
pv.append_associated(:tags,
|
|
568
|
+
{ '_type' => 'Tag', 'id' => @tag3.id },
|
|
569
|
+
before: ViewModel::Reference.new(TagView, @tag1.id),
|
|
570
|
+
deserialize_context: (context = ParentView.new_deserialize_context))
|
|
571
|
+
|
|
572
|
+
assert_equal(expected_edit_checks, context.valid_edit_refs.to_set)
|
|
573
|
+
|
|
574
|
+
assert_equal([@tag3, @tag1, @tag2],
|
|
575
|
+
tags(@parent1))
|
|
576
|
+
|
|
577
|
+
@parent1.parents_tags.where(tag_id: @tag3.id).destroy_all
|
|
578
|
+
|
|
579
|
+
# insert after
|
|
580
|
+
pv.append_associated(:tags,
|
|
581
|
+
{ '_type' => 'Tag', 'id' => @tag3.id },
|
|
582
|
+
after: ViewModel::Reference.new(TagView, @tag1.id),
|
|
583
|
+
deserialize_context: (context = ParentView.new_deserialize_context))
|
|
584
|
+
|
|
585
|
+
assert_equal(expected_edit_checks, context.valid_edit_refs.to_set)
|
|
586
|
+
|
|
587
|
+
assert_equal([@tag1, @tag3, @tag2],
|
|
588
|
+
tags(@parent1))
|
|
589
|
+
|
|
590
|
+
@parent1.parents_tags.where(tag_id: @tag3.id).destroy_all
|
|
591
|
+
|
|
592
|
+
# append
|
|
593
|
+
pv.append_associated(:tags,
|
|
594
|
+
{ '_type' => 'Tag', 'id' => @tag3.id },
|
|
595
|
+
deserialize_context: (context = ParentView.new_deserialize_context))
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
assert_equal([@tag1, @tag2, @tag3],
|
|
599
|
+
tags(@parent1))
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
class RenamingTest < ActiveSupport::TestCase
|
|
603
|
+
include ARVMTestUtilities
|
|
604
|
+
|
|
605
|
+
def before_all
|
|
606
|
+
super
|
|
607
|
+
|
|
608
|
+
ViewModel::ActiveRecord::HasManyThroughTest.build_tag(self)
|
|
609
|
+
|
|
610
|
+
build_viewmodel(:Parent) do
|
|
611
|
+
define_schema do |t|
|
|
612
|
+
t.string :name
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
define_model do
|
|
616
|
+
has_many :parents_tags, dependent: :destroy, inverse_of: :parent
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
define_viewmodel do
|
|
620
|
+
attributes :name
|
|
621
|
+
association :tags, shared: true, through: :parents_tags, through_order_attr: :position, as: :something_else
|
|
622
|
+
end
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
ViewModel::ActiveRecord::HasManyThroughTest.build_join_table_model(self)
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def setup
|
|
630
|
+
super
|
|
631
|
+
|
|
632
|
+
@parent = Parent.create(parents_tags: [ParentsTag.new(tag: Tag.new(name: 'tag name'))])
|
|
633
|
+
|
|
634
|
+
enable_logging!
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
def test_dependencies
|
|
638
|
+
root_updates, _ref_updates = ViewModel::ActiveRecord::UpdateData.parse_hashes([{ '_type' => 'Parent', 'something_else' => [] }])
|
|
639
|
+
assert_equal(DeepPreloader::Spec.new('parents_tags' => DeepPreloader::Spec.new('tag' => DeepPreloader::Spec.new)),
|
|
640
|
+
root_updates.first.preload_dependencies)
|
|
641
|
+
assert_equal({ 'something_else' => {} }, root_updates.first.updated_associations)
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
def test_renamed_roundtrip
|
|
645
|
+
context = ParentView.new_serialize_context(include: :something_else)
|
|
646
|
+
alter_by_view!(ParentView, @parent, serialize_context: context) do |view, refs|
|
|
647
|
+
assert_equal({refs.keys.first => { 'id' => @parent.parents_tags.first.tag.id,
|
|
648
|
+
'_type' => 'Tag',
|
|
649
|
+
'_version' => 1,
|
|
650
|
+
'name' => 'tag name' }}, refs)
|
|
651
|
+
assert_equal([{ '_ref' => refs.keys.first }],
|
|
652
|
+
view['something_else'])
|
|
653
|
+
|
|
654
|
+
refs.clear
|
|
655
|
+
refs['new'] = {'_type' => 'Tag', 'name' => 'tag new name'}
|
|
656
|
+
view['something_else'] = [{'_ref' => 'new'}]
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
assert_equal('tag new name', @parent.parents_tags.first.tag.name)
|
|
660
|
+
end
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
class WithChildTagTest < ActiveSupport::TestCase
|
|
664
|
+
include ARVMTestUtilities
|
|
665
|
+
|
|
666
|
+
def before_all
|
|
667
|
+
super
|
|
668
|
+
|
|
669
|
+
container = ViewModel::ActiveRecord::HasManyThroughTest
|
|
670
|
+
container.build_parent(self)
|
|
671
|
+
container.build_tag(self, with: [:ChildTag])
|
|
672
|
+
container.build_childtag(self)
|
|
673
|
+
container.build_join_table_model(self)
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
def test_preload_dependencies
|
|
677
|
+
root_updates, _ref_updates = ViewModel::ActiveRecord::UpdateData.parse_hashes([{ '_type' => 'Parent' }])
|
|
678
|
+
assert_equal(DeepPreloader::Spec.new,
|
|
679
|
+
root_updates.first.preload_dependencies,
|
|
680
|
+
'nothing loaded by default')
|
|
681
|
+
|
|
682
|
+
root_updates, _ref_updates = ViewModel::ActiveRecord::UpdateData.parse_hashes(
|
|
683
|
+
[{ '_type' => 'Parent',
|
|
684
|
+
'tags' => [{ '_ref' => 'r1' }] }],
|
|
685
|
+
{ 'r1' => { '_type' => 'Tag', 'child_tags' => [] } })
|
|
686
|
+
|
|
687
|
+
assert_equal(DeepPreloader::Spec.new('parents_tags' => DeepPreloader::Spec.new('tag' => DeepPreloader::Spec.new)),
|
|
688
|
+
root_updates.first.preload_dependencies,
|
|
689
|
+
'mentioning tags and child_tags causes through association loading, excluding shared')
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
def test_preload_dependencies_functional
|
|
693
|
+
fupdate = build_fupdate do
|
|
694
|
+
append([{ '_ref' => 'r1' }])
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
root_updates, _ref_updates = ViewModel::ActiveRecord::UpdateData.parse_hashes(
|
|
698
|
+
[{ '_type' => 'Parent',
|
|
699
|
+
'tags' => fupdate }],
|
|
700
|
+
{ 'r1' => { '_type' => 'Tag', 'child_tags' => [] } })
|
|
701
|
+
|
|
702
|
+
assert_equal(DeepPreloader::Spec.new('parents_tags' => DeepPreloader::Spec.new('tag' => DeepPreloader::Spec.new)),
|
|
703
|
+
root_updates.first.preload_dependencies,
|
|
704
|
+
'mentioning tags and child_tags in functional update value causes through association loading, ' \
|
|
705
|
+
'excluding shared')
|
|
706
|
+
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
def test_updated_associations
|
|
710
|
+
root_updates, _ref_updates = ViewModel::ActiveRecord::UpdateData.parse_hashes(
|
|
711
|
+
[{ '_type' => 'Parent',
|
|
712
|
+
'tags' => [{ '_ref' => 'r1' }] }],
|
|
713
|
+
{ 'r1' => { '_type' => 'Tag', 'child_tags' => [] } })
|
|
714
|
+
|
|
715
|
+
assert_equal({ 'tags' => { } },
|
|
716
|
+
root_updates.first.updated_associations,
|
|
717
|
+
'mentioning tags and child_tags causes through association loading, excluding shared')
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
def test_updated_associations_functional
|
|
721
|
+
fupdate = build_fupdate do
|
|
722
|
+
append([{ '_ref' => 'r1' }])
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
root_updates, _ref_updates = ViewModel::ActiveRecord::UpdateData.parse_hashes(
|
|
726
|
+
[{ '_type' => 'Parent',
|
|
727
|
+
'tags' => fupdate }],
|
|
728
|
+
{ 'r1' => { '_type' => 'Tag', 'child_tags' => [] } })
|
|
729
|
+
|
|
730
|
+
assert_equal({ 'tags' => { } },
|
|
731
|
+
root_updates.first.updated_associations,
|
|
732
|
+
'mentioning tags and child_tags in functional_update causes through association loading, ' \
|
|
733
|
+
'excluding shared')
|
|
734
|
+
end
|
|
735
|
+
end
|
|
736
|
+
end
|