iknow_view_models 3.6.5 → 3.6.6
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/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
|