dynamoid 3.10.0 → 3.11.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/README.md +182 -2
  4. data/dynamoid.gemspec +4 -4
  5. data/lib/dynamoid/adapter.rb +1 -1
  6. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/filter_expression_convertor.rb +53 -18
  7. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +5 -4
  8. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/projection_expression_convertor.rb +9 -7
  9. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +1 -1
  10. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +1 -1
  11. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/transact.rb +31 -0
  12. data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +9 -5
  13. data/lib/dynamoid/components.rb +1 -0
  14. data/lib/dynamoid/config.rb +2 -0
  15. data/lib/dynamoid/criteria/chain.rb +63 -18
  16. data/lib/dynamoid/criteria/where_conditions.rb +13 -6
  17. data/lib/dynamoid/dirty.rb +86 -11
  18. data/lib/dynamoid/dumping.rb +36 -14
  19. data/lib/dynamoid/errors.rb +14 -2
  20. data/lib/dynamoid/finders.rb +6 -6
  21. data/lib/dynamoid/loadable.rb +1 -0
  22. data/lib/dynamoid/persistence/inc.rb +6 -7
  23. data/lib/dynamoid/persistence/item_updater_with_casting_and_dumping.rb +36 -0
  24. data/lib/dynamoid/persistence/item_updater_with_dumping.rb +33 -0
  25. data/lib/dynamoid/persistence/save.rb +5 -2
  26. data/lib/dynamoid/persistence/update_fields.rb +5 -3
  27. data/lib/dynamoid/persistence/upsert.rb +5 -4
  28. data/lib/dynamoid/persistence.rb +38 -17
  29. data/lib/dynamoid/transaction_write/base.rb +47 -0
  30. data/lib/dynamoid/transaction_write/create.rb +49 -0
  31. data/lib/dynamoid/transaction_write/delete_with_instance.rb +60 -0
  32. data/lib/dynamoid/transaction_write/delete_with_primary_key.rb +59 -0
  33. data/lib/dynamoid/transaction_write/destroy.rb +79 -0
  34. data/lib/dynamoid/transaction_write/save.rb +164 -0
  35. data/lib/dynamoid/transaction_write/update_attributes.rb +46 -0
  36. data/lib/dynamoid/transaction_write/update_fields.rb +102 -0
  37. data/lib/dynamoid/transaction_write/upsert.rb +96 -0
  38. data/lib/dynamoid/transaction_write.rb +464 -0
  39. data/lib/dynamoid/type_casting.rb +3 -1
  40. data/lib/dynamoid/undumping.rb +13 -2
  41. data/lib/dynamoid/validations.rb +1 -1
  42. data/lib/dynamoid/version.rb +1 -1
  43. data/lib/dynamoid.rb +7 -0
  44. metadata +18 -5
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require 'dynamoid/persistence/update_validations'
5
+
6
+ module Dynamoid
7
+ class TransactionWrite
8
+ class UpdateFields < Base
9
+ def initialize(model_class, hash_key, range_key, attributes)
10
+ super()
11
+
12
+ @model_class = model_class
13
+ @hash_key = hash_key
14
+ @range_key = range_key
15
+ @attributes = attributes
16
+ end
17
+
18
+ def on_registration
19
+ validate_primary_key!
20
+ Dynamoid::Persistence::UpdateValidations.validate_attributes_exist(@model_class, @attributes)
21
+ end
22
+
23
+ def on_commit; end
24
+
25
+ def on_rollback; end
26
+
27
+ def aborted?
28
+ false
29
+ end
30
+
31
+ def skipped?
32
+ @attributes.empty?
33
+ end
34
+
35
+ def observable_by_user_result
36
+ nil
37
+ end
38
+
39
+ def action_request
40
+ # changed attributes to persist
41
+ changes = @attributes.dup
42
+ changes = add_timestamps(changes, skip_created_at: true)
43
+ changes_dumped = Dynamoid::Dumping.dump_attributes(changes, @model_class.attributes)
44
+
45
+ # primary key to look up an item to update
46
+ key = { @model_class.hash_key => @hash_key }
47
+ key[@model_class.range_key] = @range_key if @model_class.range_key?
48
+
49
+ # Build UpdateExpression and keep names and values placeholders mapping
50
+ # in ExpressionAttributeNames and ExpressionAttributeValues.
51
+ update_expression_statements = []
52
+ expression_attribute_names = {}
53
+ expression_attribute_values = {}
54
+
55
+ changes_dumped.each_with_index do |(name, value), i|
56
+ name_placeholder = "#_n#{i}"
57
+ value_placeholder = ":_s#{i}"
58
+
59
+ update_expression_statements << "#{name_placeholder} = #{value_placeholder}"
60
+ expression_attribute_names[name_placeholder] = name
61
+ expression_attribute_values[value_placeholder] = value
62
+ end
63
+
64
+ update_expression = "SET #{update_expression_statements.join(', ')}"
65
+
66
+ # require primary key to exist
67
+ condition_expression = "attribute_exists(#{@model_class.hash_key})"
68
+ if @model_class.range_key?
69
+ condition_expression += " AND attribute_exists(#{@model_class.range_key})"
70
+ end
71
+
72
+ {
73
+ update: {
74
+ key: key,
75
+ table_name: @model_class.table_name,
76
+ update_expression: update_expression,
77
+ expression_attribute_names: expression_attribute_names,
78
+ expression_attribute_values: expression_attribute_values,
79
+ condition_expression: condition_expression
80
+ }
81
+ }
82
+ end
83
+
84
+ private
85
+
86
+ def validate_primary_key!
87
+ raise Dynamoid::Errors::MissingHashKey if @hash_key.nil?
88
+ raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @range_key.nil?
89
+ end
90
+
91
+ def add_timestamps(attributes, skip_created_at: false)
92
+ return attributes unless @model_class.timestamps_enabled?
93
+
94
+ result = attributes.clone
95
+ timestamp = DateTime.now.in_time_zone(Time.zone)
96
+ result[:created_at] ||= timestamp unless skip_created_at
97
+ result[:updated_at] ||= timestamp
98
+ result
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require 'dynamoid/persistence/update_validations'
5
+
6
+ module Dynamoid
7
+ class TransactionWrite
8
+ class Upsert < Base
9
+ def initialize(model_class, hash_key, range_key, attributes)
10
+ super()
11
+
12
+ @model_class = model_class
13
+ @hash_key = hash_key
14
+ @range_key = range_key
15
+ @attributes = attributes
16
+ end
17
+
18
+ def on_registration
19
+ validate_primary_key!
20
+ Dynamoid::Persistence::UpdateValidations.validate_attributes_exist(@model_class, @attributes)
21
+ end
22
+
23
+ def on_commit; end
24
+
25
+ def on_rollback; end
26
+
27
+ def aborted?
28
+ false
29
+ end
30
+
31
+ def skipped?
32
+ attributes_to_assign = @attributes.except(@model_class.hash_key, @model_class.range_key)
33
+ attributes_to_assign.empty? && !@model_class.timestamps_enabled?
34
+ end
35
+
36
+ def observable_by_user_result
37
+ nil
38
+ end
39
+
40
+ def action_request
41
+ # changed attributes to persist
42
+ changes = @attributes.dup
43
+ changes = add_timestamps(changes, skip_created_at: true)
44
+ changes_dumped = Dynamoid::Dumping.dump_attributes(changes, @model_class.attributes)
45
+
46
+ # primary key to look up an item to update
47
+ key = { @model_class.hash_key => @hash_key }
48
+ key[@model_class.range_key] = @range_key if @model_class.range_key?
49
+
50
+ # Build UpdateExpression and keep names and values placeholders mapping
51
+ # in ExpressionAttributeNames and ExpressionAttributeValues.
52
+ update_expression_statements = []
53
+ expression_attribute_names = {}
54
+ expression_attribute_values = {}
55
+
56
+ changes_dumped.each_with_index do |(name, value), i|
57
+ name_placeholder = "#_n#{i}"
58
+ value_placeholder = ":_s#{i}"
59
+
60
+ update_expression_statements << "#{name_placeholder} = #{value_placeholder}"
61
+ expression_attribute_names[name_placeholder] = name
62
+ expression_attribute_values[value_placeholder] = value
63
+ end
64
+
65
+ update_expression = "SET #{update_expression_statements.join(', ')}"
66
+
67
+ {
68
+ update: {
69
+ key: key,
70
+ table_name: @model_class.table_name,
71
+ update_expression: update_expression,
72
+ expression_attribute_names: expression_attribute_names,
73
+ expression_attribute_values: expression_attribute_values
74
+ }
75
+ }
76
+ end
77
+
78
+ private
79
+
80
+ def validate_primary_key!
81
+ raise Dynamoid::Errors::MissingHashKey if @hash_key.nil?
82
+ raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @range_key.nil?
83
+ end
84
+
85
+ def add_timestamps(attributes, skip_created_at: false)
86
+ return attributes unless @model_class.timestamps_enabled?
87
+
88
+ result = attributes.clone
89
+ timestamp = DateTime.now.in_time_zone(Time.zone)
90
+ result[:created_at] ||= timestamp unless skip_created_at
91
+ result[:updated_at] ||= timestamp
92
+ result
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,464 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dynamoid/transaction_write/create'
4
+ require 'dynamoid/transaction_write/delete_with_primary_key'
5
+ require 'dynamoid/transaction_write/delete_with_instance'
6
+ require 'dynamoid/transaction_write/destroy'
7
+ require 'dynamoid/transaction_write/save'
8
+ require 'dynamoid/transaction_write/update_fields'
9
+ require 'dynamoid/transaction_write/update_attributes'
10
+ require 'dynamoid/transaction_write/upsert'
11
+
12
+ module Dynamoid
13
+ # The class +TransactionWrite+ provides means to perform multiple modifying
14
+ # operations in transaction, that is atomically, so that either all of them
15
+ # succeed, or all of them fail.
16
+ #
17
+ # The persisting methods are supposed to be as close as possible to their
18
+ # non-transactional counterparts like +.create+, +#save+ and +#delete+:
19
+ #
20
+ # user = User.new()
21
+ # payment = Payment.find(1)
22
+ #
23
+ # Dynamoid::TransactionWrite.execute do |t|
24
+ # t.save! user
25
+ # t.create! Account, name: 'A'
26
+ # t.delete payment
27
+ # end
28
+ #
29
+ # The only difference is that the methods are called on a transaction
30
+ # instance and a model or a model class should be specified.
31
+ #
32
+ # So +user.save!+ becomes +t.save!(user)+, +Account.create!(name: 'A')+
33
+ # becomes +t.create!(Account, name: 'A')+, and +payment.delete+ becomes
34
+ # +t.delete(payment)+.
35
+ #
36
+ # A transaction can be used without a block. This way a transaction instance
37
+ # should be instantiated and committed manually with +#commit+ method:
38
+ #
39
+ # t = Dynamoid::TransactionWrite.new
40
+ #
41
+ # t.save! user
42
+ # t.create! Account, name: 'A'
43
+ # t.delete payment
44
+ #
45
+ # t.commit
46
+ #
47
+ # Some persisting methods are intentionally not available in a transaction,
48
+ # e.g. +.update+ and +.update!+ that simply call +.find+ and
49
+ # +#update_attributes+ methods. These methods perform multiple operations so
50
+ # cannot be implemented in a transactional atomic way.
51
+ #
52
+ #
53
+ # ### DynamoDB's transactions
54
+ #
55
+ # The main difference between DynamoDB transactions and a common interface is
56
+ # that DynamoDB's transactions are executed in batch. So in Dynamoid no
57
+ # changes are actually persisted when some transactional method (e.g+ `#save+) is
58
+ # called. All the changes are persisted at the end.
59
+ #
60
+ # A +TransactWriteItems+ DynamoDB operation is used (see
61
+ # [documentation](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactWriteItems.html)
62
+ # for details).
63
+ #
64
+ #
65
+ # ### Callbacks
66
+ #
67
+ # The transactional methods support +before_+, +after_+ and +around_+
68
+ # callbacks to the extend the non-transactional methods support them.
69
+ #
70
+ # There is important difference - a transactional method runs callbacks
71
+ # immediately (even +after_+ ones) when it is called before changes are
72
+ # actually persisted. So code in +after_+ callbacks does not see observes
73
+ # them in DynamoDB and so for.
74
+ #
75
+ # When a callback aborts persisting of a model or a model is invalid then
76
+ # transaction is not aborted and may commit successfully.
77
+ #
78
+ #
79
+ # ### Transaction rollback
80
+ #
81
+ # A transaction is rolled back on DynamoDB's side automatically when:
82
+ # - an ongoing operation is in the process of updating the same item.
83
+ # - there is insufficient provisioned capacity for the transaction to be completed.
84
+ # - an item size becomes too large (bigger than 400 KB), a local secondary index (LSI) becomes too large, or a similar validation error occurs because of changes made by the transaction.
85
+ # - the aggregate size of the items in the transaction exceeds 4 MB.
86
+ # - there is a user error, such as an invalid data format.
87
+ #
88
+ # A transaction can be interrupted simply by an exception raised within a
89
+ # block. As far as no changes are actually persisted before the +#commit+
90
+ # method call - there is nothing to undo on the DynamoDB's site.
91
+ #
92
+ # Raising +Dynamoid::Errors::Rollback+ exception leads to interrupting a
93
+ # transation and it isn't propogated:
94
+ #
95
+ # Dynamoid::TransactionWrite.execute do |t|
96
+ # t.save! user
97
+ # t.create! Account, name: 'A'
98
+ #
99
+ # if user.is_admin?
100
+ # raise Dynamoid::Errors::Rollback
101
+ # end
102
+ # end
103
+ #
104
+ # When a transaction is successfully committed or rolled backed -
105
+ # corresponding +#after_commit+ or +#after_rollback+ callbacks are run for
106
+ # each involved model.
107
+ class TransactionWrite
108
+ def self.execute
109
+ transaction = new
110
+
111
+ begin
112
+ yield transaction
113
+ rescue StandardError => e
114
+ transaction.rollback
115
+
116
+ unless e.is_a?(Dynamoid::Errors::Rollback)
117
+ raise e
118
+ end
119
+ else
120
+ transaction.commit
121
+ end
122
+ end
123
+
124
+ def initialize
125
+ @actions = []
126
+ end
127
+
128
+ # Persist all the changes.
129
+ #
130
+ # transaction = Dynamoid::TransactionWrite.new
131
+ # # ...
132
+ # transaction.commit
133
+ def commit
134
+ actions_to_commit = @actions.reject(&:aborted?).reject(&:skipped?)
135
+ return if actions_to_commit.empty?
136
+
137
+ action_requests = actions_to_commit.map(&:action_request)
138
+ Dynamoid.adapter.transact_write_items(action_requests)
139
+ actions_to_commit.each(&:on_commit)
140
+
141
+ nil
142
+ rescue Aws::Errors::ServiceError
143
+ run_on_rollback_callbacks
144
+ raise
145
+ end
146
+
147
+ def rollback
148
+ run_on_rollback_callbacks
149
+ end
150
+
151
+ # Create new model or persist changes in already existing one.
152
+ #
153
+ # Run the validation and callbacks. Returns +true+ if saving is successful
154
+ # and +false+ otherwise.
155
+ #
156
+ # user = User.new
157
+ #
158
+ # Dynamoid::TransactionWrite.execute do |t|
159
+ # t.save!(user)
160
+ # end
161
+ #
162
+ # Validation can be skipped with +validate: false+ option:
163
+ #
164
+ # user = User.new(age: -1)
165
+ #
166
+ # Dynamoid::TransactionWrite.execute do |t|
167
+ # t.save!(user, validate: false)
168
+ # end
169
+ #
170
+ # +save!+ by default sets timestamps attributes - +created_at+ and
171
+ # +updated_at+ when creates new model and updates +updated_at+ attribute
172
+ # when updates already existing one.
173
+ #
174
+ # If a model is new and hash key (+id+ by default) is not assigned yet
175
+ # it was assigned implicitly with random UUID value.
176
+ #
177
+ # When a model is not persisted - its id should have unique value.
178
+ # Otherwise a transaction will be rolled back.
179
+ #
180
+ # @param model [Dynamoid::Document] a model
181
+ # @param options [Hash] (optional)
182
+ # @option options [true|false] :validate validate a model or not - +true+ by default (optional)
183
+ # @return [true|false] Whether saving successful or not
184
+ def save!(model, **options)
185
+ action = Dynamoid::TransactionWrite::Save.new(model, **options, raise_error: true)
186
+ register_action action
187
+ end
188
+
189
+ # Create new model or persist changes in already existing one.
190
+ #
191
+ # Run the validation and callbacks. Raise
192
+ # +Dynamoid::Errors::DocumentNotValid+ unless this object is valid.
193
+ #
194
+ # user = User.new
195
+ #
196
+ # Dynamoid::TransactionWrite.execute do |t|
197
+ # t.save(user)
198
+ # end
199
+ #
200
+ # Validation can be skipped with +validate: false+ option:
201
+ #
202
+ # user = User.new(age: -1)
203
+ #
204
+ # Dynamoid::TransactionWrite.execute do |t|
205
+ # t.save(user, validate: false)
206
+ # end
207
+ #
208
+ # +save+ by default sets timestamps attributes - +created_at+ and
209
+ # +updated_at+ when creates new model and updates +updated_at+ attribute
210
+ # when updates already existing one.
211
+ #
212
+ # If a model is new and hash key (+id+ by default) is not assigned yet
213
+ # it was assigned implicitly with random UUID value.
214
+ #
215
+ # When a model is not persisted - its id should have unique value.
216
+ # Otherwise a transaction will be rolled back.
217
+ #
218
+ # @param model [Dynamoid::Document] a model
219
+ # @param options [Hash] (optional)
220
+ # @option options [true|false] :validate validate a model or not - +true+ by default (optional)
221
+ # @return [true|false] Whether saving successful or not
222
+ def save(model, **options)
223
+ action = Dynamoid::TransactionWrite::Save.new(model, **options, raise_error: false)
224
+ register_action action
225
+ end
226
+
227
+ # Create a model.
228
+ #
229
+ # Dynamoid::TransactionWrite.execute do |t|
230
+ # t.create!(User, name: 'A')
231
+ # end
232
+ #
233
+ # Accepts both Hash and Array of Hashes and can create several models.
234
+ #
235
+ # Dynamoid::TransactionWrite.execute do |t|
236
+ # t.create!(User, [{name: 'A'}, {name: 'B'}, {name: 'C'}])
237
+ # end
238
+ #
239
+ # Instantiates a model and pass it into an optional block to set other
240
+ # attributes.
241
+ #
242
+ # Dynamoid::TransactionWrite.execute do |t|
243
+ # t.create!(User, name: 'A') do |user|
244
+ # user.initialize_roles
245
+ # end
246
+ # end
247
+ #
248
+ # Validates model and runs callbacks.
249
+ #
250
+ # @param model_class [Class] a model class which should be instantiated
251
+ # @param attributes [Hash|Array<Hash>] attributes of a model
252
+ # @param block [Proc] a block to process a model after initialization
253
+ # @return [Dynamoid::Document] a model that was instantiated but not yet persisted
254
+ def create!(model_class, attributes = {}, &block)
255
+ if attributes.is_a? Array
256
+ attributes.map do |attr|
257
+ action = Dynamoid::TransactionWrite::Create.new(model_class, attr, raise_error: true, &block)
258
+ register_action action
259
+ end
260
+ else
261
+ action = Dynamoid::TransactionWrite::Create.new(model_class, attributes, raise_error: true, &block)
262
+ register_action action
263
+ end
264
+ end
265
+
266
+ # Create a model.
267
+ #
268
+ # Dynamoid::TransactionWrite.execute do |t|
269
+ # t.create(User, name: 'A')
270
+ # end
271
+ #
272
+ # Accepts both Hash and Array of Hashes and can create several models.
273
+ #
274
+ # Dynamoid::TransactionWrite.execute do |t|
275
+ # t.create(User, [{name: 'A'}, {name: 'B'}, {name: 'C'}])
276
+ # end
277
+ #
278
+ # Instantiates a model and pass it into an optional block to set other
279
+ # attributes.
280
+ #
281
+ # Dynamoid::TransactionWrite.execute do |t|
282
+ # t.create(User, name: 'A') do |user|
283
+ # user.initialize_roles
284
+ # end
285
+ # end
286
+ #
287
+ # Validates model and runs callbacks.
288
+ #
289
+ # @param model_class [Class] a model class which should be instantiated
290
+ # @param attributes [Hash|Array<Hash>] attributes of a model
291
+ # @param block [Proc] a block to process a model after initialization
292
+ # @return [Dynamoid::Document] a model that was instantiated but not yet persisted
293
+ def create(model_class, attributes = {}, &block)
294
+ if attributes.is_a? Array
295
+ attributes.map do |attr|
296
+ action = Dynamoid::TransactionWrite::Create.new(model_class, attr, raise_error: false, &block)
297
+ register_action action
298
+ end
299
+ else
300
+ action = Dynamoid::TransactionWrite::Create.new(model_class, attributes, raise_error: false, &block)
301
+ register_action action
302
+ end
303
+ end
304
+
305
+ # Update an existing document or create a new one.
306
+ #
307
+ # If a document with specified hash and range keys doesn't exist it
308
+ # creates a new document with specified attributes. Doesn't run
309
+ # validations and callbacks.
310
+ #
311
+ # Dynamoid::TransactionWrite.execute do |t|
312
+ # t.upsert(User, '1', age: 26)
313
+ # end
314
+ #
315
+ # If range key is declared for a model it should be passed as well:
316
+ #
317
+ # Dynamoid::TransactionWrite.execute do |t|
318
+ # t.upsert(User, '1', 'Tylor', age: 26)
319
+ # end
320
+ #
321
+ # Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
322
+ # attributes is not declared in the model class.
323
+ #
324
+ # @param model_class [Class] a model class
325
+ # @param hash_key [Scalar value] hash key value
326
+ # @param range_key [Scalar value] range key value (optional)
327
+ # @param attributes [Hash]
328
+ # @return [nil]
329
+ def upsert(model_class, hash_key, range_key = nil, attributes) # rubocop:disable Style/OptionalArguments
330
+ action = Dynamoid::TransactionWrite::Upsert.new(model_class, hash_key, range_key, attributes)
331
+ register_action action
332
+ end
333
+
334
+ # Update document.
335
+ #
336
+ # Doesn't run validations and callbacks.
337
+ #
338
+ # Dynamoid::TransactionWrite.execute do |t|
339
+ # t.update_fields(User, '1', age: 26)
340
+ # end
341
+ #
342
+ # If range key is declared for a model it should be passed as well:
343
+ #
344
+ # Dynamoid::TransactionWrite.execute do |t|
345
+ # t.update_fields(User, '1', 'Tylor', age: 26)
346
+ # end
347
+ #
348
+ # Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
349
+ # attributes is not declared in the model class.
350
+ #
351
+ # @param model_class [Class] a model class
352
+ # @param hash_key [Scalar value] hash key value
353
+ # @param range_key [Scalar value] range key value (optional)
354
+ # @param attributes [Hash]
355
+ # @return [nil]
356
+ def update_fields(model_class, hash_key, range_key = nil, attributes) # rubocop:disable Style/OptionalArguments
357
+ action = Dynamoid::TransactionWrite::UpdateFields.new(model_class, hash_key, range_key, attributes)
358
+ register_action action
359
+ end
360
+
361
+ # Update multiple attributes at once.
362
+ #
363
+ # Dynamoid::TransactionWrite.execute do |t|
364
+ # t.update_attributes(user, age: 27, last_name: 'Tylor')
365
+ # end
366
+ #
367
+ # Returns +true+ if saving is successful and +false+
368
+ # otherwise.
369
+ #
370
+ # @param model [Dynamoid::Document] a model
371
+ # @param attributes [Hash] a hash of attributes to update
372
+ # @return [true|false] Whether updating successful or not
373
+ def update_attributes(model, attributes)
374
+ action = Dynamoid::TransactionWrite::UpdateAttributes.new(model, attributes, raise_error: false)
375
+ register_action action
376
+ end
377
+
378
+ # Update multiple attributes at once.
379
+ #
380
+ # Returns +true+ if saving is successful and +false+
381
+ # otherwise.
382
+ #
383
+ # Dynamoid::TransactionWrite.execute do |t|
384
+ # t.update_attributes(user, age: 27, last_name: 'Tylor')
385
+ # end
386
+ #
387
+ # Raises a +Dynamoid::Errors::DocumentNotValid+ exception if some vaidation
388
+ # fails.
389
+ #
390
+ # @param model [Dynamoid::Document] a model
391
+ # @param attributes [Hash] a hash of attributes to update
392
+ def update_attributes!(model, attributes)
393
+ action = Dynamoid::TransactionWrite::UpdateAttributes.new(model, attributes, raise_error: true)
394
+ register_action action
395
+ end
396
+
397
+ # Delete a model.
398
+ #
399
+ # Can be called either with a model:
400
+ #
401
+ # Dynamoid::TransactionWrite.execute do |t|
402
+ # t.delete(user)
403
+ # end
404
+ #
405
+ # or with a primary key:
406
+ #
407
+ # Dynamoid::TransactionWrite.execute do |t|
408
+ # t.delete(User, user_id)
409
+ # end
410
+ #
411
+ # Raise +MissingRangeKey+ if a range key is declared but not passed as argument.
412
+ #
413
+ # @param model_or_model_class [Class|Dynamoid::Document] either model or model class
414
+ # @param hash_key [Scalar value] hash key value
415
+ # @param range_key [Scalar value] range key value (optional)
416
+ # @return [Dynamoid::Document] self
417
+ def delete(model_or_model_class, hash_key = nil, range_key = nil)
418
+ action = if model_or_model_class.is_a? Class
419
+ Dynamoid::TransactionWrite::DeleteWithPrimaryKey.new(model_or_model_class, hash_key, range_key)
420
+ else
421
+ Dynamoid::TransactionWrite::DeleteWithInstance.new(model_or_model_class)
422
+ end
423
+ register_action action
424
+ end
425
+
426
+ # Delete a model.
427
+ #
428
+ # Runs callbacks.
429
+ #
430
+ # Raises +Dynamoid::Errors::RecordNotDestroyed+ exception if model deleting
431
+ # failed (e.g. aborted by a callback).
432
+ #
433
+ # @param model [Dynamoid::Document] a model
434
+ # @return [Dynamoid::Document|false] returns self if destoying is succefull, +false+ otherwise
435
+ def destroy!(model)
436
+ action = Dynamoid::TransactionWrite::Destroy.new(model, raise_error: true)
437
+ register_action action
438
+ end
439
+
440
+ # Delete a model.
441
+ #
442
+ # Runs callbacks.
443
+ #
444
+ # @param model [Dynamoid::Document] a model
445
+ # @return [Dynamoid::Document] self
446
+ def destroy(model)
447
+ action = Dynamoid::TransactionWrite::Destroy.new(model, raise_error: false)
448
+ register_action action
449
+ end
450
+
451
+ private
452
+
453
+ def register_action(action)
454
+ @actions << action
455
+ action.on_registration
456
+ action.observable_by_user_result
457
+ end
458
+
459
+ def run_on_rollback_callbacks
460
+ actions_to_commit = @actions.reject(&:aborted?).reject(&:skipped?)
461
+ actions_to_commit.each(&:on_rollback)
462
+ end
463
+ end
464
+ end
@@ -288,7 +288,9 @@ module Dynamoid
288
288
 
289
289
  class BinaryTypeCaster < Base
290
290
  def process(value)
291
- if value.is_a? String
291
+ if value.is_a?(StringIO) || value.is_a?(IO)
292
+ value
293
+ elsif value.is_a?(String)
292
294
  value.dup
293
295
  else
294
296
  value.to_s
@@ -238,7 +238,8 @@ module Dynamoid
238
238
  class SerializedUndumper < Base
239
239
  # We must use YAML.safe_load in Ruby 3.1 to handle serialized Set class
240
240
  minimum_ruby_version = ->(version) { Gem::Version.new(RUBY_VERSION) >= Gem::Version.new(version) }
241
- # Once we drop support for Rubies older than 2.6 we can remove this conditional (with major version bump)!
241
+
242
+ # Once we drop support for Rubies older than 2.6 we can remove this condition (with major version bump)!
242
243
  # YAML_SAFE_LOAD = minimum_ruby_version.call("2.6")
243
244
  # But we don't want to change behavior for Ruby <= 3.0 that has been using the gem, without a major version bump
244
245
  YAML_SAFE_LOAD = minimum_ruby_version.call('3.1')
@@ -284,7 +285,17 @@ module Dynamoid
284
285
 
285
286
  class BinaryUndumper < Base
286
287
  def process(value)
287
- Base64.strict_decode64(value)
288
+ store_as_binary = if @options[:store_as_native_binary].nil?
289
+ Dynamoid.config.store_binary_as_native
290
+ else
291
+ @options[:store_as_native_binary]
292
+ end
293
+
294
+ if store_as_binary
295
+ value.string # expect StringIO here
296
+ else
297
+ Base64.strict_decode64(value)
298
+ end
288
299
  end
289
300
  end
290
301