dynamoid 3.10.0 → 3.11.0

Sign up to get free protection for your applications and to get access to all the features.
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