dynamoid 3.11.0 → 3.13.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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +39 -3
  3. data/README.md +94 -14
  4. data/SECURITY.md +6 -6
  5. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/batch_get_item.rb +3 -1
  6. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +1 -1
  7. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +4 -1
  8. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +4 -1
  9. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/table.rb +13 -0
  10. data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +24 -9
  11. data/lib/dynamoid/config.rb +1 -0
  12. data/lib/dynamoid/criteria/chain.rb +11 -3
  13. data/lib/dynamoid/dirty.rb +22 -11
  14. data/lib/dynamoid/dumping.rb +3 -3
  15. data/lib/dynamoid/errors.rb +16 -1
  16. data/lib/dynamoid/fields/declare.rb +1 -1
  17. data/lib/dynamoid/fields.rb +44 -4
  18. data/lib/dynamoid/finders.rb +44 -19
  19. data/lib/dynamoid/persistence/inc.rb +30 -13
  20. data/lib/dynamoid/persistence/save.rb +24 -12
  21. data/lib/dynamoid/persistence/update_fields.rb +18 -5
  22. data/lib/dynamoid/persistence/update_validations.rb +3 -3
  23. data/lib/dynamoid/persistence/upsert.rb +17 -4
  24. data/lib/dynamoid/persistence.rb +273 -19
  25. data/lib/dynamoid/transaction_read/find.rb +137 -0
  26. data/lib/dynamoid/transaction_read.rb +146 -0
  27. data/lib/dynamoid/transaction_write/base.rb +12 -0
  28. data/lib/dynamoid/transaction_write/delete_with_instance.rb +7 -2
  29. data/lib/dynamoid/transaction_write/delete_with_primary_key.rb +7 -2
  30. data/lib/dynamoid/transaction_write/destroy.rb +10 -5
  31. data/lib/dynamoid/transaction_write/item_updater.rb +60 -0
  32. data/lib/dynamoid/transaction_write/save.rb +22 -9
  33. data/lib/dynamoid/transaction_write/update_fields.rb +176 -31
  34. data/lib/dynamoid/transaction_write/upsert.rb +23 -6
  35. data/lib/dynamoid/transaction_write.rb +212 -3
  36. data/lib/dynamoid/validations.rb +15 -4
  37. data/lib/dynamoid/version.rb +1 -1
  38. data/lib/dynamoid.rb +1 -0
  39. metadata +9 -9
@@ -28,9 +28,16 @@ module Dynamoid
28
28
 
29
29
  module ClassMethods
30
30
  def table_name
31
- table_base_name = options[:name] || base_class.name.split('::').last.downcase.pluralize
31
+ return @table_name if @table_name
32
32
 
33
- @table_name ||= [Dynamoid::Config.namespace.to_s, table_base_name].reject(&:empty?).join('_')
33
+ if options[:arn]
34
+ @table_name = options[:arn]
35
+ return @table_name
36
+ end
37
+
38
+ base_name = options[:name] || base_class.name.split('::').last.downcase.pluralize
39
+ namespace = Dynamoid::Config.namespace.to_s
40
+ @table_name = [namespace, base_name].reject(&:empty?).join('_')
34
41
  end
35
42
 
36
43
  # Create a table.
@@ -185,6 +192,9 @@ module Dynamoid
185
192
  #
186
193
  # Validates model and runs callbacks.
187
194
  #
195
+ # Raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is required
196
+ # but not specified or has value +nil+.
197
+ #
188
198
  # @param attrs [Hash|Array<Hash>] Attributes of a model
189
199
  # @param block [Proc] Block to process a document after initialization
190
200
  # @return [Dynamoid::Document] The created document
@@ -206,6 +216,9 @@ module Dynamoid
206
216
  # Raises an exception +Dynamoid::Errors::DocumentNotValid+ if validation
207
217
  # failed.
208
218
  #
219
+ # If any of the +before_*+ callbacks throws +:abort+ the creation is
220
+ # cancelled and +create!+ raises +Dynamoid::Errors::RecordNotSaved+.
221
+ #
209
222
  # Accepts both Hash and Array of Hashes and can create several
210
223
  # models.
211
224
  #
@@ -220,6 +233,9 @@ module Dynamoid
220
233
  #
221
234
  # Validates model and runs callbacks.
222
235
  #
236
+ # Raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is required
237
+ # but not specified or has value +nil+.
238
+ #
223
239
  # @param attrs [Hash|Array<Hash>] Attributes with which to create the object.
224
240
  # @param block [Proc] Block to process a document after initialization
225
241
  # @return [Dynamoid::Document] The created document
@@ -311,10 +327,16 @@ module Dynamoid
311
327
  # Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
312
328
  # attributes is not on the model
313
329
  #
330
+ # Raises +Dynamoid::Errors::MissingHashKey+ if a partition key has value
331
+ # +nil+ and +Dynamoid::Errors::MissingRangeKey+ if a sort key is required
332
+ # but has value +nil+.
333
+ #
314
334
  # @param hash_key_value [Scalar value] hash key
315
335
  # @param range_key_value [Scalar value] range key (optional)
316
336
  # @param attrs [Hash]
317
337
  # @param conditions [Hash] (optional)
338
+ # @option conditions [Hash] :if conditions on attribute values
339
+ # @option conditions [Hash] :unless_exists conditions on attributes presence
318
340
  # @return [Dynamoid::Document|nil] Updated document
319
341
  def update_fields(hash_key_value, range_key_value = nil, attrs = {}, conditions = {})
320
342
  optional_params = [range_key_value, attrs, conditions].compact
@@ -369,10 +391,16 @@ module Dynamoid
369
391
  # Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
370
392
  # attributes is not declared in the model class.
371
393
  #
394
+ # Raises +Dynamoid::Errors::MissingHashKey+ if a partition key has value
395
+ # +nil+ and raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is
396
+ # required but has value +nil+.
397
+ #
372
398
  # @param hash_key_value [Scalar value] hash key
373
399
  # @param range_key_value [Scalar value] range key (optional)
374
400
  # @param attrs [Hash]
375
401
  # @param conditions [Hash] (optional)
402
+ # @option conditions [Hash] :if conditions on attribute values
403
+ # @option conditions [Hash] :unless_exists conditions on attributes presence
376
404
  # @return [Dynamoid::Document|nil] Updated document
377
405
  def upsert(hash_key_value, range_key_value = nil, attrs = {}, conditions = {})
378
406
  optional_params = [range_key_value, attrs, conditions].compact
@@ -426,9 +454,92 @@ module Dynamoid
426
454
  # @option counters [true | Symbol | Array<Symbol>] :touch to update update_at attribute and optionally the specified ones
427
455
  # @return [Model class] self
428
456
  def inc(hash_key_value, range_key_value = nil, counters)
457
+ # It's similar to Rails' #update_counters.
429
458
  Inc.call(self, hash_key_value, range_key_value, counters)
430
459
  self
431
460
  end
461
+
462
+ # Delete a model by a given primary key.
463
+ #
464
+ # Raises +Dynamoid::Errors::MissingHashKey+ if a partition key has value
465
+ # +nil+ and raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is
466
+ # required but has value +nil+ or is missing.
467
+ #
468
+ # @param ids [String|Array] primary key or an array of primary keys
469
+ # @return nil
470
+ #
471
+ # @example Delete a model by given partition key:
472
+ # User.delete(user_id)
473
+ #
474
+ # @example Delete a model by given partition and sort keys:
475
+ # User.delete(user_id, sort_key)
476
+ #
477
+ # @example Delete multiple models by given partition keys:
478
+ # User.delete([id1, id2, id3])
479
+ #
480
+ # @example Delete multiple models by given partition and sort keys:
481
+ # User.delete([[id1, sk1], [id2, sk2], [id3, sk3]])
482
+ def delete(*ids)
483
+ if ids.empty?
484
+ raise Dynamoid::Errors::MissingHashKey
485
+ end
486
+
487
+ if ids[0].is_a?(Array)
488
+ # given multiple keys
489
+ # Model.delete([id1, id2, id3])
490
+ keys = ids[0] # ignore other arguments
491
+
492
+ ids = []
493
+ if self.range_key
494
+ # compound primary key
495
+ # expect [hash key, range key] pairs
496
+
497
+ range_key = []
498
+
499
+ # assume all elements are pairs, that's arrays
500
+ keys.each do |pk, sk|
501
+ raise Dynamoid::Errors::MissingHashKey if pk.nil?
502
+ raise Dynamoid::Errors::MissingRangeKey if self.range_key && sk.nil?
503
+
504
+ partition_key_dumped = cast_and_dump(hash_key, pk)
505
+ sort_key_dumped = cast_and_dump(self.range_key, sk)
506
+
507
+ ids << partition_key_dumped
508
+ range_key << sort_key_dumped
509
+ end
510
+ else
511
+ # simple primary key
512
+
513
+ range_key = nil
514
+
515
+ keys.each do |pk|
516
+ raise Dynamoid::Errors::MissingHashKey if pk.nil?
517
+
518
+ partition_key_dumped = cast_and_dump(hash_key, pk)
519
+ ids << partition_key_dumped
520
+ end
521
+ end
522
+
523
+ options = range_key ? { range_key: range_key } : {}
524
+ Dynamoid.adapter.delete(table_name, ids, options)
525
+ else
526
+ # given single primary key:
527
+ # Model.delete(partition_key)
528
+ # Model.delete(partition_key, sort_key)
529
+
530
+ partition_key, sort_key = ids
531
+
532
+ raise Dynamoid::Errors::MissingHashKey if partition_key.nil?
533
+ raise Dynamoid::Errors::MissingRangeKey if range_key? && sort_key.nil?
534
+
535
+ options = sort_key ? { range_key: cast_and_dump(self.range_key, sort_key) } : {}
536
+ partition_key_dumped = cast_and_dump(hash_key, partition_key)
537
+
538
+ Dynamoid.adapter.delete(table_name, partition_key_dumped, options)
539
+ end
540
+
541
+ nil
542
+ end
432
543
  end
433
544
 
434
545
  # Update document timestamps.
@@ -519,7 +630,8 @@ module Dynamoid
519
630
  # If a model is new and hash key (+id+ by default) is not assigned yet
520
631
  # it was assigned implicitly with random UUID value.
521
632
  #
522
- # If +lock_version+ attribute is declared it will be incremented. If it's blank then it will be initialized with 1.
633
+ # If +lock_version+ attribute is declared it will be incremented. If it's
634
+ # blank then it will be initialized with 1.
523
635
  #
524
636
  # +save+ method call raises +Dynamoid::Errors::RecordNotUnique+ exception
525
637
  # if primary key (hash key + optional range key) already exists in a
@@ -530,6 +642,11 @@ module Dynamoid
530
642
  # already changed concurrently and +lock_version+ was consequently
531
643
  # increased.
532
644
  #
645
+ # Raises +Dynamoid::Errors::MissingHashKey+ if a model is already persisted
646
+ # and a partition key has value +nil+ and raises
647
+ # +Dynamoid::Errors::MissingRangeKey+ if a sort key is required but has
648
+ # value +nil+.
649
+ #
533
650
  # When a table is not created yet the first +save+ method call will create
534
651
  # a table. It's useful in test environment to avoid explicit table
535
652
  # creation.
@@ -553,6 +670,88 @@ module Dynamoid
553
670
  end
554
671
  end
555
672
 
673
+ # Create new model or persist changes.
674
+ #
675
+ # Run the validation and callbacks. Raises
676
+ # +Dynamoid::Errors::DocumentNotValid+ is validation fails.
677
+ #
678
+ # user = User.create
679
+ #
680
+ # user.age = 26
681
+ # user.save! # => user
682
+ #
683
+ # Validation can be skipped with +validate: false+ option:
684
+ #
685
+ # user = User.new(age: -1)
686
+ # user.save!(validate: false) # => user
687
+ #
688
+ # If any of the +before_*+ callbacks throws +:abort+ the saving is
689
+ # cancelled and +save!+ raises +Dynamoid::Errors::RecordNotSaved+.
690
+ #
691
+ # +save!+ by default sets timestamps attributes - +created_at+ and
692
+ # +updated_at+ when creates new model and updates +updated_at+ attribute
693
+ # when updates already existing one.
694
+ #
695
+ # Changing +updated_at+ attribute at updating a model can be skipped with
696
+ # +touch: false+ option:
697
+ #
698
+ # user.save!(touch: false)
699
+ #
700
+ # If a model is new and hash key (+id+ by default) is not assigned yet
701
+ # it was assigned implicitly with random UUID value.
702
+ #
703
+ # If +lock_version+ attribute is declared it will be incremented. If it's
704
+ # blank then it will be initialized with 1.
705
+ #
706
+ # +save!+ method call raises +Dynamoid::Errors::RecordNotUnique+ exception
707
+ # if primary key (hash key + optional range key) already exists in a
708
+ # table.
709
+ #
710
+ # +save!+ method call raises +Dynamoid::Errors::StaleObjectError+ exception
711
+ # if there is +lock_version+ attribute and the document in a table was
712
+ # already changed concurrently and +lock_version+ was consequently
713
+ # increased.
714
+ #
715
+ # +save!+ method call raises +Dynamoid::Errors::RecordNotSaved+ exception
716
+ # if some callback aborted execution.
717
+ #
718
+ # Raises +Dynamoid::Errors::MissingHashKey+ if a model is already persisted
719
+ # and a partition key has value +nil+ and raises
720
+ # +Dynamoid::Errors::MissingRangeKey+ if a sort key is required but has
721
+ # value +nil+.
722
+ #
723
+ # When a table is not created yet the first +save!+ method call will create
724
+ # a table. It's useful in test environment to avoid explicit table
725
+ # creation.
726
+ #
727
+ # @param options [Hash] (optional)
728
+ # @option options [true|false] :validate validate a model or not - +true+ by default (optional)
729
+ # @option options [true|false] :touch update tiemstamps fields or not - +true+ by default (optional)
730
+ # @return [true|false] Whether saving successful or not
731
+ def save!(options = {})
732
+ # validation is handled in the Validation module
733
+
734
+ if Dynamoid.config.create_table_on_save
735
+ self.class.create_table(sync: true)
736
+ end
737
+
738
+ create_or_update = new_record? ? :create : :update
739
+ aborted = true
740
+
741
+ run_callbacks(:save) do
742
+ run_callbacks(create_or_update) do
743
+ aborted = false
744
+ Save.call(self, touch: options[:touch])
745
+ end
746
+ end
747
+
748
+ if aborted
749
+ raise Dynamoid::Errors::RecordNotSaved, self
750
+ end
751
+
752
+ self
753
+ end
754
+
556
755
  # Update multiple attributes at once, saving the object once the updates
557
756
  # are complete.
558
757
  #
@@ -564,6 +763,10 @@ module Dynamoid
564
763
  # Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
565
764
  # attributes is not on the model
566
765
  #
766
+ # Raises +Dynamoid::Errors::MissingHashKey+ if a partition key has value
767
+ # +nil+ and raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is
768
+ # required but has value +nil+.
769
+ #
567
770
  # @param attributes [Hash] a hash of attributes to update
568
771
  # @return [true|false] Whether updating successful or not
569
772
  # @since 0.2.0
@@ -580,9 +783,17 @@ module Dynamoid
580
783
  # Raises a +Dynamoid::Errors::DocumentNotValid+ exception if some vaidation
581
784
  # fails.
582
785
  #
786
+ # If any of the +before_*+ callbacks throws +:abort+ the updating is
787
+ # cancelled and +update_attributes!+ raises
788
+ # +Dynamoid::Errors::RecordNotSaved+.
789
+ #
583
790
  # Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
584
791
  # attributes is not on the model
585
792
  #
793
+ # Raises +Dynamoid::Errors::MissingHashKey+ if a partition key has value
794
+ # +nil+ and raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is
795
+ # required but has value +nil+.
796
+ #
586
797
  # @param attributes [Hash] a hash of attributes to update
587
798
  def update_attributes!(attributes)
588
799
  attributes.each { |attribute, value| write_attribute(attribute, value) }
@@ -591,8 +802,6 @@ module Dynamoid
591
802
 
592
803
  # Update a single attribute, saving the object afterwards.
593
804
  #
594
- # Returns +true+ if saving is successful and +false+ otherwise.
595
- #
596
805
  # user.update_attribute(:last_name, 'Tylor')
597
806
  #
598
807
  # Validation is skipped.
@@ -607,9 +816,28 @@ module Dynamoid
607
816
  # @since 0.2.0
608
817
  def update_attribute(attribute, value)
609
818
  # final implementation is in the Dynamoid::Validation module
610
- write_attribute(attribute, value)
611
- save
612
- self
819
+ end
820
+
821
+ # Update a single attribute, saving the object afterwards.
822
+ #
823
+ # user.update_attribute!(:last_name, 'Tylor')
824
+ #
825
+ # Validation is skipped.
826
+ #
827
+ # If any of the +before_*+ callbacks throws +:abort+ the updating is
828
+ # cancelled and +update_attribute!+ raises
829
+ # +Dynamoid::Errors::RecordNotSaved+.
830
+ #
831
+ # Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
832
+ # attributes is not on the model
833
+ #
834
+ # @param attribute [Symbol] attribute name to update
835
+ # @param value [Object] the value to assign it
836
+ # @return [Dynamoid::Document] self
837
+ #
838
+ # @since 0.2.0
839
+ def update_attribute!(attribute, value)
840
+ # final implementation is in the Dynamoid::Validation module
613
841
  end
614
842
 
615
843
  # Update a model.
@@ -620,8 +848,7 @@ module Dynamoid
620
848
  # attributes. Supports following operations: +add+, +delete+ and +set+.
621
849
  #
622
850
  # 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+).
851
+ # collections if attribute is a set.
625
852
  #
626
853
  # user.update! do |t|
627
854
  # t.add(age: 1, followers_count: 5)
@@ -646,7 +873,7 @@ module Dynamoid
646
873
  # {parameter}[https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/LegacyConditionalParameters.AttributeUpdates.html]
647
874
  # of +UpdateItem+ operation.
648
875
  #
649
- # It's an atomic operation. So adding or deleting elements in a collection
876
+ # It's atomic operations. So adding or deleting elements in a collection
650
877
  # or incrementing or decrementing a numeric field is atomic and does not
651
878
  # interfere with other write requests.
652
879
  #
@@ -686,9 +913,18 @@ module Dynamoid
686
913
 
687
914
  begin
688
915
  table_name = self.class.table_name
916
+ partition_key_dumped = Dumping.dump_field(hash_key, self.class.attributes[self.class.hash_key])
917
+ conditions = conditions.dup
918
+ conditions[:if] ||= {}
919
+ conditions[:if][self.class.hash_key] = partition_key_dumped
920
+ if self.class.range_key
921
+ sort_key_dumped = Dumping.dump_field(range_value, self.class.attributes[self.class.range_key])
922
+ conditions[:if][self.class.range_key] = sort_key_dumped
923
+ end
924
+
689
925
  update_item_options = options.merge(conditions: conditions)
690
926
 
691
- new_attrs = Dynamoid.adapter.update_item(table_name, hash_key, update_item_options) do |t|
927
+ new_attrs = Dynamoid.adapter.update_item(table_name, partition_key_dumped, update_item_options) do |t|
692
928
  item_updater = ItemUpdaterWithDumping.new(self.class, t)
693
929
 
694
930
  item_updater.add(lock_version: 1) if self.class.attributes[:lock_version]
@@ -701,6 +937,9 @@ module Dynamoid
701
937
  end
702
938
  load(Undumping.undump_attributes(new_attrs, self.class.attributes))
703
939
  rescue Dynamoid::Errors::ConditionalCheckFailedException
940
+ # exception may be raised either because of failed user provided conditions
941
+ # or because of conditions on partition and sort keys. We cannot
942
+ # distinguish these two cases.
704
943
  raise Dynamoid::Errors::StaleObjectError.new(self, 'update')
705
944
  end
706
945
  end
@@ -716,8 +955,7 @@ module Dynamoid
716
955
  # attributes. Supports following operations: +add+, +delete+ and +set+.
717
956
  #
718
957
  # 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+).
958
+ # collections if attribute is a set.
721
959
  #
722
960
  # user.update do |t|
723
961
  # t.add(age: 1, followers_count: 5)
@@ -891,16 +1129,19 @@ module Dynamoid
891
1129
  #
892
1130
  # Returns +self+ if deleted successfully and +false+ otherwise.
893
1131
  #
1132
+ # Raises +Dynamoid::Errors::MissingHashKey+ if a partition key has value
1133
+ # +nil+ and raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is
1134
+ # required but has value +nil+.
1135
+ #
894
1136
  # @return [Dynamoid::Document|false] whether deleted successfully
895
1137
  # @since 0.2.0
896
1138
  def destroy
897
- ret = run_callbacks(:destroy) do
1139
+ run_callbacks(:destroy) do
898
1140
  delete
1141
+ @destroyed = true
899
1142
  end
900
1143
 
901
- @destroyed = true
902
-
903
- ret == false ? false : self
1144
+ @destroyed ? self : false
904
1145
  end
905
1146
 
906
1147
  # Delete a model.
@@ -912,6 +1153,10 @@ module Dynamoid
912
1153
  #
913
1154
  # Raises +Dynamoid::Errors::RecordNotDestroyed+ exception if model deleting
914
1155
  # failed.
1156
+ #
1157
+ # Raises +Dynamoid::Errors::MissingHashKey+ if a partition key has value
1158
+ # +nil+ and raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is
1159
+ # required but has value +nil+.
915
1160
  def destroy!
916
1161
  destroy || (raise Dynamoid::Errors::RecordNotDestroyed, self)
917
1162
  end
@@ -924,10 +1169,18 @@ module Dynamoid
924
1169
  # Raises +Dynamoid::Errors::StaleObjectError+ exception if cannot delete a
925
1170
  # model.
926
1171
  #
1172
+ # Raises +Dynamoid::Errors::MissingHashKey+ if a partition key has value
1173
+ # +nil+ and raises +Dynamoid::Errors::MissingRangeKey+ if a sort key is
1174
+ # required but has value +nil+.
1175
+ #
927
1176
  # @return [Dynamoid::Document] self
928
1177
  # @since 0.2.0
929
1178
  def delete
1179
+ raise Dynamoid::Errors::MissingHashKey if hash_key.nil?
1180
+ raise Dynamoid::Errors::MissingRangeKey if self.class.range_key? && range_value.nil?
1181
+
930
1182
  options = range_key ? { range_key: Dumping.dump_field(read_attribute(range_key), self.class.attributes[range_key]) } : {}
1183
+ partition_key_dumped = Dumping.dump_field(hash_key, self.class.attributes[self.class.hash_key])
931
1184
 
932
1185
  # Add an optimistic locking check if the lock_version column exists
933
1186
  if self.class.attributes[:lock_version]
@@ -943,7 +1196,7 @@ module Dynamoid
943
1196
 
944
1197
  @destroyed = true
945
1198
 
946
- Dynamoid.adapter.delete(self.class.table_name, hash_key, options)
1199
+ Dynamoid.adapter.delete(self.class.table_name, partition_key_dumped, options)
947
1200
 
948
1201
  self.class.associations.each_key do |name|
949
1202
  send(name).disassociate_source
@@ -951,6 +1204,7 @@ module Dynamoid
951
1204
 
952
1205
  self
953
1206
  rescue Dynamoid::Errors::ConditionalCheckFailedException
1207
+ @destroyed = false
954
1208
  raise Dynamoid::Errors::StaleObjectError.new(self, 'delete')
955
1209
  end
956
1210
  end
@@ -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