iknow_view_models 3.6.5 → 3.6.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/iknow_view_models/version.rb +1 -1
- data/lib/view_model/active_record.rb +6 -0
- data/lib/view_model/deserialization_error.rb +39 -23
- data/lib/view_model/record.rb +14 -0
- data/test/unit/view_model/active_record_test.rb +66 -1
- data/test/unit/view_model/deserialization_error/unique_violation_test.rb +13 -5
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: eb322edf3edec85adb744dba24cab64e15914716f16cfa5ec6bb75de5fcb8e33
|
4
|
+
data.tar.gz: 4b41f0f4ba1fa9a3d7568ee8b0005c4139e8186c91244c7aa04ff8b3bae19fce
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a51e6a510110c90e3b4a77d0f3da2df2191e68d12137cd9ab040a97125f799e8292ae86c9b53f39b85ebb589d785fa791e8bf56883b122d6b08bfc15b2a0f702
|
7
|
+
data.tar.gz: 03c790566b0304ab4333e632fd81ba8886425ee8a45f1edb2fc5c777af7e2bcbfccecaa947c8a3001d47711ed8dee4132dcb4b52284cd799b915e147de9daf35
|
@@ -174,6 +174,12 @@ class ViewModel::ActiveRecord < ViewModel::Record
|
|
174
174
|
.run!(deserialize_context: deserialize_context)
|
175
175
|
end
|
176
176
|
end
|
177
|
+
rescue ViewModel::DeserializationError => e
|
178
|
+
if (new_error = customize_deserialization_error(e))
|
179
|
+
raise new_error
|
180
|
+
else
|
181
|
+
raise
|
182
|
+
end
|
177
183
|
end
|
178
184
|
|
179
185
|
# Constructs a preload specification of the required models for
|
@@ -347,7 +347,7 @@ class ViewModel
|
|
347
347
|
# back when we can't.
|
348
348
|
def self.from_exception(exception, nodes = [])
|
349
349
|
case exception.cause
|
350
|
-
when PG::UniqueViolation
|
350
|
+
when PG::UniqueViolation, PG::ExclusionViolation
|
351
351
|
UniqueViolation.from_postgres_error(exception.cause, nodes)
|
352
352
|
else
|
353
353
|
self.new(exception.message, nodes)
|
@@ -357,36 +357,60 @@ class ViewModel
|
|
357
357
|
|
358
358
|
class UniqueViolation < DeserializationError
|
359
359
|
status 400
|
360
|
-
attr_reader :detail, :constraint, :columns, :values
|
360
|
+
attr_reader :detail, :constraint, :columns, :values, :conflicts
|
361
361
|
|
362
|
-
PG_ERROR_FIELD_CONSTRAINT_NAME = 'n'.ord # Not exposed in pg gem
|
363
362
|
def self.from_postgres_error(err, nodes)
|
364
363
|
result = err.result
|
365
|
-
constraint = result.error_field(
|
366
|
-
message_detail = result.error_field(PG::
|
364
|
+
constraint = result.error_field(PG::PG_DIAG_CONSTRAINT_NAME)
|
365
|
+
message_detail = result.error_field(PG::PG_DIAG_MESSAGE_DETAIL)
|
367
366
|
|
368
|
-
columns, values = parse_message_detail(message_detail)
|
367
|
+
columns, values, conflicts = parse_message_detail(message_detail)
|
369
368
|
|
370
369
|
unless columns
|
371
370
|
# Couldn't parse the detail message, fall back on an unparsed error
|
372
371
|
return DatabaseConstraint.new(err.message, nodes)
|
373
372
|
end
|
374
373
|
|
375
|
-
self.new(err.message, constraint, columns, values, nodes)
|
374
|
+
self.new(err.message, constraint, columns, values, conflicts, nodes)
|
376
375
|
end
|
377
376
|
|
378
377
|
class << self
|
379
378
|
DETAIL_PREFIX = 'Key ('
|
380
|
-
|
381
|
-
|
379
|
+
UNIQUE_SUFFIX_TEMPLATE = /\A\)=\((?<values>.*)\) already exists\.\z/
|
380
|
+
EXCLUSION_SUFFIX_TEMPLATE = /\A\)=\((?<values>.*)\) conflicts with existing key \(.*\)=\((?<conflicts>.*)\)\.\z/
|
381
|
+
|
382
382
|
def parse_message_detail(detail)
|
383
383
|
stream = detail.dup
|
384
|
-
|
385
384
|
return nil unless stream.delete_prefix!(DETAIL_PREFIX)
|
386
|
-
return nil unless stream.delete_suffix!(DETAIL_SUFFIX)
|
387
385
|
|
388
386
|
# The message should start with an identifier list: pop off identifier
|
389
387
|
# tokens while we can.
|
388
|
+
identifiers = parse_identifiers(stream)
|
389
|
+
return nil if identifiers.nil?
|
390
|
+
|
391
|
+
# The message should now contain ")=(" followed by the value list and
|
392
|
+
# the suffix, potentially including a conflict list. We consider the
|
393
|
+
# value and conflict lists to be essentially unparseable because they
|
394
|
+
# are free to contain commas and no escaping is used. We make a best
|
395
|
+
# effort to extract them anyway.
|
396
|
+
values, conflicts =
|
397
|
+
if (m = UNIQUE_SUFFIX_TEMPLATE.match(stream))
|
398
|
+
m.values_at(:values)
|
399
|
+
elsif (m = EXCLUSION_SUFFIX_TEMPLATE.match(stream))
|
400
|
+
m.values_at(:values, :conflicts)
|
401
|
+
else
|
402
|
+
return nil
|
403
|
+
end
|
404
|
+
|
405
|
+
return identifiers, values, conflicts
|
406
|
+
end
|
407
|
+
|
408
|
+
private
|
409
|
+
|
410
|
+
QUOTED_IDENTIFIER = /\A"(?:[^"]|"")+"/.freeze
|
411
|
+
UNQUOTED_IDENTIFIER = /\A(?:\p{Alpha}|_)(?:\p{Alnum}|_)*/.freeze
|
412
|
+
|
413
|
+
def parse_identifiers(stream)
|
390
414
|
identifiers = []
|
391
415
|
|
392
416
|
identifier = parse_identifier(stream)
|
@@ -401,18 +425,9 @@ class ViewModel
|
|
401
425
|
identifiers << identifier
|
402
426
|
end
|
403
427
|
|
404
|
-
|
405
|
-
# value list.
|
406
|
-
return nil unless stream.delete_prefix!(DETAIL_INFIX)
|
407
|
-
|
408
|
-
[identifiers, stream]
|
428
|
+
identifiers
|
409
429
|
end
|
410
430
|
|
411
|
-
private
|
412
|
-
|
413
|
-
QUOTED_IDENTIFIER = /\A"(?:[^"]|"")+"/.freeze
|
414
|
-
UNQUOTED_IDENTIFIER = /\A(?:\p{Alpha}|_)(?:\p{Alnum}|_)*/.freeze
|
415
|
-
|
416
431
|
def parse_identifier(stream)
|
417
432
|
if (identifier = stream.slice!(UNQUOTED_IDENTIFIER))
|
418
433
|
identifier
|
@@ -424,16 +439,17 @@ class ViewModel
|
|
424
439
|
end
|
425
440
|
end
|
426
441
|
|
427
|
-
def initialize(detail, constraint, columns, values, nodes = [])
|
442
|
+
def initialize(detail, constraint, columns, values, conflicts, nodes = [])
|
428
443
|
@detail = detail
|
429
444
|
@constraint = constraint
|
430
445
|
@columns = columns
|
431
446
|
@values = values
|
447
|
+
@conflicts = conflicts
|
432
448
|
super(nodes)
|
433
449
|
end
|
434
450
|
|
435
451
|
def meta
|
436
|
-
super.merge(constraint: @constraint, columns: @columns, values: @values)
|
452
|
+
super.merge(constraint: @constraint, columns: @columns, values: @values, conflicts: @conflicts)
|
437
453
|
end
|
438
454
|
end
|
439
455
|
|
data/lib/view_model/record.rb
CHANGED
@@ -102,6 +102,12 @@ class ViewModel::Record < ViewModel
|
|
102
102
|
|
103
103
|
viewmodel
|
104
104
|
end
|
105
|
+
rescue ViewModel::DeserializationError => e
|
106
|
+
if (new_error = customize_deserialization_error(e))
|
107
|
+
raise new_error
|
108
|
+
else
|
109
|
+
raise
|
110
|
+
end
|
105
111
|
end
|
106
112
|
|
107
113
|
def deserialize_members_from_view(viewmodel, view_hash, references:, deserialize_context:)
|
@@ -164,6 +170,14 @@ class ViewModel::Record < ViewModel
|
|
164
170
|
|
165
171
|
@model_class = type
|
166
172
|
end
|
173
|
+
|
174
|
+
# Overriding this method allows matching and customization of deserialization
|
175
|
+
# errors in individual viewmodel types. This method is called on error from
|
176
|
+
# #deserialize_from_view. If a value is returned, that exception is raised
|
177
|
+
# instead.
|
178
|
+
def customize_deserialization_error(e)
|
179
|
+
nil
|
180
|
+
end
|
167
181
|
end
|
168
182
|
|
169
183
|
delegate :model_class, to: 'self.class'
|
@@ -445,6 +445,44 @@ class ViewModel::ActiveRecordTest < ActiveSupport::TestCase
|
|
445
445
|
end
|
446
446
|
end
|
447
447
|
|
448
|
+
class CustomizedErrorTest < ActiveSupport::TestCase
|
449
|
+
include ARVMTestUtilities
|
450
|
+
class TestError < ViewModel::Error; end
|
451
|
+
|
452
|
+
def before_all
|
453
|
+
super
|
454
|
+
|
455
|
+
build_viewmodel(:Model) do
|
456
|
+
define_schema do |t|
|
457
|
+
t.integer :value, null: false
|
458
|
+
end
|
459
|
+
|
460
|
+
define_model do
|
461
|
+
end
|
462
|
+
|
463
|
+
define_viewmodel do
|
464
|
+
root!
|
465
|
+
|
466
|
+
attribute :value
|
467
|
+
end
|
468
|
+
end
|
469
|
+
|
470
|
+
ModelView.define_singleton_method(:customize_deserialization_error) do |e|
|
471
|
+
TestError.new if e.is_a?(ViewModel::DeserializationError::DatabaseConstraint)
|
472
|
+
end
|
473
|
+
end
|
474
|
+
|
475
|
+
def test_customized_error
|
476
|
+
m = Model.create!(value: 1)
|
477
|
+
|
478
|
+
assert_raises(TestError) do
|
479
|
+
alter_by_view!(ModelView, m) do |view, refs|
|
480
|
+
view['value'] = nil
|
481
|
+
end
|
482
|
+
end
|
483
|
+
end
|
484
|
+
end
|
485
|
+
|
448
486
|
# Parent view should be correctly passed down the tree when deserializing
|
449
487
|
class DeferredConstraintTest < ActiveSupport::TestCase
|
450
488
|
include ARVMTestUtilities
|
@@ -455,6 +493,7 @@ class ViewModel::ActiveRecordTest < ActiveSupport::TestCase
|
|
455
493
|
build_viewmodel(:List) do
|
456
494
|
define_schema do |t|
|
457
495
|
t.integer :child_id
|
496
|
+
t.integer :value
|
458
497
|
end
|
459
498
|
|
460
499
|
define_model do
|
@@ -464,9 +503,11 @@ class ViewModel::ActiveRecordTest < ActiveSupport::TestCase
|
|
464
503
|
define_viewmodel do
|
465
504
|
root!
|
466
505
|
association :child
|
506
|
+
attribute :value
|
467
507
|
end
|
468
508
|
end
|
469
509
|
List.connection.execute('ALTER TABLE lists ADD CONSTRAINT unique_child UNIQUE (child_id) DEFERRABLE INITIALLY DEFERRED')
|
510
|
+
List.connection.execute('ALTER TABLE lists ADD CONSTRAINT excluded_value EXCLUDE USING btree (value WITH =) DEFERRABLE INITIALLY DEFERRED')
|
470
511
|
end
|
471
512
|
|
472
513
|
def test_deferred_constraint_violation
|
@@ -483,13 +524,37 @@ class ViewModel::ActiveRecordTest < ActiveSupport::TestCase
|
|
483
524
|
constraint = 'unique_child'
|
484
525
|
columns = ['child_id']
|
485
526
|
values = l1.child.id.to_s
|
527
|
+
conflicts = nil
|
528
|
+
|
529
|
+
assert_match(/#{constraint}/, ex.message)
|
530
|
+
assert_equal(constraint, ex.constraint)
|
531
|
+
assert_equal(columns, ex.columns)
|
532
|
+
assert_equal(values, ex.values)
|
533
|
+
|
534
|
+
assert_equal({ constraint: constraint, columns: columns, values: values, conflicts: conflicts, nodes: [] }, ex.meta)
|
535
|
+
end
|
536
|
+
|
537
|
+
def test_deferred_exclusion_violation
|
538
|
+
l1 = List.create!(value: 1)
|
539
|
+
l2 = List.create!
|
540
|
+
|
541
|
+
ex = assert_raises(ViewModel::DeserializationError::UniqueViolation) do
|
542
|
+
alter_by_view!(ListView, l2) do |view, refs|
|
543
|
+
view['value'] = 1
|
544
|
+
end
|
545
|
+
end
|
546
|
+
|
547
|
+
constraint = 'excluded_value'
|
548
|
+
columns = ['value']
|
549
|
+
values = '1'
|
550
|
+
conflicts = '1'
|
486
551
|
|
487
552
|
assert_match(/#{constraint}/, ex.message)
|
488
553
|
assert_equal(constraint, ex.constraint)
|
489
554
|
assert_equal(columns, ex.columns)
|
490
555
|
assert_equal(values, ex.values)
|
491
556
|
|
492
|
-
assert_equal({ constraint: constraint, columns: columns, values: values, nodes: [] }, ex.meta)
|
557
|
+
assert_equal({ constraint: constraint, columns: columns, values: values, conflicts: conflicts, nodes: [] }, ex.meta)
|
493
558
|
end
|
494
559
|
end
|
495
560
|
end
|
@@ -34,7 +34,15 @@ class ViewModel::DeserializationError::UniqueViolationTest < ActiveSupport::Test
|
|
34
34
|
let(:detail_message) { 'Key (x)=(a) already exists.' }
|
35
35
|
|
36
36
|
it 'parses the key and value' do
|
37
|
-
expect(parse).to eq([['x'], 'a'])
|
37
|
+
expect(parse).to eq([['x'], 'a', nil])
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe 'with a exclusion conflict' do
|
42
|
+
let(:detail_message) { 'Key (x)=(a) conflicts with existing key (x)=(z).' }
|
43
|
+
|
44
|
+
it 'parses the key and value' do
|
45
|
+
expect(parse).to eq([['x'], 'a', 'z'])
|
38
46
|
end
|
39
47
|
end
|
40
48
|
|
@@ -42,7 +50,7 @@ class ViewModel::DeserializationError::UniqueViolationTest < ActiveSupport::Test
|
|
42
50
|
let(:detail_message) { 'Key (x, y)=(a, b) already exists.' }
|
43
51
|
|
44
52
|
it 'parses the keys and value' do
|
45
|
-
expect(parse).to eq([['x', 'y'], 'a, b'])
|
53
|
+
expect(parse).to eq([['x', 'y'], 'a, b', nil])
|
46
54
|
end
|
47
55
|
end
|
48
56
|
|
@@ -50,7 +58,7 @@ class ViewModel::DeserializationError::UniqueViolationTest < ActiveSupport::Test
|
|
50
58
|
let(:detail_message) { 'Key ("x, y", z)=(a, b, c) already exists.' }
|
51
59
|
|
52
60
|
it 'parses the keys and value' do
|
53
|
-
expect(parse).to eq([['x, y', 'z'], 'a, b, c'])
|
61
|
+
expect(parse).to eq([['x, y', 'z'], 'a, b, c', nil])
|
54
62
|
end
|
55
63
|
end
|
56
64
|
|
@@ -58,7 +66,7 @@ class ViewModel::DeserializationError::UniqueViolationTest < ActiveSupport::Test
|
|
58
66
|
let(:detail_message) { 'Key ("""x"", ""y""", z)=(a, b, c) already exists.' }
|
59
67
|
|
60
68
|
it 'parses the keys and value' do
|
61
|
-
expect(parse).to eq([['"x", "y"', 'z'], 'a, b, c'])
|
69
|
+
expect(parse).to eq([['"x", "y"', 'z'], 'a, b, c', nil])
|
62
70
|
end
|
63
71
|
end
|
64
72
|
|
@@ -66,7 +74,7 @@ class ViewModel::DeserializationError::UniqueViolationTest < ActiveSupport::Test
|
|
66
74
|
let(:detail_message) { 'Key (a, b)=(a, b)=(c, d) already exists.' }
|
67
75
|
|
68
76
|
it 'parses the keys and value' do
|
69
|
-
expect(parse).to eq([['a', 'b'], 'a, b)=(c, d'])
|
77
|
+
expect(parse).to eq([['a', 'b'], 'a, b)=(c, d', nil])
|
70
78
|
end
|
71
79
|
end
|
72
80
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: iknow_view_models
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.6.
|
4
|
+
version: 3.6.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- iKnow Team
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-
|
11
|
+
date: 2022-10-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: actionpack
|