dynamoid 3.11.0 → 3.12.1

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.
@@ -12,10 +12,12 @@ module Dynamoid
12
12
 
13
13
  def initialize(model, touch: nil)
14
14
  @model = model
15
- @touch = touch # touch=false means explicit disabling of updating the `updated_at` attribute
15
+ @touch = touch # `touch: false` means explicit disabling of updating the `updated_at` attribute
16
16
  end
17
17
 
18
18
  def call
19
+ validate_primary_key!
20
+
19
21
  @model.hash_key = SecureRandom.uuid if @model.hash_key.blank?
20
22
 
21
23
  return true unless @model.changed?
@@ -36,8 +38,10 @@ module Dynamoid
36
38
  Dynamoid.adapter.write(@model.class.table_name, attributes_dumped, conditions_for_write)
37
39
  else
38
40
  attributes_to_persist = @model.attributes.slice(*@model.changed.map(&:to_sym))
41
+ partition_key_dumped = dump(@model.class.hash_key, @model.hash_key)
42
+ options = options_to_update_item(partition_key_dumped)
39
43
 
40
- Dynamoid.adapter.update_item(@model.class.table_name, @model.hash_key, options_to_update_item) do |t|
44
+ Dynamoid.adapter.update_item(@model.class.table_name, partition_key_dumped, options) do |t|
41
45
  item_updater = ItemUpdaterWithDumping.new(@model.class, t)
42
46
 
43
47
  attributes_to_persist.each do |name, value|
@@ -58,22 +62,25 @@ module Dynamoid
58
62
 
59
63
  private
60
64
 
65
+ def validate_primary_key!
66
+ raise Dynamoid::Errors::MissingHashKey if !@model.new_record? && @model.hash_key.nil?
67
+ raise Dynamoid::Errors::MissingRangeKey if @model.class.range_key? && @model.range_value.nil?
68
+ end
69
+
61
70
  # Should be called after incrementing `lock_version` attribute
62
71
  def conditions_for_write
63
72
  conditions = {}
64
73
 
65
74
  # Add an 'exists' check to prevent overwriting existing records with new ones
66
- if @model.new_record?
67
- conditions[:unless_exists] = [@model.class.hash_key]
68
- if @model.range_key
69
- conditions[:unless_exists] << @model.range_key
70
- end
75
+ conditions[:unless_exists] = [@model.class.hash_key]
76
+ if @model.range_key
77
+ conditions[:unless_exists] << @model.range_key
71
78
  end
72
79
 
73
80
  # Add an optimistic locking check if the lock_version column exists
74
81
  # Uses the original lock_version value from Dirty API
75
82
  # in case user changed 'lock_version' manually
76
- if @model.class.attributes[:lock_version] && (@model.changes[:lock_version][0])
83
+ if @model.class.attributes[:lock_version] && @model.changes[:lock_version][0]
77
84
  conditions[:if] ||= {}
78
85
  conditions[:if][:lock_version] = @model.changes[:lock_version][0]
79
86
  end
@@ -81,22 +88,22 @@ module Dynamoid
81
88
  conditions
82
89
  end
83
90
 
84
- def options_to_update_item
91
+ def options_to_update_item(partition_key_dumped)
85
92
  options = {}
86
93
 
87
94
  if @model.class.range_key
88
- value_dumped = Dumping.dump_field(@model.range_value, @model.class.attributes[@model.class.range_key])
95
+ value_dumped = dump(@model.class.range_key, @model.range_value)
89
96
  options[:range_key] = value_dumped
90
97
  end
91
98
 
92
99
  conditions = {}
93
100
  conditions[:if] ||= {}
94
- conditions[:if][@model.class.hash_key] = @model.hash_key
101
+ conditions[:if][@model.class.hash_key] = partition_key_dumped
95
102
 
96
103
  # Add an optimistic locking check if the lock_version column exists
97
104
  # Uses the original lock_version value from Dirty API
98
105
  # in case user changed 'lock_version' manually
99
- if @model.class.attributes[:lock_version] && (@model.changes[:lock_version][0])
106
+ if @model.class.attributes[:lock_version] && @model.changes[:lock_version][0]
100
107
  conditions[:if] ||= {}
101
108
  conditions[:if][:lock_version] = @model.changes[:lock_version][0]
102
109
  end
@@ -105,6 +112,11 @@ module Dynamoid
105
112
 
106
113
  options
107
114
  end
115
+
116
+ def dump(name, value)
117
+ options = @model.class.attributes[name]
118
+ Dumping.dump_field(value, options)
119
+ end
108
120
  end
109
121
  end
110
122
  end
@@ -16,9 +16,12 @@ module Dynamoid
16
16
  @sort_key = sort_key
17
17
  @attributes = attributes.symbolize_keys
18
18
  @conditions = conditions
19
+
20
+ @partition_key_dumped = cast_and_dump(@model_class.hash_key, @partition_key)
19
21
  end
20
22
 
21
23
  def call
24
+ validate_primary_key!
22
25
  UpdateValidations.validate_attributes_exist(@model_class, @attributes)
23
26
 
24
27
  if @model_class.timestamps_enabled?
@@ -32,8 +35,13 @@ module Dynamoid
32
35
 
33
36
  private
34
37
 
38
+ def validate_primary_key!
39
+ raise Dynamoid::Errors::MissingHashKey if @partition_key.nil?
40
+ raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @sort_key.nil?
41
+ end
42
+
35
43
  def update_item
36
- Dynamoid.adapter.update_item(@model_class.table_name, @partition_key, options_to_update_item) do |t|
44
+ Dynamoid.adapter.update_item(@model_class.table_name, @partition_key_dumped, options_to_update_item) do |t|
37
45
  item_updater = ItemUpdaterWithCastingAndDumping.new(@model_class, t)
38
46
 
39
47
  @attributes.each do |k, v|
@@ -50,18 +58,23 @@ module Dynamoid
50
58
  options = {}
51
59
 
52
60
  if @model_class.range_key
53
- value_casted = TypeCasting.cast_field(@sort_key, @model_class.attributes[@model_class.range_key])
54
- value_dumped = Dumping.dump_field(value_casted, @model_class.attributes[@model_class.range_key])
55
- options[:range_key] = value_dumped
61
+ range_key_dumped = cast_and_dump(@model_class.range_key, @sort_key)
62
+ options[:range_key] = range_key_dumped
56
63
  end
57
64
 
58
65
  conditions = @conditions.deep_dup
59
66
  conditions[:if] ||= {}
60
- conditions[:if][@model_class.hash_key] = @partition_key
67
+ conditions[:if][@model_class.hash_key] = @partition_key_dumped
61
68
  options[:conditions] = conditions
62
69
 
63
70
  options
64
71
  end
72
+
73
+ def cast_and_dump(name, value)
74
+ options = @model_class.attributes[name]
75
+ value_casted = TypeCasting.cast_field(value, options)
76
+ Dumping.dump_field(value_casted, options)
77
+ end
65
78
  end
66
79
  end
67
80
  end
@@ -7,9 +7,9 @@ module Dynamoid
7
7
  def self.validate_attributes_exist(model_class, attributes)
8
8
  model_attributes = model_class.attributes.keys
9
9
 
10
- attributes.each_key do |attr_name|
11
- unless model_attributes.include?(attr_name)
12
- raise Dynamoid::Errors::UnknownAttribute, "Attribute #{attr_name} does not exist in #{model_class}"
10
+ attributes.each_key do |name|
11
+ unless model_attributes.include?(name)
12
+ raise Dynamoid::Errors::UnknownAttribute.new(model_class, name)
13
13
  end
14
14
  end
15
15
  end
@@ -19,6 +19,7 @@ module Dynamoid
19
19
  end
20
20
 
21
21
  def call
22
+ validate_primary_key!
22
23
  UpdateValidations.validate_attributes_exist(@model_class, @attributes)
23
24
 
24
25
  if @model_class.timestamps_enabled?
@@ -32,8 +33,15 @@ module Dynamoid
32
33
 
33
34
  private
34
35
 
36
+ def validate_primary_key!
37
+ raise Dynamoid::Errors::MissingHashKey if @partition_key.nil?
38
+ raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @sort_key.nil?
39
+ end
40
+
35
41
  def update_item
36
- Dynamoid.adapter.update_item(@model_class.table_name, @partition_key, options_to_update_item) do |t|
42
+ partition_key_dumped = cast_and_dump(@model_class.hash_key, @partition_key)
43
+
44
+ Dynamoid.adapter.update_item(@model_class.table_name, partition_key_dumped, options_to_update_item) do |t|
37
45
  item_updater = ItemUpdaterWithCastingAndDumping.new(@model_class, t)
38
46
 
39
47
  @attributes.each do |k, v|
@@ -46,9 +54,8 @@ module Dynamoid
46
54
  options = {}
47
55
 
48
56
  if @model_class.range_key
49
- value_casted = TypeCasting.cast_field(@sort_key, @model_class.attributes[@model_class.range_key])
50
- value_dumped = Dumping.dump_field(value_casted, @model_class.attributes[@model_class.range_key])
51
- options[:range_key] = value_dumped
57
+ range_key_dumped = cast_and_dump(@model_class.range_key, @sort_key)
58
+ options[:range_key] = range_key_dumped
52
59
  end
53
60
 
54
61
  options[:conditions] = @conditions
@@ -58,6 +65,12 @@ module Dynamoid
58
65
  def undump_attributes(raw_attributes)
59
66
  Undumping.undump_attributes(raw_attributes, @model_class.attributes)
60
67
  end
68
+
69
+ def cast_and_dump(name, value)
70
+ options = @model_class.attributes[name]
71
+ value_casted = TypeCasting.cast_field(value, options)
72
+ Dumping.dump_field(value_casted, options)
73
+ end
61
74
  end
62
75
  end
63
76
  end
@@ -185,6 +185,9 @@ module Dynamoid
185
185
  #
186
186
  # Validates model and runs callbacks.
187
187
  #
188
+ # Raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is required
189
+ # but not specified or has value +nil+.
190
+ #
188
191
  # @param attrs [Hash|Array<Hash>] Attributes of a model
189
192
  # @param block [Proc] Block to process a document after initialization
190
193
  # @return [Dynamoid::Document] The created document
@@ -220,6 +223,9 @@ module Dynamoid
220
223
  #
221
224
  # Validates model and runs callbacks.
222
225
  #
226
+ # Raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is required
227
+ # but not specified or has value +nil+.
228
+ #
223
229
  # @param attrs [Hash|Array<Hash>] Attributes with which to create the object.
224
230
  # @param block [Proc] Block to process a document after initialization
225
231
  # @return [Dynamoid::Document] The created document
@@ -311,10 +317,16 @@ module Dynamoid
311
317
  # Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
312
318
  # attributes is not on the model
313
319
  #
320
+ # Raises +Dynamoid::Errors::MissingHashKey+ if a partition key has value
321
+ # +nil+ and +Dynamoid::Errors::MissingRangeKey+ if a sort key is required
322
+ # but has value +nil+.
323
+ #
314
324
  # @param hash_key_value [Scalar value] hash key
315
325
  # @param range_key_value [Scalar value] range key (optional)
316
326
  # @param attrs [Hash]
317
327
  # @param conditions [Hash] (optional)
328
+ # @option conditions [Hash] :if conditions on attribute values
329
+ # @option conditions [Hash] :unless_exists conditions on attributes presence
318
330
  # @return [Dynamoid::Document|nil] Updated document
319
331
  def update_fields(hash_key_value, range_key_value = nil, attrs = {}, conditions = {})
320
332
  optional_params = [range_key_value, attrs, conditions].compact
@@ -369,10 +381,16 @@ module Dynamoid
369
381
  # Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
370
382
  # attributes is not declared in the model class.
371
383
  #
384
+ # Raises +Dynamoid::Errors::MissingHashKey+ if a partition key has value
385
+ # +nil+ and raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is
386
+ # required but has value +nil+.
387
+ #
372
388
  # @param hash_key_value [Scalar value] hash key
373
389
  # @param range_key_value [Scalar value] range key (optional)
374
390
  # @param attrs [Hash]
375
391
  # @param conditions [Hash] (optional)
392
+ # @option conditions [Hash] :if conditions on attribute values
393
+ # @option conditions [Hash] :unless_exists conditions on attributes presence
376
394
  # @return [Dynamoid::Document|nil] Updated document
377
395
  def upsert(hash_key_value, range_key_value = nil, attrs = {}, conditions = {})
378
396
  optional_params = [range_key_value, attrs, conditions].compact
@@ -426,6 +444,7 @@ module Dynamoid
426
444
  # @option counters [true | Symbol | Array<Symbol>] :touch to update update_at attribute and optionally the specified ones
427
445
  # @return [Model class] self
428
446
  def inc(hash_key_value, range_key_value = nil, counters)
447
+ # It's similar to Rails' #update_counters.
429
448
  Inc.call(self, hash_key_value, range_key_value, counters)
430
449
  self
431
450
  end
@@ -519,7 +538,8 @@ module Dynamoid
519
538
  # If a model is new and hash key (+id+ by default) is not assigned yet
520
539
  # it was assigned implicitly with random UUID value.
521
540
  #
522
- # If +lock_version+ attribute is declared it will be incremented. If it's blank then it will be initialized with 1.
541
+ # If +lock_version+ attribute is declared it will be incremented. If it's
542
+ # blank then it will be initialized with 1.
523
543
  #
524
544
  # +save+ method call raises +Dynamoid::Errors::RecordNotUnique+ exception
525
545
  # if primary key (hash key + optional range key) already exists in a
@@ -530,6 +550,11 @@ module Dynamoid
530
550
  # already changed concurrently and +lock_version+ was consequently
531
551
  # increased.
532
552
  #
553
+ # Raises +Dynamoid::Errors::MissingHashKey+ if a model is already persisted
554
+ # and a partition key has value +nil+ and raises
555
+ # +Dynamoid::Errors::MissingRangeKey+ if a sort key is required but has
556
+ # value +nil+.
557
+ #
533
558
  # When a table is not created yet the first +save+ method call will create
534
559
  # a table. It's useful in test environment to avoid explicit table
535
560
  # creation.
@@ -553,6 +578,85 @@ module Dynamoid
553
578
  end
554
579
  end
555
580
 
581
+ # Create new model or persist changes.
582
+ #
583
+ # Run the validation and callbacks. Raises
584
+ # +Dynamoid::Errors::DocumentNotValid+ is validation fails.
585
+ #
586
+ # user = User.create
587
+ #
588
+ # user.age = 26
589
+ # user.save! # => user
590
+ #
591
+ # Validation can be skipped with +validate: false+ option:
592
+ #
593
+ # user = User.new(age: -1)
594
+ # user.save!(validate: false) # => user
595
+ #
596
+ # +save!+ by default sets timestamps attributes - +created_at+ and
597
+ # +updated_at+ when creates new model and updates +updated_at+ attribute
598
+ # when updates already existing one.
599
+ #
600
+ # Changing +updated_at+ attribute at updating a model can be skipped with
601
+ # +touch: false+ option:
602
+ #
603
+ # user.save!(touch: false)
604
+ #
605
+ # If a model is new and hash key (+id+ by default) is not assigned yet
606
+ # it was assigned implicitly with random UUID value.
607
+ #
608
+ # If +lock_version+ attribute is declared it will be incremented. If it's
609
+ # blank then it will be initialized with 1.
610
+ #
611
+ # +save!+ method call raises +Dynamoid::Errors::RecordNotUnique+ exception
612
+ # if primary key (hash key + optional range key) already exists in a
613
+ # table.
614
+ #
615
+ # +save!+ method call raises +Dynamoid::Errors::StaleObjectError+ exception
616
+ # if there is +lock_version+ attribute and the document in a table was
617
+ # already changed concurrently and +lock_version+ was consequently
618
+ # increased.
619
+ #
620
+ # +save!+ method call raises +Dynamoid::Errors::RecordNotSaved+ exception
621
+ # if some callback aborted execution.
622
+ #
623
+ # Raises +Dynamoid::Errors::MissingHashKey+ if a model is already persisted
624
+ # and a partition key has value +nil+ and raises
625
+ # +Dynamoid::Errors::MissingRangeKey+ if a sort key is required but has
626
+ # value +nil+.
627
+ #
628
+ # When a table is not created yet the first +save!+ method call will create
629
+ # a table. It's useful in test environment to avoid explicit table
630
+ # creation.
631
+ #
632
+ # @param options [Hash] (optional)
633
+ # @option options [true|false] :validate validate a model or not - +true+ by default (optional)
634
+ # @option options [true|false] :touch update tiemstamps fields or not - +true+ by default (optional)
635
+ # @return [true|false] Whether saving successful or not
636
+ def save!(options = {})
637
+ # validation is handled in the Validation module
638
+
639
+ if Dynamoid.config.create_table_on_save
640
+ self.class.create_table(sync: true)
641
+ end
642
+
643
+ create_or_update = new_record? ? :create : :update
644
+ aborted = true
645
+
646
+ run_callbacks(:save) do
647
+ run_callbacks(create_or_update) do
648
+ aborted = false
649
+ Save.call(self, touch: options[:touch])
650
+ end
651
+ end
652
+
653
+ if aborted
654
+ raise Dynamoid::Errors::RecordNotSaved, self
655
+ end
656
+
657
+ self
658
+ end
659
+
556
660
  # Update multiple attributes at once, saving the object once the updates
557
661
  # are complete.
558
662
  #
@@ -564,6 +668,10 @@ module Dynamoid
564
668
  # Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
565
669
  # attributes is not on the model
566
670
  #
671
+ # Raises +Dynamoid::Errors::MissingHashKey+ if a partition key has value
672
+ # +nil+ and raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is
673
+ # required but has value +nil+.
674
+ #
567
675
  # @param attributes [Hash] a hash of attributes to update
568
676
  # @return [true|false] Whether updating successful or not
569
677
  # @since 0.2.0
@@ -583,6 +691,10 @@ module Dynamoid
583
691
  # Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
584
692
  # attributes is not on the model
585
693
  #
694
+ # Raises +Dynamoid::Errors::MissingHashKey+ if a partition key has value
695
+ # +nil+ and raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is
696
+ # required but has value +nil+.
697
+ #
586
698
  # @param attributes [Hash] a hash of attributes to update
587
699
  def update_attributes!(attributes)
588
700
  attributes.each { |attribute, value| write_attribute(attribute, value) }
@@ -607,9 +719,6 @@ module Dynamoid
607
719
  # @since 0.2.0
608
720
  def update_attribute(attribute, value)
609
721
  # final implementation is in the Dynamoid::Validation module
610
- write_attribute(attribute, value)
611
- save
612
- self
613
722
  end
614
723
 
615
724
  # Update a model.
@@ -620,8 +729,7 @@ module Dynamoid
620
729
  # attributes. Supports following operations: +add+, +delete+ and +set+.
621
730
  #
622
731
  # Operation +add+ just adds a value for numeric attributes and join
623
- # collections if attribute is a collection (one of +array+, +set+ or
624
- # +map+).
732
+ # collections if attribute is a set.
625
733
  #
626
734
  # user.update! do |t|
627
735
  # t.add(age: 1, followers_count: 5)
@@ -646,7 +754,7 @@ module Dynamoid
646
754
  # {parameter}[https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/LegacyConditionalParameters.AttributeUpdates.html]
647
755
  # of +UpdateItem+ operation.
648
756
  #
649
- # It's an atomic operation. So adding or deleting elements in a collection
757
+ # It's atomic operations. So adding or deleting elements in a collection
650
758
  # or incrementing or decrementing a numeric field is atomic and does not
651
759
  # interfere with other write requests.
652
760
  #
@@ -686,9 +794,18 @@ module Dynamoid
686
794
 
687
795
  begin
688
796
  table_name = self.class.table_name
797
+ partition_key_dumped = Dumping.dump_field(hash_key, self.class.attributes[self.class.hash_key])
798
+ conditions = conditions.dup
799
+ conditions[:if] ||= {}
800
+ conditions[:if][self.class.hash_key] = partition_key_dumped
801
+ if self.class.range_key
802
+ sort_key_dumped = Dumping.dump_field(range_value, self.class.attributes[self.class.range_key])
803
+ conditions[:if][self.class.range_key] = sort_key_dumped
804
+ end
805
+
689
806
  update_item_options = options.merge(conditions: conditions)
690
807
 
691
- new_attrs = Dynamoid.adapter.update_item(table_name, hash_key, update_item_options) do |t|
808
+ new_attrs = Dynamoid.adapter.update_item(table_name, partition_key_dumped, update_item_options) do |t|
692
809
  item_updater = ItemUpdaterWithDumping.new(self.class, t)
693
810
 
694
811
  item_updater.add(lock_version: 1) if self.class.attributes[:lock_version]
@@ -701,6 +818,9 @@ module Dynamoid
701
818
  end
702
819
  load(Undumping.undump_attributes(new_attrs, self.class.attributes))
703
820
  rescue Dynamoid::Errors::ConditionalCheckFailedException
821
+ # exception may be raised either because of failed user provided conditions
822
+ # or because of conditions on partition and sort keys. We cannot
823
+ # distinguish these two cases.
704
824
  raise Dynamoid::Errors::StaleObjectError.new(self, 'update')
705
825
  end
706
826
  end
@@ -716,8 +836,7 @@ module Dynamoid
716
836
  # attributes. Supports following operations: +add+, +delete+ and +set+.
717
837
  #
718
838
  # Operation +add+ just adds a value for numeric attributes and join
719
- # collections if attribute is a collection (one of +array+, +set+ or
720
- # +map+).
839
+ # collections if attribute is a set.
721
840
  #
722
841
  # user.update do |t|
723
842
  # t.add(age: 1, followers_count: 5)
@@ -891,6 +1010,10 @@ module Dynamoid
891
1010
  #
892
1011
  # Returns +self+ if deleted successfully and +false+ otherwise.
893
1012
  #
1013
+ # Raises +Dynamoid::Errors::MissingHashKey+ if a partition key has value
1014
+ # +nil+ and raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is
1015
+ # required but has value +nil+.
1016
+ #
894
1017
  # @return [Dynamoid::Document|false] whether deleted successfully
895
1018
  # @since 0.2.0
896
1019
  def destroy
@@ -912,6 +1035,10 @@ module Dynamoid
912
1035
  #
913
1036
  # Raises +Dynamoid::Errors::RecordNotDestroyed+ exception if model deleting
914
1037
  # failed.
1038
+ #
1039
+ # Raises +Dynamoid::Errors::MissingHashKey+ if a partition key has value
1040
+ # +nil+ and raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is
1041
+ # required but has value +nil+.
915
1042
  def destroy!
916
1043
  destroy || (raise Dynamoid::Errors::RecordNotDestroyed, self)
917
1044
  end
@@ -924,10 +1051,18 @@ module Dynamoid
924
1051
  # Raises +Dynamoid::Errors::StaleObjectError+ exception if cannot delete a
925
1052
  # model.
926
1053
  #
1054
+ # Raises +Dynamoid::Errors::MissingHashKey+ if a partition key has value
1055
+ # +nil+ and raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is
1056
+ # required but has value +nil+.
1057
+ #
927
1058
  # @return [Dynamoid::Document] self
928
1059
  # @since 0.2.0
929
1060
  def delete
1061
+ raise Dynamoid::Errors::MissingHashKey if hash_key.nil?
1062
+ raise Dynamoid::Errors::MissingRangeKey if self.class.range_key? && range_value.nil?
1063
+
930
1064
  options = range_key ? { range_key: Dumping.dump_field(read_attribute(range_key), self.class.attributes[range_key]) } : {}
1065
+ partition_key_dumped = Dumping.dump_field(hash_key, self.class.attributes[self.class.hash_key])
931
1066
 
932
1067
  # Add an optimistic locking check if the lock_version column exists
933
1068
  if self.class.attributes[:lock_version]
@@ -943,7 +1078,7 @@ module Dynamoid
943
1078
 
944
1079
  @destroyed = true
945
1080
 
946
- Dynamoid.adapter.delete(self.class.table_name, hash_key, options)
1081
+ Dynamoid.adapter.delete(self.class.table_name, partition_key_dumped, options)
947
1082
 
948
1083
  self.class.associations.each_key do |name|
949
1084
  send(name).disassociate_source
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dynamoid
4
+ class TransactionRead
5
+ class Find
6
+ attr_reader :model_class
7
+
8
+ def initialize(model_class, *ids, **options)
9
+ @model_class = model_class
10
+ @ids = ids
11
+ @options = options
12
+ end
13
+
14
+ def on_registration
15
+ validate_primary_key!
16
+ end
17
+
18
+ def observable_by_user_result
19
+ nil
20
+ end
21
+
22
+ def action_request
23
+ if single_key_given?
24
+ action_request_for_single_key
25
+ else
26
+ action_request_for_multiple_keys
27
+ end
28
+ end
29
+
30
+ def process_responses(responses)
31
+ models = responses.map do |response|
32
+ if response.item
33
+ @model_class.from_database(response.item)
34
+ elsif @options[:raise_error] == false
35
+ nil
36
+ else
37
+ message = build_record_not_found_exception_message(responses)
38
+ raise Dynamoid::Errors::RecordNotFound, message
39
+ end
40
+ end
41
+
42
+ unless single_key_given?
43
+ models.compact!
44
+ end
45
+
46
+ models.each { |m| m&.run_callbacks :find }
47
+ models
48
+ end
49
+
50
+ private
51
+
52
+ def single_key_given?
53
+ @ids.size == 1 && !@ids[0].is_a?(Array)
54
+ end
55
+
56
+ def validate_primary_key!
57
+ if single_key_given?
58
+ partition_key = @ids[0]
59
+ sort_key = @options[:range_key]
60
+
61
+ raise Dynamoid::Errors::MissingHashKey if partition_key.nil?
62
+ raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key && sort_key.nil?
63
+ else
64
+ ids = @ids.flatten(1)
65
+
66
+ raise Dynamoid::Errors::MissingHashKey if ids.any? { |pk, _sk| pk.nil? }
67
+ raise Errors::MissingRangeKey if @model_class.range_key && ids.any? { |_pk, sk| sk.nil? }
68
+ end
69
+ end
70
+
71
+ def action_request_for_single_key
72
+ partition_key = @ids[0]
73
+
74
+ key = { @model_class.hash_key => cast_and_dump_attribute(@model_class.hash_key, partition_key) }
75
+
76
+ if @model_class.range_key
77
+ sort_key = @options[:range_key]
78
+ key[@model_class.range_key] = cast_and_dump_attribute(@model_class.range_key, sort_key)
79
+ end
80
+
81
+ {
82
+ get: {
83
+ key: key,
84
+ table_name: @model_class.table_name
85
+ }
86
+ }
87
+ end
88
+
89
+ def action_request_for_multiple_keys
90
+ @ids.flatten(1).map do |id|
91
+ if @model_class.range_key
92
+ # expect [hash-key, range-key] pair
93
+ pk, sk = id
94
+ pk_dumped = cast_and_dump_attribute(@model_class.hash_key, pk)
95
+ sk_dumped = cast_and_dump_attribute(@model_class.range_key, sk)
96
+
97
+ key = { @model_class.hash_key => pk_dumped, @model_class.range_key => sk_dumped }
98
+ else
99
+ pk_dumped = cast_and_dump_attribute(@model_class.hash_key, id)
100
+
101
+ key = { @model_class.hash_key => pk_dumped }
102
+ end
103
+
104
+ {
105
+ get: {
106
+ key: key,
107
+ table_name: @model_class.table_name
108
+ }
109
+ }
110
+ end
111
+ end
112
+
113
+ def cast_and_dump_attribute(name, value)
114
+ attribute_options = @model_class.attributes[name]
115
+ casted_value = TypeCasting.cast_field(value, attribute_options)
116
+ Dumping.dump_field(casted_value, attribute_options)
117
+ end
118
+
119
+ def build_record_not_found_exception_message(responses)
120
+ items = responses.map(&:item)
121
+ ids = @ids.flatten(1)
122
+
123
+ if single_key_given?
124
+ id = ids[0]
125
+ primary_key = @model_class.range_key ? "(#{id.inspect},#{@options[:range_key].inspect})" : id.inspect
126
+ message = "Couldn't find #{@model_class.name} with primary key #{primary_key}"
127
+ else
128
+ ids_list = @model_class.range_key ? ids.map { |pk, sk| "(#{pk.inspect},#{sk.inspect})" } : ids.map(&:inspect)
129
+ message = "Couldn't find all #{@model_class.name.pluralize} with primary keys [#{ids_list.join(', ')}] "
130
+ message += "(found #{items.compact.size} results, but was looking for #{items.size})"
131
+ end
132
+
133
+ message
134
+ end
135
+ end
136
+ end
137
+ end