iknow_view_models 3.13.0 → 3.14.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/.github/workflows/test.yml +2 -6
- data/iknow_view_models.gemspec +3 -3
- data/lib/iknow_view_models/version.rb +1 -1
- data/lib/view_model/active_record/update_context.rb +5 -3
- data/lib/view_model/active_record/update_operation.rb +119 -124
- data/lib/view_model/active_record.rb +10 -14
- data/lib/view_model/deserialization_error.rb +14 -4
- data/lib/view_model/error_wrapping.rb +23 -0
- data/nix/dependencies.nix +1 -2
- data/shell.nix +1 -3
- data/test/helpers/arvm_test_utilities.rb +1 -0
- data/test/unit/view_model/error_wrapping_test.rb +129 -0
- metadata +14 -13
- data/gemfiles/rails_5_2.gemfile +0 -10
- data/gemfiles/rails_6_0.gemfile +0 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b6b63efe5e196fe293c68497b57f53356366632e08f3b2ed97b2240ceb1e2f96
|
4
|
+
data.tar.gz: e8c8d932be65e4f811ac1f80b45338a66681ad6c9b3e5e2957ce123ccff4b70c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7e050339a0ac0d06d70e55edc30a2f7a8afe2a58d64f933cc48de18de9c7f98a1fc21b0fe50666b459cf3971cc89b32c75b72fb76c30e6cf3c3f9294e7a864b4
|
7
|
+
data.tar.gz: 8c7fb549e253196df7ab6ca2e56f0659cff104e0e3e9c58806d3681b67ef8c69ecfdae8a781110768249f738ff69e81a0b86360c66f45fb7536ef9bf9ac3fced
|
data/.github/workflows/test.yml
CHANGED
@@ -26,12 +26,8 @@ jobs:
|
|
26
26
|
strategy:
|
27
27
|
fail-fast: false
|
28
28
|
matrix:
|
29
|
-
ruby-version: ['
|
29
|
+
ruby-version: ['3.1', '3.2', '3.3']
|
30
30
|
include:
|
31
|
-
- ruby-version: '2.7'
|
32
|
-
bundle-gemfile: gemfiles/rails_5_2.gemfile
|
33
|
-
- ruby-version: '3.0'
|
34
|
-
bundle-gemfile: gemfiles/rails_6_0.gemfile
|
35
31
|
- ruby-version: '3.1'
|
36
32
|
bundle-gemfile: gemfiles/rails_6_1.gemfile
|
37
33
|
- ruby-version: '3.2'
|
@@ -53,7 +49,7 @@ jobs:
|
|
53
49
|
- name: Run tests
|
54
50
|
run: bundle exec rake test
|
55
51
|
- name: Upload result
|
56
|
-
uses: actions/upload-artifact@
|
52
|
+
uses: actions/upload-artifact@v4
|
57
53
|
if: always()
|
58
54
|
with:
|
59
55
|
path: test/reports/
|
data/iknow_view_models.gemspec
CHANGED
@@ -21,9 +21,9 @@ Gem::Specification.new do |spec|
|
|
21
21
|
|
22
22
|
spec.required_ruby_version = '>= 2.7.3'
|
23
23
|
|
24
|
-
spec.add_dependency 'actionpack', '>=
|
25
|
-
spec.add_dependency 'activerecord', '>=
|
26
|
-
spec.add_dependency 'activesupport', '>=
|
24
|
+
spec.add_dependency 'actionpack', '>= 6.1'
|
25
|
+
spec.add_dependency 'activerecord', '>= 6.1'
|
26
|
+
spec.add_dependency 'activesupport', '>= 6.1'
|
27
27
|
|
28
28
|
spec.add_dependency 'acts_as_manual_list'
|
29
29
|
spec.add_dependency 'deep_preloader', '>= 1.0.2'
|
@@ -5,6 +5,8 @@
|
|
5
5
|
# the mechanism by which it is applied to models.
|
6
6
|
class ViewModel::ActiveRecord
|
7
7
|
class UpdateContext
|
8
|
+
include ViewModel::ErrorWrapping
|
9
|
+
|
8
10
|
ReleaseEntry = Struct.new(:viewmodel, :association_data) do
|
9
11
|
def initialize(*)
|
10
12
|
super
|
@@ -276,10 +278,10 @@ class ViewModel::ActiveRecord
|
|
276
278
|
# human-readable error details.
|
277
279
|
def check_deferred_constraints!(model_class)
|
278
280
|
if model_class.connection.adapter_name == 'PostgreSQL'
|
279
|
-
|
281
|
+
wrap_active_record_errors([]) do
|
282
|
+
model_class.connection.execute('SET CONSTRAINTS ALL IMMEDIATE')
|
283
|
+
end
|
280
284
|
end
|
281
|
-
rescue ::ActiveRecord::StatementInvalid => ex
|
282
|
-
raise ViewModel::DeserializationError::DatabaseConstraint.from_exception(ex)
|
283
285
|
end
|
284
286
|
end
|
285
287
|
end
|
@@ -9,6 +9,7 @@ class ViewModel::ActiveRecord
|
|
9
9
|
class UpdateOperation
|
10
10
|
# inverse association and record to update a change in parent from a child
|
11
11
|
ParentData = Struct.new(:association_reflection, :viewmodel)
|
12
|
+
include ViewModel::ErrorWrapping
|
12
13
|
|
13
14
|
enum :RunState, [:Pending, :Running, :Run]
|
14
15
|
|
@@ -68,152 +69,148 @@ class ViewModel::ActiveRecord
|
|
68
69
|
|
69
70
|
model.class.transaction do
|
70
71
|
# Run context and viewmodel hooks
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
# update position
|
81
|
-
if reposition_to.present?
|
82
|
-
debug "-> #{debug_name}: Updating position to #{reposition_to}"
|
83
|
-
viewmodel._list_attribute = reposition_to
|
84
|
-
end
|
85
|
-
|
86
|
-
# Visit attributes and associations as much as possible in the order
|
87
|
-
# that they're declared in the view. We can visit attributes and
|
88
|
-
# points-to associations before save, but points-from associations
|
89
|
-
# must be visited after save.
|
90
|
-
pre_save_members, post_save_members = viewmodel.class._members.values.partition do |member_data|
|
91
|
-
!member_data.association? || member_data.pointer_location == :local
|
92
|
-
end
|
93
|
-
|
94
|
-
pre_save_members.each do |member_data|
|
95
|
-
if member_data.association?
|
96
|
-
next unless association_updates.include?(member_data)
|
97
|
-
|
98
|
-
child_operation = association_updates[member_data]
|
72
|
+
wrap_active_record_errors(self.blame_reference) do
|
73
|
+
ViewModel::Callbacks.wrap_deserialize(viewmodel, deserialize_context: deserialize_context) do |hook_control|
|
74
|
+
# update parent association
|
75
|
+
if reparent_to.present?
|
76
|
+
debug "-> #{debug_name}: Updating parent pointer to '#{reparent_to.viewmodel.class.view_name}:#{reparent_to.viewmodel.id}'"
|
77
|
+
association = model.association(reparent_to.association_reflection.name)
|
78
|
+
association.writer(reparent_to.viewmodel.model)
|
79
|
+
debug "<- #{debug_name}: Updated parent pointer"
|
80
|
+
end
|
99
81
|
|
100
|
-
|
101
|
-
|
82
|
+
# update position
|
83
|
+
if reposition_to.present?
|
84
|
+
debug "-> #{debug_name}: Updating position to #{reposition_to}"
|
85
|
+
viewmodel._list_attribute = reposition_to
|
86
|
+
end
|
102
87
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
88
|
+
# Visit attributes and associations as much as possible in the order
|
89
|
+
# that they're declared in the view. We can visit attributes and
|
90
|
+
# points-to associations before save, but points-from associations
|
91
|
+
# must be visited after save.
|
92
|
+
pre_save_members, post_save_members = viewmodel.class._members.values.partition do |member_data|
|
93
|
+
!member_data.association? || member_data.pointer_location == :local
|
94
|
+
end
|
109
95
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
96
|
+
pre_save_members.each do |member_data|
|
97
|
+
if member_data.association?
|
98
|
+
next unless association_updates.include?(member_data)
|
99
|
+
|
100
|
+
child_operation = association_updates[member_data]
|
101
|
+
|
102
|
+
reflection = member_data.direct_reflection
|
103
|
+
debug "-> #{debug_name}: Updating points-to association '#{reflection.name}'"
|
104
|
+
|
105
|
+
association = model.association(reflection.name)
|
106
|
+
new_target =
|
107
|
+
if child_operation
|
108
|
+
child_ctx = viewmodel.context_for_child(member_data.association_name, context: deserialize_context)
|
109
|
+
child_viewmodel = child_operation.run!(deserialize_context: child_ctx)
|
110
|
+
propagate_tree_changes(member_data, child_viewmodel.previous_changes)
|
111
|
+
|
112
|
+
child_viewmodel.model
|
113
|
+
end
|
114
|
+
association.writer(new_target)
|
115
|
+
debug "<- #{debug_name}: Updated points-to association '#{reflection.name}'"
|
116
|
+
else
|
117
|
+
attr_name = member_data.name
|
118
|
+
next unless attributes.include?(attr_name)
|
119
|
+
|
120
|
+
serialized_value = attributes[attr_name]
|
121
|
+
# Note that the VM::AR deserialization tree asserts ownership over any
|
122
|
+
# references it's provided, and so they're intentionally not passed on
|
123
|
+
# to attribute deserialization for use by their `using:` viewmodels. A
|
124
|
+
# (better?) alternative would be to provide them as reference-only
|
125
|
+
# hashes, to indicate that no modification can be permitted.
|
126
|
+
viewmodel.public_send("deserialize_#{attr_name}", serialized_value,
|
127
|
+
references: {},
|
128
|
+
deserialize_context: deserialize_context)
|
129
|
+
end
|
127
130
|
end
|
128
|
-
end
|
129
131
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
132
|
+
# If a request makes no assertions about the model, we don't demand
|
133
|
+
# that the current state of the model is valid. This permits making
|
134
|
+
# edits to other models that refer to this model when this model is
|
135
|
+
# invalid.
|
136
|
+
unless reference_only? && !viewmodel.new_model?
|
137
|
+
deserialize_context.run_callback(ViewModel::Callbacks::Hook::BeforeValidate, viewmodel)
|
138
|
+
viewmodel.validate!
|
139
|
+
end
|
138
140
|
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
begin
|
141
|
+
# Save if the model has been altered. Covers not only models with
|
142
|
+
# view changes but also lock version assertions.
|
143
|
+
if viewmodel.model.changed? || viewmodel.model.new_record?
|
144
|
+
debug "-> #{debug_name}: Saving"
|
144
145
|
model.save!
|
145
|
-
|
146
|
-
raise ViewModel::DeserializationError::Validation.from_active_model(ex.errors, blame_reference)
|
147
|
-
rescue ::ActiveRecord::StaleObjectError => _ex
|
148
|
-
raise ViewModel::DeserializationError::LockFailure.new(blame_reference)
|
146
|
+
debug "<- #{debug_name}: Saved"
|
149
147
|
end
|
150
|
-
debug "<- #{debug_name}: Saved"
|
151
|
-
end
|
152
148
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
149
|
+
# Update association cache of pointed-from associations after save: the
|
150
|
+
# child update will have saved the pointer.
|
151
|
+
post_save_members.each do |association_data|
|
152
|
+
next unless association_updates.include?(association_data)
|
157
153
|
|
158
|
-
|
159
|
-
|
154
|
+
child_operation = association_updates[association_data]
|
155
|
+
reflection = association_data.direct_reflection
|
160
156
|
|
161
|
-
|
157
|
+
debug "-> #{debug_name}: Updating pointed-to association '#{reflection.name}'"
|
162
158
|
|
163
|
-
|
164
|
-
|
159
|
+
association = model.association(reflection.name)
|
160
|
+
child_ctx = viewmodel.context_for_child(association_data.association_name, context: deserialize_context)
|
165
161
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
162
|
+
new_target =
|
163
|
+
if child_operation
|
164
|
+
ViewModel::Utils.map_one_or_many(child_operation) do |op|
|
165
|
+
child_viewmodel = op.run!(deserialize_context: child_ctx)
|
166
|
+
propagate_tree_changes(association_data, child_viewmodel.previous_changes)
|
171
167
|
|
172
|
-
|
168
|
+
child_viewmodel.model
|
169
|
+
end
|
173
170
|
end
|
174
|
-
end
|
175
171
|
|
176
|
-
|
172
|
+
association.target = new_target
|
177
173
|
|
178
|
-
|
179
|
-
|
174
|
+
debug "<- #{debug_name}: Updated pointed-to association '#{reflection.name}'"
|
175
|
+
end
|
180
176
|
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
177
|
+
if self.released_children.present?
|
178
|
+
# Released children that were not reclaimed by other parents during the
|
179
|
+
# build phase will be deleted: check access control.
|
180
|
+
debug "-> #{debug_name}: Checking released children permissions"
|
181
|
+
self.released_children.reject(&:claimed?).each do |released_child|
|
182
|
+
debug "-> #{debug_name}: Checking #{released_child.viewmodel.to_reference}"
|
183
|
+
child_vm = released_child.viewmodel
|
184
|
+
child_association_data = released_child.association_data
|
185
|
+
child_ctx = viewmodel.context_for_child(child_association_data.association_name, context: deserialize_context)
|
186
|
+
|
187
|
+
ViewModel::Callbacks.wrap_deserialize(child_vm, deserialize_context: child_ctx) do |child_hook_control|
|
188
|
+
changes = ViewModel::Changes.new(deleted: true)
|
189
|
+
child_ctx.run_callback(ViewModel::Callbacks::Hook::OnChange,
|
190
|
+
child_vm,
|
191
|
+
changes: changes)
|
192
|
+
child_hook_control.record_changes(changes)
|
193
|
+
end
|
198
194
|
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
195
|
+
if child_association_data.nested?
|
196
|
+
viewmodel.nested_children_changed!
|
197
|
+
elsif child_association_data.owned?
|
198
|
+
viewmodel.referenced_children_changed!
|
199
|
+
end
|
203
200
|
end
|
201
|
+
debug "<- #{debug_name}: Finished checking released children permissions"
|
204
202
|
end
|
205
|
-
debug "<- #{debug_name}: Finished checking released children permissions"
|
206
|
-
end
|
207
203
|
|
208
|
-
|
204
|
+
final_changes = viewmodel.clear_changes!
|
209
205
|
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
206
|
+
if final_changes.changed?
|
207
|
+
# Now that the change has been fully attempted, call the OnChange
|
208
|
+
# hook if local changes were made
|
209
|
+
deserialize_context.run_callback(ViewModel::Callbacks::Hook::OnChange, viewmodel, changes: final_changes)
|
210
|
+
end
|
215
211
|
|
216
|
-
|
212
|
+
hook_control.record_changes(final_changes)
|
213
|
+
end
|
217
214
|
end
|
218
215
|
end
|
219
216
|
|
@@ -221,8 +218,6 @@ class ViewModel::ActiveRecord
|
|
221
218
|
|
222
219
|
@run_state = RunState::Run
|
223
220
|
viewmodel
|
224
|
-
rescue ::ActiveRecord::StatementInvalid, ::ActiveRecord::InvalidForeignKey, ::ActiveRecord::RecordNotSaved => ex
|
225
|
-
raise ViewModel::DeserializationError::DatabaseConstraint.from_exception(ex, blame_reference)
|
226
221
|
end
|
227
222
|
|
228
223
|
def propagate_tree_changes(association_data, child_changes)
|
@@ -20,6 +20,7 @@ class ViewModel::ActiveRecord < ViewModel::Record
|
|
20
20
|
AFTER_ATTRIBUTE = 'after'
|
21
21
|
|
22
22
|
require 'view_model/utils/collections'
|
23
|
+
require 'view_model/error_wrapping'
|
23
24
|
require 'view_model/active_record/association_data'
|
24
25
|
require 'view_model/active_record/update_data'
|
25
26
|
require 'view_model/active_record/update_context'
|
@@ -30,6 +31,7 @@ class ViewModel::ActiveRecord < ViewModel::Record
|
|
30
31
|
require 'view_model/active_record/association_manipulation'
|
31
32
|
|
32
33
|
include AssociationManipulation
|
34
|
+
include ViewModel::ErrorWrapping
|
33
35
|
|
34
36
|
attr_reader :changed_associations
|
35
37
|
|
@@ -285,14 +287,14 @@ class ViewModel::ActiveRecord < ViewModel::Record
|
|
285
287
|
|
286
288
|
def destroy!(deserialize_context: self.class.new_deserialize_context)
|
287
289
|
model_class.transaction do
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
290
|
+
wrap_active_record_errors(self.blame_reference) do
|
291
|
+
ViewModel::Callbacks.wrap_deserialize(self, deserialize_context: deserialize_context) do |hook_control|
|
292
|
+
changes = ViewModel::Changes.new(deleted: true)
|
293
|
+
deserialize_context.run_callback(ViewModel::Callbacks::Hook::OnChange, self, changes: changes)
|
294
|
+
hook_control.record_changes(changes)
|
295
|
+
model.destroy!
|
296
|
+
end
|
293
297
|
end
|
294
|
-
rescue ::ActiveRecord::StatementInvalid, ::ActiveRecord::InvalidForeignKey, ::ActiveRecord::RecordNotSaved => e
|
295
|
-
raise ViewModel::DeserializationError::DatabaseConstraint.from_exception(e, self.blame_reference)
|
296
298
|
end
|
297
299
|
end
|
298
300
|
|
@@ -371,14 +373,8 @@ class ViewModel::ActiveRecord < ViewModel::Record
|
|
371
373
|
end
|
372
374
|
end
|
373
375
|
|
374
|
-
# Rails 6.1 introduced "previously_new_record?", but this library still
|
375
|
-
# supports activerecord >= 5.0. This is an approximation.
|
376
376
|
def self.model_previously_new?(model)
|
377
|
-
|
378
|
-
old_id, _new_id = id_changes
|
379
|
-
return true if old_id.nil?
|
380
|
-
end
|
381
|
-
false
|
377
|
+
model.previously_new_record?
|
382
378
|
end
|
383
379
|
|
384
380
|
# Helper to return entities that were part of the last deserialization. The
|
@@ -350,6 +350,16 @@ class ViewModel
|
|
350
350
|
end
|
351
351
|
end
|
352
352
|
|
353
|
+
class TransientDatabaseError < DeserializationError
|
354
|
+
status 502
|
355
|
+
attr_reader :detail
|
356
|
+
|
357
|
+
def initialize(detail, nodes = [])
|
358
|
+
@detail = detail
|
359
|
+
super(nodes)
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
353
363
|
class DatabaseConstraint < DeserializationError
|
354
364
|
status 400
|
355
365
|
attr_reader :detail
|
@@ -359,9 +369,9 @@ class ViewModel
|
|
359
369
|
super(nodes)
|
360
370
|
end
|
361
371
|
|
362
|
-
# Database constraint errors are pretty opaque and
|
363
|
-
# do our best to parse out what metadata we can
|
364
|
-
# back when we can't.
|
372
|
+
# Database constraint errors that come from Postgres are pretty opaque and
|
373
|
+
# stringly typed. We can do our best to parse out what metadata we can
|
374
|
+
# from the error, and fall back when we can't.
|
365
375
|
def self.from_exception(exception, nodes = [])
|
366
376
|
case exception.cause
|
367
377
|
when PG::UniqueViolation, PG::ExclusionViolation
|
@@ -478,7 +488,7 @@ class ViewModel
|
|
478
488
|
@attribute = attribute
|
479
489
|
@reason = reason
|
480
490
|
@details = details
|
481
|
-
super(
|
491
|
+
super(Array.wrap(node))
|
482
492
|
end
|
483
493
|
|
484
494
|
def detail
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ViewModel
|
4
|
+
module ErrorWrapping
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
# Catch and translate ActiveRecord errors that map to standard ViewModel
|
8
|
+
# errors. Blame may be either a single VM::Reference or an array of them, or
|
9
|
+
# an empty array if there is no specific node that the error may be attached
|
10
|
+
# to.
|
11
|
+
def wrap_active_record_errors(blame)
|
12
|
+
yield
|
13
|
+
rescue ::ActiveRecord::RecordInvalid => e
|
14
|
+
raise ViewModel::DeserializationError::Validation.from_active_model(e.record.errors, Array.wrap(blame))
|
15
|
+
rescue ::ActiveRecord::StaleObjectError => _e
|
16
|
+
raise ViewModel::DeserializationError::LockFailure.new(Array.wrap(blame))
|
17
|
+
rescue ::ActiveRecord::QueryAborted, ::ActiveRecord::PreparedStatementCacheExpired, ::ActiveRecord::TransactionRollbackError => e
|
18
|
+
raise ViewModel::DeserializationError::TransientDatabaseError.new(e.message, Array.wrap(blame))
|
19
|
+
rescue ::ActiveRecord::StatementInvalid, ::ActiveRecord::InvalidForeignKey, ::ActiveRecord::RecordNotSaved => e
|
20
|
+
raise ViewModel::DeserializationError::DatabaseConstraint.from_exception(e, Array.wrap(blame))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/nix/dependencies.nix
CHANGED
data/shell.nix
CHANGED
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'minitest/autorun'
|
4
|
+
require 'minitest/unit'
|
5
|
+
require 'minitest/hooks'
|
6
|
+
require 'rspec/expectations/minitest_integration'
|
7
|
+
|
8
|
+
require_relative '../../helpers/arvm_test_utilities'
|
9
|
+
require_relative '../../helpers/arvm_test_models'
|
10
|
+
require_relative '../../helpers/viewmodel_spec_helpers'
|
11
|
+
|
12
|
+
require 'view_model'
|
13
|
+
require 'view_model/active_record'
|
14
|
+
|
15
|
+
class ViewModel::ErrorWrappingTest < ActiveSupport::TestCase
|
16
|
+
include ARVMTestUtilities
|
17
|
+
extend Minitest::Spec::DSL
|
18
|
+
|
19
|
+
class Subject
|
20
|
+
include ViewModel::ErrorWrapping
|
21
|
+
end
|
22
|
+
|
23
|
+
let(:subject) { Subject.new }
|
24
|
+
|
25
|
+
describe 'wrap_active_record_errors' do
|
26
|
+
include ViewModelSpecHelpers::ParentAndBelongsToChild
|
27
|
+
|
28
|
+
let(:blame) { ViewModel::Reference.new(viewmodel_class, nil) }
|
29
|
+
|
30
|
+
describe 'RecordInvalid' do
|
31
|
+
before do
|
32
|
+
model_class.send(:validates, :name, inclusion: { in: ['cat'] })
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'wraps the error' do
|
36
|
+
ex = assert_raises(ViewModel::DeserializationError::Validation) do
|
37
|
+
subject.wrap_active_record_errors(blame) do
|
38
|
+
model_class.create!(name: 'a')
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
expect(ex.attribute).to eq('name')
|
43
|
+
expect(ex.reason).to match(/not included/)
|
44
|
+
expect(ex.details).to include(error: :inclusion)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe 'StaleObjectError' do
|
49
|
+
def model_attributes
|
50
|
+
super.merge(
|
51
|
+
schema: ->(t) { t.integer :lock_version, default: 0, null: false },
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'wraps the error' do
|
56
|
+
model = model_class.create!(name: 'a')
|
57
|
+
assert_raises(ViewModel::DeserializationError::LockFailure) do
|
58
|
+
subject.wrap_active_record_errors(blame) do
|
59
|
+
model.name = 'yes'
|
60
|
+
model.lock_version = 10
|
61
|
+
model.save!
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe 'transient' do
|
68
|
+
def with_timeout(timeout, conn = ActiveRecord::Base.connection, &block)
|
69
|
+
original_timeout = conn.select_value('SHOW statement_timeout')
|
70
|
+
conn.execute("SET SESSION statement_timeout = #{conn.quote(timeout)}")
|
71
|
+
block.call(conn)
|
72
|
+
ensure
|
73
|
+
begin
|
74
|
+
conn.execute("SET SESSION statement_timeout = #{conn.quote(original_timeout)}")
|
75
|
+
rescue ActiveRecord::StatementInvalid => e
|
76
|
+
raise unless e.cause.is_a?(PG::InFailedSqlTransaction)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'wraps the error' do
|
81
|
+
assert_raises(ViewModel::DeserializationError::TransientDatabaseError) do
|
82
|
+
subject.wrap_active_record_errors(blame) do
|
83
|
+
with_timeout(1) do
|
84
|
+
ActiveRecord::Base.connection.execute('SELECT pg_sleep(10)')
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe 'check constraint' do
|
92
|
+
def model_attributes
|
93
|
+
super.merge(
|
94
|
+
schema: ->(t) { t.check_constraint "name = 'cat'" },
|
95
|
+
)
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'wraps the error' do
|
99
|
+
ex = assert_raises(ViewModel::DeserializationError::DatabaseConstraint) do
|
100
|
+
subject.wrap_active_record_errors(blame) do
|
101
|
+
model_class.create!(name: 'a')
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
expect(ex.message).to match(/violates check constraint/)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
describe 'unique constraint' do
|
110
|
+
def model_attributes
|
111
|
+
super.merge(
|
112
|
+
schema: ->(t) { t.index :name, unique: true },
|
113
|
+
)
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'wraps the error' do
|
117
|
+
model_class.create!(name: 'a')
|
118
|
+
|
119
|
+
ex = assert_raises(ViewModel::DeserializationError::UniqueViolation) do
|
120
|
+
subject.wrap_active_record_errors(blame) do
|
121
|
+
model_class.create!(name: 'a')
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
expect(ex.columns).to eq(['name'])
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: iknow_view_models
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.14.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- iKnow Team
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2025-06-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: actionpack
|
@@ -16,42 +16,42 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '6.1'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '6.1'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: activerecord
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '6.1'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '6.1'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: activesupport
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - ">="
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
47
|
+
version: '6.1'
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
54
|
+
version: '6.1'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: acts_as_manual_list
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -392,8 +392,6 @@ files:
|
|
392
392
|
- LICENSE.txt
|
393
393
|
- README.md
|
394
394
|
- Rakefile
|
395
|
-
- gemfiles/rails_5_2.gemfile
|
396
|
-
- gemfiles/rails_6_0.gemfile
|
397
395
|
- gemfiles/rails_6_1.gemfile
|
398
396
|
- gemfiles/rails_7_0.gemfile
|
399
397
|
- gemfiles/rails_7_1.gemfile
|
@@ -433,6 +431,7 @@ files:
|
|
433
431
|
- lib/view_model/deserialize_context.rb
|
434
432
|
- lib/view_model/error.rb
|
435
433
|
- lib/view_model/error_view.rb
|
434
|
+
- lib/view_model/error_wrapping.rb
|
436
435
|
- lib/view_model/garbage_collection.rb
|
437
436
|
- lib/view_model/migratable_view.rb
|
438
437
|
- lib/view_model/migration.rb
|
@@ -488,6 +487,7 @@ files:
|
|
488
487
|
- test/unit/view_model/callbacks_test.rb
|
489
488
|
- test/unit/view_model/controller_test.rb
|
490
489
|
- test/unit/view_model/deserialization_error/unique_violation_test.rb
|
490
|
+
- test/unit/view_model/error_wrapping_test.rb
|
491
491
|
- test/unit/view_model/garbage_collection_test.rb
|
492
492
|
- test/unit/view_model/record_test.rb
|
493
493
|
- test/unit/view_model/registry_test.rb
|
@@ -497,7 +497,7 @@ homepage: https://github.com/iknow/cerego_view_models
|
|
497
497
|
licenses:
|
498
498
|
- MIT
|
499
499
|
metadata: {}
|
500
|
-
post_install_message:
|
500
|
+
post_install_message:
|
501
501
|
rdoc_options: []
|
502
502
|
require_paths:
|
503
503
|
- lib
|
@@ -513,7 +513,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
513
513
|
version: '0'
|
514
514
|
requirements: []
|
515
515
|
rubygems_version: 3.1.6
|
516
|
-
signing_key:
|
516
|
+
signing_key:
|
517
517
|
specification_version: 4
|
518
518
|
summary: ViewModels provide a means of encapsulating a collection of related data
|
519
519
|
and specifying its JSON serialization.
|
@@ -550,6 +550,7 @@ test_files:
|
|
550
550
|
- test/unit/view_model/callbacks_test.rb
|
551
551
|
- test/unit/view_model/controller_test.rb
|
552
552
|
- test/unit/view_model/deserialization_error/unique_violation_test.rb
|
553
|
+
- test/unit/view_model/error_wrapping_test.rb
|
553
554
|
- test/unit/view_model/garbage_collection_test.rb
|
554
555
|
- test/unit/view_model/record_test.rb
|
555
556
|
- test/unit/view_model/registry_test.rb
|
data/gemfiles/rails_5_2.gemfile
DELETED