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.
Files changed (92) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +115 -0
  3. data/.gitignore +36 -0
  4. data/.travis.yml +31 -0
  5. data/Appraisals +9 -0
  6. data/Gemfile +19 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +19 -0
  9. data/Rakefile +21 -0
  10. data/appveyor.yml +22 -0
  11. data/gemfiles/rails_5_2.gemfile +15 -0
  12. data/gemfiles/rails_6_0_beta.gemfile +15 -0
  13. data/iknow_view_models.gemspec +49 -0
  14. data/lib/iknow_view_models.rb +12 -0
  15. data/lib/iknow_view_models/railtie.rb +8 -0
  16. data/lib/iknow_view_models/version.rb +5 -0
  17. data/lib/view_model.rb +333 -0
  18. data/lib/view_model/access_control.rb +154 -0
  19. data/lib/view_model/access_control/composed.rb +216 -0
  20. data/lib/view_model/access_control/open.rb +13 -0
  21. data/lib/view_model/access_control/read_only.rb +13 -0
  22. data/lib/view_model/access_control/tree.rb +264 -0
  23. data/lib/view_model/access_control_error.rb +10 -0
  24. data/lib/view_model/active_record.rb +383 -0
  25. data/lib/view_model/active_record/association_data.rb +178 -0
  26. data/lib/view_model/active_record/association_manipulation.rb +389 -0
  27. data/lib/view_model/active_record/cache.rb +265 -0
  28. data/lib/view_model/active_record/cache/cacheable_view.rb +51 -0
  29. data/lib/view_model/active_record/cloner.rb +113 -0
  30. data/lib/view_model/active_record/collection_nested_controller.rb +100 -0
  31. data/lib/view_model/active_record/controller.rb +77 -0
  32. data/lib/view_model/active_record/controller_base.rb +185 -0
  33. data/lib/view_model/active_record/nested_controller_base.rb +93 -0
  34. data/lib/view_model/active_record/singular_nested_controller.rb +34 -0
  35. data/lib/view_model/active_record/update_context.rb +252 -0
  36. data/lib/view_model/active_record/update_data.rb +749 -0
  37. data/lib/view_model/active_record/update_operation.rb +810 -0
  38. data/lib/view_model/active_record/visitor.rb +77 -0
  39. data/lib/view_model/after_transaction_runner.rb +29 -0
  40. data/lib/view_model/callbacks.rb +219 -0
  41. data/lib/view_model/changes.rb +62 -0
  42. data/lib/view_model/config.rb +29 -0
  43. data/lib/view_model/controller.rb +142 -0
  44. data/lib/view_model/deserialization_error.rb +437 -0
  45. data/lib/view_model/deserialize_context.rb +16 -0
  46. data/lib/view_model/error.rb +191 -0
  47. data/lib/view_model/error_view.rb +35 -0
  48. data/lib/view_model/record.rb +367 -0
  49. data/lib/view_model/record/attribute_data.rb +48 -0
  50. data/lib/view_model/reference.rb +31 -0
  51. data/lib/view_model/references.rb +48 -0
  52. data/lib/view_model/registry.rb +73 -0
  53. data/lib/view_model/schemas.rb +45 -0
  54. data/lib/view_model/serialization_error.rb +10 -0
  55. data/lib/view_model/serialize_context.rb +118 -0
  56. data/lib/view_model/test_helpers.rb +103 -0
  57. data/lib/view_model/test_helpers/arvm_builder.rb +111 -0
  58. data/lib/view_model/traversal_context.rb +126 -0
  59. data/lib/view_model/utils.rb +24 -0
  60. data/lib/view_model/utils/collections.rb +49 -0
  61. data/test/helpers/arvm_test_models.rb +59 -0
  62. data/test/helpers/arvm_test_utilities.rb +187 -0
  63. data/test/helpers/callback_tracer.rb +27 -0
  64. data/test/helpers/controller_test_helpers.rb +270 -0
  65. data/test/helpers/match_enumerator.rb +58 -0
  66. data/test/helpers/query_logging.rb +71 -0
  67. data/test/helpers/test_access_control.rb +56 -0
  68. data/test/helpers/viewmodel_spec_helpers.rb +326 -0
  69. data/test/unit/view_model/access_control_test.rb +769 -0
  70. data/test/unit/view_model/active_record/alias_test.rb +35 -0
  71. data/test/unit/view_model/active_record/belongs_to_test.rb +376 -0
  72. data/test/unit/view_model/active_record/cache_test.rb +351 -0
  73. data/test/unit/view_model/active_record/cloner_test.rb +313 -0
  74. data/test/unit/view_model/active_record/controller_test.rb +561 -0
  75. data/test/unit/view_model/active_record/counter_test.rb +80 -0
  76. data/test/unit/view_model/active_record/customization_test.rb +388 -0
  77. data/test/unit/view_model/active_record/has_many_test.rb +957 -0
  78. data/test/unit/view_model/active_record/has_many_through_poly_test.rb +269 -0
  79. data/test/unit/view_model/active_record/has_many_through_test.rb +736 -0
  80. data/test/unit/view_model/active_record/has_one_test.rb +334 -0
  81. data/test/unit/view_model/active_record/namespacing_test.rb +75 -0
  82. data/test/unit/view_model/active_record/optional_attribute_view_test.rb +58 -0
  83. data/test/unit/view_model/active_record/poly_test.rb +320 -0
  84. data/test/unit/view_model/active_record/shared_test.rb +285 -0
  85. data/test/unit/view_model/active_record/version_test.rb +121 -0
  86. data/test/unit/view_model/active_record_test.rb +542 -0
  87. data/test/unit/view_model/callbacks_test.rb +582 -0
  88. data/test/unit/view_model/deserialization_error/unique_violation_test.rb +73 -0
  89. data/test/unit/view_model/record_test.rb +524 -0
  90. data/test/unit/view_model/traversal_context_test.rb +371 -0
  91. data/test/unit/view_model_test.rb +62 -0
  92. 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