iknow_view_models 2.9.0 → 3.0.0
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/iknow_view_models.gemspec +1 -1
- data/lib/iknow_view_models/version.rb +1 -1
- data/lib/view_model/active_record/association_data.rb +206 -92
- data/lib/view_model/active_record/association_manipulation.rb +22 -12
- data/lib/view_model/active_record/cache/cacheable_view.rb +3 -13
- data/lib/view_model/active_record/cache.rb +2 -2
- data/lib/view_model/active_record/cloner.rb +11 -11
- data/lib/view_model/active_record/controller.rb +0 -2
- data/lib/view_model/active_record/update_context.rb +21 -3
- data/lib/view_model/active_record/update_data.rb +43 -45
- data/lib/view_model/active_record/update_operation.rb +265 -153
- data/lib/view_model/active_record/visitor.rb +9 -6
- data/lib/view_model/active_record.rb +94 -74
- data/lib/view_model/after_transaction_runner.rb +3 -18
- data/lib/view_model/changes.rb +24 -16
- data/lib/view_model/config.rb +6 -2
- data/lib/view_model/deserialization_error.rb +31 -0
- data/lib/view_model/deserialize_context.rb +2 -6
- data/lib/view_model/error_view.rb +6 -5
- data/lib/view_model/record/attribute_data.rb +11 -6
- data/lib/view_model/record.rb +44 -24
- data/lib/view_model/serialize_context.rb +2 -63
- data/lib/view_model.rb +17 -8
- data/shell.nix +1 -1
- data/test/helpers/arvm_test_utilities.rb +6 -0
- data/test/helpers/controller_test_helpers.rb +5 -3
- data/test/helpers/viewmodel_spec_helpers.rb +63 -52
- data/test/unit/view_model/access_control_test.rb +88 -37
- data/test/unit/view_model/active_record/belongs_to_test.rb +110 -178
- data/test/unit/view_model/active_record/cache_test.rb +3 -2
- data/test/unit/view_model/active_record/cloner_test.rb +1 -1
- data/test/unit/view_model/active_record/controller_test.rb +12 -20
- data/test/unit/view_model/active_record/has_many_test.rb +540 -316
- data/test/unit/view_model/active_record/has_many_through_poly_test.rb +12 -15
- data/test/unit/view_model/active_record/has_many_through_test.rb +15 -58
- data/test/unit/view_model/active_record/has_one_test.rb +288 -135
- data/test/unit/view_model/active_record/poly_test.rb +0 -1
- data/test/unit/view_model/active_record/shared_test.rb +21 -39
- data/test/unit/view_model/active_record/version_test.rb +3 -2
- data/test/unit/view_model/active_record_test.rb +5 -63
- data/test/unit/view_model/callbacks_test.rb +1 -0
- data/test/unit/view_model/record_test.rb +0 -32
- data/test/unit/view_model/traversal_context_test.rb +13 -12
- metadata +5 -8
- data/test/unit/view_model/active_record/optional_attribute_view_test.rb +0 -58
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'active_support/core_ext'
|
2
4
|
require 'view_model/traversal_context'
|
3
5
|
|
@@ -17,52 +19,6 @@ class ViewModel::SerializeContext < ViewModel::TraversalContext
|
|
17
19
|
end
|
18
20
|
|
19
21
|
delegate :references, :flatten_references, to: :shared_context
|
20
|
-
|
21
|
-
attr_reader :include, :prune
|
22
|
-
|
23
|
-
def initialize(include: nil, prune: nil, **rest)
|
24
|
-
super(**rest)
|
25
|
-
@include = self.class.normalize_includes(include)
|
26
|
-
@prune = self.class.normalize_includes(prune)
|
27
|
-
end
|
28
|
-
|
29
|
-
def initialize_as_child(include:, prune:, **rest)
|
30
|
-
super(**rest)
|
31
|
-
@include = include
|
32
|
-
@prune = prune
|
33
|
-
end
|
34
|
-
|
35
|
-
def for_child(parent_viewmodel, association_name:, **rest)
|
36
|
-
super(parent_viewmodel,
|
37
|
-
association_name: association_name,
|
38
|
-
include: @include.try { |i| i[association_name] },
|
39
|
-
prune: @prune.try { |p| p[association_name] },
|
40
|
-
**rest)
|
41
|
-
end
|
42
|
-
|
43
|
-
def includes_member?(member_name, default)
|
44
|
-
member_name = member_name.to_s
|
45
|
-
|
46
|
-
# Every node in the include tree is to be included
|
47
|
-
included = @include.try { |is| is.has_key?(member_name) }
|
48
|
-
# whereas only the leaves of the prune tree are to be removed
|
49
|
-
pruned = @prune.try { |ps| ps.fetch(member_name, :sentinel).nil? }
|
50
|
-
|
51
|
-
(default || included) && !pruned
|
52
|
-
end
|
53
|
-
|
54
|
-
def add_includes(includes)
|
55
|
-
return if includes.blank?
|
56
|
-
@include ||= {}
|
57
|
-
@include.deep_merge!(self.class.normalize_includes(includes))
|
58
|
-
end
|
59
|
-
|
60
|
-
def add_prunes(prunes)
|
61
|
-
return if prunes.blank?
|
62
|
-
@prune ||= {}
|
63
|
-
@prune.deep_merge!(self.class.normalize_includes(prunes))
|
64
|
-
end
|
65
|
-
|
66
22
|
delegate :add_reference, :has_references?, to: :references
|
67
23
|
|
68
24
|
# Return viewmodels referenced during serialization and clear @references.
|
@@ -98,21 +54,4 @@ class ViewModel::SerializeContext < ViewModel::TraversalContext
|
|
98
54
|
def serialize_references_to_hash
|
99
55
|
Jbuilder.new { |json| serialize_references(json) }.attributes!
|
100
56
|
end
|
101
|
-
|
102
|
-
def self.normalize_includes(includes)
|
103
|
-
case includes
|
104
|
-
when Array
|
105
|
-
includes.each_with_object({}) do |v, new_includes|
|
106
|
-
new_includes.merge!(normalize_includes(v))
|
107
|
-
end
|
108
|
-
when Hash
|
109
|
-
includes.each_with_object({}) do |(k,v), new_includes|
|
110
|
-
new_includes[k.to_s] = normalize_includes(v)
|
111
|
-
end
|
112
|
-
when nil
|
113
|
-
nil
|
114
|
-
else
|
115
|
-
{ includes.to_s => nil }
|
116
|
-
end
|
117
|
-
end
|
118
57
|
end
|
data/lib/view_model.rb
CHANGED
@@ -20,6 +20,7 @@ class ViewModel
|
|
20
20
|
attr_accessor :_attributes
|
21
21
|
attr_accessor :schema_version
|
22
22
|
attr_reader :view_aliases
|
23
|
+
attr_writer :view_name
|
23
24
|
|
24
25
|
def inherited(subclass)
|
25
26
|
subclass.initialize_as_viewmodel
|
@@ -41,15 +42,23 @@ class ViewModel
|
|
41
42
|
end
|
42
43
|
end
|
43
44
|
|
44
|
-
def view_name=(name)
|
45
|
-
@view_name = name
|
46
|
-
end
|
47
|
-
|
48
45
|
def add_view_alias(as)
|
49
46
|
view_aliases << as
|
50
47
|
ViewModel::Registry.register(self, as: as)
|
51
48
|
end
|
52
49
|
|
50
|
+
# ViewModels are either roots or children. Root viewmodels may be
|
51
|
+
# (de)serialized directly, whereas child viewmodels are always nested within
|
52
|
+
# their parent. Associations to root viewmodel types always use indirect
|
53
|
+
# references.
|
54
|
+
def root?
|
55
|
+
false
|
56
|
+
end
|
57
|
+
|
58
|
+
def root!
|
59
|
+
define_singleton_method(:root?) { true }
|
60
|
+
end
|
61
|
+
|
53
62
|
# ViewModels are typically going to be pretty simple structures. Make it a
|
54
63
|
# bit easier to define them: attributes specified this way are given
|
55
64
|
# accessors and assigned in order by the default constructor.
|
@@ -59,7 +68,7 @@ class ViewModel
|
|
59
68
|
|
60
69
|
def attribute(attr, **_args)
|
61
70
|
unless attr.is_a?(Symbol)
|
62
|
-
raise ArgumentError.new(
|
71
|
+
raise ArgumentError.new('ViewModel attributes must be symbols')
|
63
72
|
end
|
64
73
|
|
65
74
|
attr_accessor attr
|
@@ -116,7 +125,7 @@ class ViewModel
|
|
116
125
|
# If this viewmodel represents an AR model, what associations does it make
|
117
126
|
# use of? Returns a includes spec appropriate for DeepPreloader, either as
|
118
127
|
# AR-style nested hashes or DeepPreloader::Spec.
|
119
|
-
def eager_includes(serialize_context: new_serialize_context,
|
128
|
+
def eager_includes(serialize_context: new_serialize_context, include_referenced: true)
|
120
129
|
{}
|
121
130
|
end
|
122
131
|
|
@@ -225,10 +234,10 @@ class ViewModel
|
|
225
234
|
schema_version == self.schema_version
|
226
235
|
end
|
227
236
|
|
228
|
-
def preload_for_serialization(viewmodels, serialize_context: new_serialize_context,
|
237
|
+
def preload_for_serialization(viewmodels, serialize_context: new_serialize_context, include_referenced: true, lock: nil)
|
229
238
|
Array.wrap(viewmodels).group_by(&:class).each do |type, views|
|
230
239
|
DeepPreloader.preload(views.map(&:model),
|
231
|
-
type.eager_includes(serialize_context: serialize_context,
|
240
|
+
type.eager_includes(serialize_context: serialize_context, include_referenced: include_referenced),
|
232
241
|
lock: lock)
|
233
242
|
end
|
234
243
|
end
|
data/shell.nix
CHANGED
@@ -4,6 +4,12 @@ require 'minitest/hooks'
|
|
4
4
|
require 'view_model'
|
5
5
|
require 'view_model/test_helpers'
|
6
6
|
|
7
|
+
unless ViewModel::Config.configured?
|
8
|
+
ViewModel::Config.configure! do
|
9
|
+
debug_deserialization true
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
7
13
|
require_relative 'query_logging.rb'
|
8
14
|
|
9
15
|
ActiveSupport::TestCase.include(Minitest::Hooks)
|
@@ -33,6 +33,7 @@ module ControllerTestModels
|
|
33
33
|
has_many :parents
|
34
34
|
end
|
35
35
|
define_viewmodel do
|
36
|
+
root!
|
36
37
|
attributes :name
|
37
38
|
end
|
38
39
|
end
|
@@ -77,11 +78,12 @@ module ControllerTestModels
|
|
77
78
|
belongs_to :category
|
78
79
|
end
|
79
80
|
define_viewmodel do
|
81
|
+
root!
|
80
82
|
attributes :name
|
81
83
|
associations :label, :target
|
82
|
-
association :children
|
84
|
+
association :children
|
83
85
|
association :poly, viewmodels: [:PolyOne, :PolyTwo]
|
84
|
-
association :category,
|
86
|
+
association :category, external: true
|
85
87
|
end
|
86
88
|
end
|
87
89
|
|
@@ -99,7 +101,7 @@ module ControllerTestModels
|
|
99
101
|
define_model do
|
100
102
|
belongs_to :parent, inverse_of: :children
|
101
103
|
acts_as_manual_list scope: :parent
|
102
|
-
validates :age, numericality: {less_than: 42}, allow_nil: true
|
104
|
+
validates :age, numericality: { less_than: 42 }, allow_nil: true
|
103
105
|
end
|
104
106
|
define_viewmodel do
|
105
107
|
attributes :name, :age
|
@@ -77,7 +77,7 @@ module ViewModelSpecHelpers
|
|
77
77
|
ViewModel::TestHelpers::ARVMBuilder::Spec.new(
|
78
78
|
schema: ->(t) { t.string :name },
|
79
79
|
model: ->(m) {},
|
80
|
-
viewmodel: ->(v) { attribute :name }
|
80
|
+
viewmodel: ->(v) { root!; attribute :name }
|
81
81
|
)
|
82
82
|
end
|
83
83
|
|
@@ -99,6 +99,10 @@ module ViewModelSpecHelpers
|
|
99
99
|
raise RuntimeError.new('Test model does not have a child association')
|
100
100
|
end
|
101
101
|
|
102
|
+
def subject_association_features
|
103
|
+
{}
|
104
|
+
end
|
105
|
+
|
102
106
|
def subject_association_name
|
103
107
|
subject_association.association_name
|
104
108
|
end
|
@@ -114,9 +118,10 @@ module ViewModelSpecHelpers
|
|
114
118
|
include ViewModelSpecHelpers::Base
|
115
119
|
|
116
120
|
def model_attributes
|
121
|
+
f = subject_association_features
|
117
122
|
super.merge(schema: ->(t) { t.references :child, foreign_key: true },
|
118
123
|
model: ->(m) { belongs_to :child, inverse_of: :model, dependent: :destroy },
|
119
|
-
viewmodel: ->(v) { association :child })
|
124
|
+
viewmodel: ->(v) { association :child, **f })
|
120
125
|
end
|
121
126
|
|
122
127
|
def child_attributes
|
@@ -134,17 +139,33 @@ module ViewModelSpecHelpers
|
|
134
139
|
end
|
135
140
|
end
|
136
141
|
|
142
|
+
module ParentAndSharedBelongsToChild
|
143
|
+
extend ActiveSupport::Concern
|
144
|
+
include ViewModelSpecHelpers::ParentAndBelongsToChild
|
145
|
+
def child_attributes
|
146
|
+
super.merge(viewmodel: ->(v) { root! })
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
137
150
|
module List
|
138
151
|
extend ActiveSupport::Concern
|
139
152
|
include ViewModelSpecHelpers::Base
|
140
153
|
|
141
154
|
def model_attributes
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
155
|
+
ViewModel::TestHelpers::ARVMBuilder::Spec.new(
|
156
|
+
schema: ->(t) {
|
157
|
+
t.string :name
|
158
|
+
t.integer :next_id
|
159
|
+
},
|
160
|
+
model: ->(m) {
|
161
|
+
belongs_to :next, class_name: self.name, inverse_of: :previous, dependent: :destroy
|
162
|
+
has_one :previous, class_name: self.name, foreign_key: :next_id, inverse_of: :next
|
163
|
+
},
|
164
|
+
viewmodel: ->(v) {
|
165
|
+
# Not a root
|
166
|
+
association :next
|
167
|
+
attribute :name
|
168
|
+
})
|
148
169
|
end
|
149
170
|
|
150
171
|
def subject_association
|
@@ -157,9 +178,10 @@ module ViewModelSpecHelpers
|
|
157
178
|
include ViewModelSpecHelpers::Base
|
158
179
|
|
159
180
|
def model_attributes
|
181
|
+
f = subject_association_features
|
160
182
|
super.merge(
|
161
183
|
model: ->(m) { has_one :child, inverse_of: :model, dependent: :destroy },
|
162
|
-
viewmodel: ->(v) { association :child }
|
184
|
+
viewmodel: ->(v) { association :child, **f }
|
163
185
|
)
|
164
186
|
end
|
165
187
|
|
@@ -181,14 +203,23 @@ module ViewModelSpecHelpers
|
|
181
203
|
end
|
182
204
|
end
|
183
205
|
|
206
|
+
module ParentAndReferencedHasOneChild
|
207
|
+
extend ActiveSupport::Concern
|
208
|
+
include ViewModelSpecHelpers::ParentAndHasOneChild
|
209
|
+
def child_attributes
|
210
|
+
super.merge(viewmodel: ->(v) { root! })
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
184
214
|
module ParentAndHasManyChildren
|
185
215
|
extend ActiveSupport::Concern
|
186
216
|
include ViewModelSpecHelpers::Base
|
187
217
|
|
188
218
|
def model_attributes
|
219
|
+
f = subject_association_features
|
189
220
|
super.merge(
|
190
221
|
model: ->(m) { has_many :children, inverse_of: :model, dependent: :destroy },
|
191
|
-
viewmodel: ->(v) { association :children }
|
222
|
+
viewmodel: ->(v) { association :children, **f }
|
192
223
|
)
|
193
224
|
end
|
194
225
|
|
@@ -210,22 +241,23 @@ module ViewModelSpecHelpers
|
|
210
241
|
end
|
211
242
|
end
|
212
243
|
|
213
|
-
module
|
244
|
+
module ParentAndSharedHasManyChildren
|
214
245
|
extend ActiveSupport::Concern
|
215
|
-
include ViewModelSpecHelpers::
|
216
|
-
|
217
|
-
|
218
|
-
super.merge(
|
219
|
-
model: ->(m) { has_many :children, inverse_of: :model, dependent: :destroy },
|
220
|
-
viewmodel: ->(v) { association :children },
|
221
|
-
)
|
246
|
+
include ViewModelSpecHelpers::ParentAndHasManyChildren
|
247
|
+
def child_attributes
|
248
|
+
super.merge(viewmodel: ->(v) { root! })
|
222
249
|
end
|
250
|
+
end
|
251
|
+
|
252
|
+
module ParentAndOrderedChildren
|
253
|
+
extend ActiveSupport::Concern
|
254
|
+
include ViewModelSpecHelpers::ParentAndHasManyChildren
|
223
255
|
|
224
256
|
def child_attributes
|
225
257
|
super.merge(
|
226
|
-
schema: ->(t) { t.
|
227
|
-
model: ->(
|
228
|
-
viewmodel: ->(
|
258
|
+
schema: ->(t) { t.float :position, null: false },
|
259
|
+
model: ->(_m) { acts_as_manual_list scope: :model },
|
260
|
+
viewmodel: ->(_v) { acts_as_list :position },
|
229
261
|
)
|
230
262
|
end
|
231
263
|
|
@@ -233,7 +265,7 @@ module ViewModelSpecHelpers
|
|
233
265
|
# child depends on parent, ensure it's touched first
|
234
266
|
viewmodel_class
|
235
267
|
|
236
|
-
# Add a deferrable unique position
|
268
|
+
# Add a deferrable unique position constraint
|
237
269
|
super do |klass|
|
238
270
|
model = klass.model_class
|
239
271
|
table = model.table_name
|
@@ -242,38 +274,15 @@ module ViewModelSpecHelpers
|
|
242
274
|
SQL
|
243
275
|
end
|
244
276
|
end
|
245
|
-
|
246
|
-
def subject_association
|
247
|
-
viewmodel_class._association_data('children')
|
248
|
-
end
|
249
277
|
end
|
250
278
|
|
251
|
-
module ParentAndSharedChild
|
252
|
-
extend ActiveSupport::Concern
|
253
|
-
include ViewModelSpecHelpers::Base
|
254
279
|
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
model: ->(m) { belongs_to :child, inverse_of: :model, dependent: :destroy },
|
259
|
-
viewmodel: ->(v) { association :child, shared: true }
|
260
|
-
)
|
261
|
-
end
|
262
|
-
|
263
|
-
def child_attributes
|
264
|
-
super.merge(
|
265
|
-
model: ->(m) { has_one :model, inverse_of: :child }
|
266
|
-
)
|
267
|
-
end
|
268
|
-
|
269
|
-
# parent depends on child, ensure it's touched first
|
270
|
-
def viewmodel_class
|
271
|
-
child_viewmodel_class
|
272
|
-
super
|
273
|
-
end
|
280
|
+
module ParentAndExternalSharedChild
|
281
|
+
extend ActiveSupport::Concern
|
282
|
+
include ViewModelSpecHelpers::ParentAndSharedBelongsToChild
|
274
283
|
|
275
|
-
def
|
276
|
-
|
284
|
+
def subject_association_features
|
285
|
+
{ external: true }
|
277
286
|
end
|
278
287
|
end
|
279
288
|
|
@@ -282,15 +291,17 @@ module ViewModelSpecHelpers
|
|
282
291
|
include ViewModelSpecHelpers::Base
|
283
292
|
|
284
293
|
def model_attributes
|
294
|
+
f = subject_association_features
|
285
295
|
super.merge(
|
286
296
|
model: ->(m) { has_many :model_children, inverse_of: :model, dependent: :destroy },
|
287
|
-
viewmodel: ->(v) { association :children,
|
297
|
+
viewmodel: ->(v) { association :children, through: :model_children, through_order_attr: :position, **f }
|
288
298
|
)
|
289
299
|
end
|
290
300
|
|
291
301
|
def child_attributes
|
292
302
|
super.merge(
|
293
|
-
model: ->(m) { has_many :model_children, inverse_of: :child, dependent: :destroy }
|
303
|
+
model: ->(m) { has_many :model_children, inverse_of: :child, dependent: :destroy },
|
304
|
+
viewmodel: ->(v) { root! }
|
294
305
|
)
|
295
306
|
end
|
296
307
|
|