dynamoid 3.13.1 → 3.14.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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -5
  3. data/README.md +60 -1584
  4. data/lib/dynamoid/associations/association.rb +10 -2
  5. data/lib/dynamoid/associations/belongs_to.rb +3 -1
  6. data/lib/dynamoid/associations/has_and_belongs_to_many.rb +0 -1
  7. data/lib/dynamoid/associations/has_many.rb +0 -1
  8. data/lib/dynamoid/associations/has_one.rb +1 -2
  9. data/lib/dynamoid/associations/many_association.rb +1 -0
  10. data/lib/dynamoid/associations.rb +64 -48
  11. data/lib/dynamoid/components.rb +1 -0
  12. data/lib/dynamoid/criteria/chain.rb +43 -30
  13. data/lib/dynamoid/dirty.rb +3 -2
  14. data/lib/dynamoid/document.rb +29 -21
  15. data/lib/dynamoid/fields.rb +55 -41
  16. data/lib/dynamoid/finders.rb +1 -1
  17. data/lib/dynamoid/indexes.rb +29 -21
  18. data/lib/dynamoid/persistence/inc.rb +15 -7
  19. data/lib/dynamoid/persistence/save.rb +21 -14
  20. data/lib/dynamoid/persistence.rb +88 -54
  21. data/lib/dynamoid/transactions/mutation/base.rb +76 -0
  22. data/lib/dynamoid/transactions/mutation/builders/delete_request_builder.rb +44 -0
  23. data/lib/dynamoid/transactions/mutation/builders/update_request_builder.rb +139 -0
  24. data/lib/dynamoid/transactions/mutation/create.rb +52 -0
  25. data/lib/dynamoid/transactions/mutation/delete_with_instance.rb +80 -0
  26. data/lib/dynamoid/transactions/mutation/delete_with_primary_key.rb +62 -0
  27. data/lib/dynamoid/transactions/mutation/destroy.rb +96 -0
  28. data/lib/dynamoid/transactions/mutation/import.rb +84 -0
  29. data/lib/dynamoid/transactions/mutation/inc.rb +101 -0
  30. data/lib/dynamoid/transactions/mutation/increment.rb +63 -0
  31. data/lib/dynamoid/transactions/mutation/save.rb +194 -0
  32. data/lib/dynamoid/transactions/mutation/touch.rb +99 -0
  33. data/lib/dynamoid/transactions/mutation/update_attributes.rb +49 -0
  34. data/lib/dynamoid/transactions/mutation/update_fields.rb +185 -0
  35. data/lib/dynamoid/transactions/mutation/upsert.rb +94 -0
  36. data/lib/dynamoid/transactions/mutation.rb +935 -0
  37. data/lib/dynamoid/transactions/retrieval/find.rb +140 -0
  38. data/lib/dynamoid/transactions/retrieval.rb +150 -0
  39. data/lib/dynamoid/transactions.rb +63 -0
  40. data/lib/dynamoid/version.rb +1 -1
  41. data/lib/dynamoid.rb +1 -2
  42. metadata +23 -17
  43. data/lib/dynamoid/transaction_read/find.rb +0 -137
  44. data/lib/dynamoid/transaction_read.rb +0 -146
  45. data/lib/dynamoid/transaction_write/base.rb +0 -59
  46. data/lib/dynamoid/transaction_write/create.rb +0 -49
  47. data/lib/dynamoid/transaction_write/delete_with_instance.rb +0 -65
  48. data/lib/dynamoid/transaction_write/delete_with_primary_key.rb +0 -64
  49. data/lib/dynamoid/transaction_write/destroy.rb +0 -84
  50. data/lib/dynamoid/transaction_write/item_updater.rb +0 -60
  51. data/lib/dynamoid/transaction_write/save.rb +0 -177
  52. data/lib/dynamoid/transaction_write/update_attributes.rb +0 -46
  53. data/lib/dynamoid/transaction_write/update_fields.rb +0 -247
  54. data/lib/dynamoid/transaction_write/upsert.rb +0 -113
  55. data/lib/dynamoid/transaction_write.rb +0 -673
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative 'inc'
5
+
6
+ module Dynamoid
7
+ module Transactions
8
+ class Mutation
9
+ # @private
10
+ class Increment < Base
11
+ def initialize(model, attribute, by, touch: nil)
12
+ super()
13
+
14
+ @model = model
15
+ @attribute = attribute
16
+ @by = by
17
+ @touch = touch
18
+ @action_requests = [] # will be filled in later with Inc#action_requests
19
+ end
20
+
21
+ def on_registration
22
+ @model.run_callbacks(:touch) do
23
+ @model.increment(@attribute, @by)
24
+ change = @model.read_attribute(@attribute) - (@model.send(:attribute_was, @attribute) || 0)
25
+
26
+ inc_action = Inc.new(@model.class, @model.hash_key, @model.range_value, { @attribute => change, touch: @touch })
27
+ inc_action.on_registration
28
+ @action_requests = inc_action.action_requests
29
+ end
30
+ end
31
+
32
+ def on_commit
33
+ # ignore Inc#on_commit
34
+ @model.clear_attribute_changes(@attribute)
35
+ @model.run_callbacks(:commit)
36
+ end
37
+
38
+ def on_rollback
39
+ # ignore Inc#on_rollback
40
+ @model.run_callbacks(:rollback)
41
+ end
42
+
43
+ def aborted?
44
+ # ignore Inc#abort?
45
+ false
46
+ end
47
+
48
+ def skipped?
49
+ # ignore Inc#skipped?
50
+ @by == 0 # rubocop:disable Style/NumericPredicate
51
+ end
52
+
53
+ def observable_by_user_result
54
+ @model
55
+ end
56
+
57
+ def action_requests # rubocop:disable Style/TrivialAccessors
58
+ @action_requests
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative 'builders/update_request_builder'
5
+
6
+ module Dynamoid
7
+ module Transactions
8
+ class Mutation
9
+ # @private
10
+ class Save < Base
11
+ def initialize(model, **options)
12
+ super()
13
+
14
+ @model = model
15
+ @model_class = model.class
16
+ @options = options
17
+
18
+ @aborted = false
19
+ @was_new_record = model.new_record?
20
+ @valid = nil
21
+ end
22
+
23
+ def on_registration
24
+ if @options[:validate] != false && !(@valid = @model.valid?)
25
+ if @options[:raise_error]
26
+ raise Dynamoid::Errors::DocumentNotValid, @model
27
+ else
28
+ @aborted = true
29
+ return
30
+ end
31
+ end
32
+
33
+ @aborted = true
34
+ callback_name = @was_new_record ? :create : :update
35
+
36
+ @model.run_callbacks(:save) do
37
+ @model.run_callbacks(callback_name) do
38
+ @model.run_callbacks(:validate) do
39
+ validate_primary_key!
40
+
41
+ @aborted = false
42
+ true
43
+ end
44
+ end
45
+ end
46
+
47
+ if @aborted && @options[:raise_error]
48
+ raise Dynamoid::Errors::RecordNotSaved, @model
49
+ end
50
+
51
+ if @was_new_record && @model.hash_key.nil?
52
+ @model.hash_key = SecureRandom.uuid
53
+ end
54
+
55
+ if @model.class.attributes[:lock_version]
56
+ if @model.lock_version.nil? && @model.new_record?
57
+ @model.lock_version = 1
58
+ end
59
+
60
+ if @model.lock_version && !@model.changes[:lock_version]
61
+ @model.lock_version += 1
62
+ end
63
+ end
64
+ end
65
+
66
+ def on_commit
67
+ return if @aborted
68
+
69
+ @model.changes_applied
70
+
71
+ if @was_new_record
72
+ @model.new_record = false
73
+ end
74
+
75
+ @model.run_callbacks(:commit)
76
+ end
77
+
78
+ def on_rollback
79
+ @model.run_callbacks(:rollback)
80
+ end
81
+
82
+ def aborted?
83
+ @aborted
84
+ end
85
+
86
+ def skipped?
87
+ @model.persisted? && !@model.changed?
88
+ end
89
+
90
+ def observable_by_user_result
91
+ !@aborted
92
+ end
93
+
94
+ def action_requests
95
+ request = if @was_new_record
96
+ action_request_to_create
97
+ else
98
+ action_request_to_update
99
+ end
100
+
101
+ [request]
102
+ end
103
+
104
+ private
105
+
106
+ def validate_primary_key!
107
+ raise Dynamoid::Errors::MissingHashKey if !@was_new_record && @model.hash_key.nil?
108
+ raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @model.range_value.nil?
109
+ end
110
+
111
+ def action_request_to_create
112
+ touch_model_timestamps(skip_created_at: false)
113
+
114
+ attributes_dumped = Dynamoid::Dumping.dump_attributes(@model.attributes, @model_class.attributes)
115
+ attributes_dumped = sanitize_item(attributes_dumped)
116
+
117
+ # require primary key not to exist yet
118
+ expression_attribute_names = { '#_h' => @model_class.hash_key }
119
+ condition = 'attribute_not_exists(#_h)'
120
+
121
+ if @model_class.range_key?
122
+ expression_attribute_names['#_r'] = @model_class.range_key
123
+ condition += ' AND attribute_not_exists(#_r)'
124
+ end
125
+
126
+ {
127
+ put: {
128
+ item: attributes_dumped,
129
+ table_name: @model_class.table_name,
130
+ condition_expression: condition,
131
+ expression_attribute_names: expression_attribute_names
132
+ }
133
+ }
134
+ end
135
+
136
+ def action_request_to_update
137
+ touch_model_timestamps(skip_created_at: true)
138
+
139
+ # changed attributes to persist
140
+ changes = @model.attributes.slice(*@model.changed.map(&:to_sym))
141
+ changes_dumped = Dynamoid::Dumping.dump_attributes(changes, @model_class.attributes)
142
+
143
+ builder = Builders::UpdateRequestBuilder.new(@model_class)
144
+ builder.hash_key = dump_attribute(@model_class.hash_key, @model.hash_key)
145
+ builder.range_key = dump_attribute(@model_class.range_key, @model.range_value) if @model_class.range_key?
146
+
147
+ attributes_to_set = {}
148
+ attributes_to_remove = []
149
+
150
+ changes_dumped.each do |name, value|
151
+ if value || Dynamoid.config.store_attribute_with_nil_value
152
+ attributes_to_set[name] = value
153
+ else
154
+ attributes_to_remove << name
155
+ end
156
+ end
157
+
158
+ builder.set_attributes(attributes_to_set)
159
+ builder.remove_attributes(attributes_to_remove)
160
+
161
+ if @model_class.attributes[:lock_version]
162
+ lock_version = if @model.changes[:lock_version].nil?
163
+ @model.lock_version
164
+ else
165
+ @model.changes[:lock_version][0]
166
+ end
167
+
168
+ # skip concurrency control when lock_version is nil
169
+ if lock_version
170
+ builder.add_expression_attribute_name('#_lock_version', 'lock_version')
171
+ builder.add_expression_attribute_value(':lock_version_value', lock_version)
172
+ builder.condition_expression = '#_lock_version = :lock_version_value'
173
+ end
174
+ end
175
+
176
+ builder.request
177
+ end
178
+
179
+ def touch_model_timestamps(skip_created_at:)
180
+ return unless @model_class.timestamps_enabled?
181
+
182
+ timestamp = DateTime.now.in_time_zone(Time.zone)
183
+ @model.updated_at = timestamp unless @options[:touch] == false && !@was_new_record
184
+ @model.created_at ||= timestamp unless skip_created_at
185
+ end
186
+
187
+ def dump_attribute(name, value)
188
+ options = @model_class.attributes[name]
189
+ Dumping.dump_field(value, options)
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative 'builders/update_request_builder'
5
+
6
+ module Dynamoid
7
+ module Transactions
8
+ class Mutation
9
+ # @private
10
+ class Touch < Base
11
+ def initialize(model, *names, time: nil)
12
+ super()
13
+
14
+ @model = model
15
+ @names = names
16
+ @time = time
17
+ end
18
+
19
+ def on_registration
20
+ if @model.new_record?
21
+ raise Dynamoid::Errors::Error, 'cannot touch on a new or destroyed record object'
22
+ end
23
+
24
+ validate_primary_key!
25
+
26
+ @model.run_callbacks(:touch) do
27
+ @time_to_assign = @time || DateTime.now.in_time_zone(Time.zone)
28
+
29
+ @model.updated_at = @time_to_assign if @model.class.timestamps_enabled?
30
+ @names.each do |name|
31
+ @model.write_attribute(name, @time_to_assign)
32
+ end
33
+ end
34
+ end
35
+
36
+ def on_commit
37
+ attribute_names = @names.map(&:to_s)
38
+ attribute_names << 'updated_at' if @model.class.timestamps_enabled?
39
+ @model.clear_attribute_changes(attribute_names)
40
+ @model.run_callbacks(:commit)
41
+ end
42
+
43
+ def on_rollback
44
+ @model.run_callbacks(:rollback)
45
+ end
46
+
47
+ def aborted?
48
+ false
49
+ end
50
+
51
+ def skipped?
52
+ !@model.class.timestamps_enabled? && @names.empty?
53
+ end
54
+
55
+ def observable_by_user_result
56
+ @model
57
+ end
58
+
59
+ def action_requests
60
+ builder = Builders::UpdateRequestBuilder.new(@model.class)
61
+ builder.hash_key = dump(@model.class.hash_key, @model.hash_key)
62
+ builder.range_key = dump(@model.class.range_key, @model.range_value) if @model.class.range_key?
63
+
64
+ # require primary key to exist
65
+ builder.add_expression_attribute_name('#_h', @model.class.hash_key)
66
+ condition_expression = 'attribute_exists(#_h)'
67
+
68
+ if @model.class.range_key?
69
+ builder.add_expression_attribute_name('#_r', @model.class.range_key)
70
+ condition_expression += ' AND attribute_exists(#_r)'
71
+ end
72
+ builder.condition_expression = condition_expression
73
+
74
+ attributes_to_set = {}
75
+ attributes_to_set[:updated_at] = dump(:updated_at, @model[:updated_at]) if @model.class.timestamps_enabled?
76
+
77
+ @names.each do |name|
78
+ attributes_to_set[name] = dump(name, @model[name])
79
+ end
80
+
81
+ builder.set_attributes(attributes_to_set)
82
+ [builder.request]
83
+ end
84
+
85
+ private
86
+
87
+ def validate_primary_key!
88
+ raise Dynamoid::Errors::MissingHashKey if @model.hash_key.nil?
89
+ raise Dynamoid::Errors::MissingRangeKey if @model.class.range_key? && @model.range_value.nil?
90
+ end
91
+
92
+ def dump(name, value)
93
+ options = @model.class.attributes[name]
94
+ Dumping.dump_field(value, options)
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require 'dynamoid/persistence/update_validations'
5
+
6
+ module Dynamoid
7
+ module Transactions
8
+ class Mutation
9
+ # @private
10
+ class UpdateAttributes < Base
11
+ def initialize(model, attributes, **options)
12
+ super()
13
+
14
+ @model = model
15
+ @model.assign_attributes(attributes)
16
+ @save_action = Save.new(model, **options)
17
+ end
18
+
19
+ def on_registration
20
+ @save_action.on_registration
21
+ end
22
+
23
+ def on_commit
24
+ @save_action.on_commit
25
+ end
26
+
27
+ def on_rollback
28
+ @save_action.on_rollback
29
+ end
30
+
31
+ def aborted?
32
+ @save_action.aborted?
33
+ end
34
+
35
+ def skipped?
36
+ @save_action.skipped?
37
+ end
38
+
39
+ def observable_by_user_result
40
+ @save_action.observable_by_user_result
41
+ end
42
+
43
+ def action_requests
44
+ @save_action.action_requests
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative 'builders/update_request_builder'
5
+
6
+ module Dynamoid
7
+ module Transactions
8
+ class Mutation
9
+ # @private
10
+ class UpdateFields < Base
11
+ class ItemUpdater
12
+ attr_reader :attributes_to_set, :attributes_to_add, :attributes_to_delete, :attributes_to_remove
13
+
14
+ def initialize(model_class)
15
+ @model_class = model_class
16
+
17
+ @attributes_to_set = {}
18
+ @attributes_to_add = {}
19
+ @attributes_to_delete = {}
20
+ @attributes_to_remove = []
21
+ end
22
+
23
+ def empty?
24
+ [@attributes_to_set, @attributes_to_add, @attributes_to_delete, @attributes_to_remove].all?(&:empty?)
25
+ end
26
+
27
+ def set(attributes)
28
+ validate_attribute_names!(attributes.keys)
29
+ if Dynamoid.config.store_attribute_with_nil_value
30
+ @attributes_to_set.merge!(attributes)
31
+ else
32
+ @attributes_to_set.merge!(attributes.reject { |_, v| v.nil? })
33
+ @attributes_to_remove += attributes.select { |_, v| v.nil? }.keys
34
+ end
35
+ end
36
+
37
+ # adds to array of fields for use in REMOVE update expression
38
+ def remove(*names)
39
+ validate_attribute_names!(names)
40
+ @attributes_to_remove += names
41
+ end
42
+
43
+ # increments a number or adds to a set, starts at 0 or [] if it doesn't yet exist
44
+ def add(attributes)
45
+ validate_attribute_names!(attributes.keys)
46
+ @attributes_to_add.merge!(attributes)
47
+ end
48
+
49
+ # deletes a value or values from a set
50
+ def delete(attributes)
51
+ validate_attribute_names!(attributes.keys)
52
+ @attributes_to_delete.merge!(attributes)
53
+ end
54
+
55
+ private
56
+
57
+ def validate_attribute_names!(names)
58
+ UpdateFields.validate_attribute_names!(@model_class, names)
59
+ end
60
+ end
61
+
62
+ def initialize(model_class, hash_key, range_key, attributes, &block)
63
+ super()
64
+
65
+ @model_class = model_class
66
+ @hash_key = hash_key
67
+ @range_key = range_key
68
+ @attributes = attributes || {}
69
+ @block = block
70
+ end
71
+
72
+ def on_registration
73
+ validate_primary_key!
74
+ validate_attribute_names!(@model_class, @attributes.keys)
75
+
76
+ if @block
77
+ @item_updater = ItemUpdater.new(@model_class)
78
+ @block.call(@item_updater)
79
+ end
80
+ end
81
+
82
+ def on_commit; end
83
+
84
+ def on_rollback; end
85
+
86
+ def aborted?
87
+ false
88
+ end
89
+
90
+ def skipped?
91
+ @attributes.empty? && (!@item_updater || @item_updater.empty?)
92
+ end
93
+
94
+ def observable_by_user_result
95
+ nil
96
+ end
97
+
98
+ def action_requests
99
+ builder = Builders::UpdateRequestBuilder.new(@model_class)
100
+
101
+ # primary key to look up an item to update
102
+ builder.hash_key = cast_and_dump(@model_class.hash_key, @hash_key)
103
+ builder.range_key = cast_and_dump(@model_class.range_key, @range_key) if @model_class.range_key?
104
+
105
+ # require primary key to exist
106
+ builder.add_expression_attribute_name('#_h', @model_class.hash_key)
107
+ condition_expression = 'attribute_exists(#_h)'
108
+
109
+ if @model_class.range_key?
110
+ builder.add_expression_attribute_name('#_r', @model_class.range_key)
111
+ condition_expression += ' AND attribute_exists(#_r)'
112
+ end
113
+ builder.condition_expression = condition_expression
114
+
115
+ # changed attributes to persist
116
+ changes = add_timestamps(@attributes, skip_created_at: true)
117
+ changes_dumped = Dynamoid::Dumping.dump_attributes(changes, @model_class.attributes)
118
+
119
+ if Dynamoid.config.store_attribute_with_nil_value
120
+ builder.set_attributes(changes_dumped)
121
+ else
122
+ nil_attributes = changes_dumped.select { |_, v| v.nil? }
123
+ non_nil_attributes = changes_dumped.reject { |_, v| v.nil? } # rubocop:disable Style/PartitionInsteadOfDoubleSelect
124
+
125
+ builder.remove_attributes(nil_attributes.keys)
126
+ builder.set_attributes(non_nil_attributes)
127
+ end
128
+
129
+ # given a block
130
+ if @item_updater
131
+ builder.set_attributes(@item_updater.attributes_to_set)
132
+ builder.remove_attributes(@item_updater.attributes_to_remove)
133
+
134
+ @item_updater.attributes_to_add.each do |name, value|
135
+ # The ADD section in UpdateExpressions requires values to be a
136
+ # set to update a set attribute.
137
+ # Allow specifying values as any Enumerable collection (e.g. Array).
138
+ # Allow a single value not wrapped into a Set
139
+ if @model_class.attributes[name][:type] == :set
140
+ value = value.is_a?(Enumerable) ? Set.new(value) : Set[value]
141
+ end
142
+
143
+ builder.add_value(name, value)
144
+ end
145
+
146
+ @item_updater.attributes_to_delete.each do |name, value|
147
+ # The DELETE section in UpdateExpressions requires values to be a
148
+ # set to update a set attribute.
149
+ # Allow specifying values as any Enumerable collection (e.g. Array).
150
+ # Allow a single value not wrapped into a Set
151
+ value = value.is_a?(Enumerable) ? Set.new(value) : Set[value]
152
+
153
+ builder.delete_value(name, value)
154
+ end
155
+ end
156
+
157
+ [builder.request]
158
+ end
159
+
160
+ private
161
+
162
+ def validate_primary_key!
163
+ raise Dynamoid::Errors::MissingHashKey if @hash_key.nil?
164
+ raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @range_key.nil?
165
+ end
166
+
167
+ def add_timestamps(attributes, skip_created_at: false)
168
+ return attributes unless @model_class.timestamps_enabled?
169
+
170
+ result = attributes.clone
171
+ timestamp = DateTime.now.in_time_zone(Time.zone)
172
+ result[:created_at] ||= timestamp unless skip_created_at
173
+ result[:updated_at] ||= timestamp
174
+ result
175
+ end
176
+
177
+ def cast_and_dump(name, value)
178
+ options = @model_class.attributes[name]
179
+ value_casted = TypeCasting.cast_field(value, options)
180
+ Dumping.dump_field(value_casted, options)
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative 'builders/update_request_builder'
5
+
6
+ module Dynamoid
7
+ module Transactions
8
+ class Mutation
9
+ # @private
10
+ class Upsert < Base
11
+ def initialize(model_class, hash_key, range_key, attributes)
12
+ super()
13
+
14
+ @model_class = model_class
15
+ @hash_key = hash_key
16
+ @range_key = range_key
17
+ @attributes = attributes
18
+ end
19
+
20
+ def on_registration
21
+ validate_primary_key!
22
+ validate_attribute_names!(@model_class, @attributes.keys)
23
+ end
24
+
25
+ def on_commit; end
26
+
27
+ def on_rollback; end
28
+
29
+ def aborted?
30
+ false
31
+ end
32
+
33
+ def skipped?
34
+ attributes_to_assign = @attributes.except(@model_class.hash_key, @model_class.range_key)
35
+ attributes_to_assign.empty? && !@model_class.timestamps_enabled?
36
+ end
37
+
38
+ def observable_by_user_result
39
+ nil
40
+ end
41
+
42
+ def action_requests
43
+ # changed attributes to persist
44
+ changes = @attributes.dup
45
+ changes = add_timestamps(changes, skip_created_at: true)
46
+ changes_dumped = Dynamoid::Dumping.dump_attributes(changes, @model_class.attributes)
47
+
48
+ builder = Builders::UpdateRequestBuilder.new(@model_class)
49
+ builder.hash_key = cast_and_dump(@model_class.hash_key, @hash_key)
50
+ builder.range_key = cast_and_dump(@model_class.range_key, @range_key) if @model_class.range_key?
51
+
52
+ attributes_to_set = {}
53
+ attributes_to_remove = []
54
+
55
+ changes_dumped.each do |name, value|
56
+ if value || Dynamoid.config.store_attribute_with_nil_value
57
+ attributes_to_set[name] = value
58
+ else
59
+ attributes_to_remove << name
60
+ end
61
+ end
62
+
63
+ builder.set_attributes(attributes_to_set)
64
+ builder.remove_attributes(attributes_to_remove)
65
+
66
+ [builder.request]
67
+ end
68
+
69
+ private
70
+
71
+ def validate_primary_key!
72
+ raise Dynamoid::Errors::MissingHashKey if @hash_key.nil?
73
+ raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @range_key.nil?
74
+ end
75
+
76
+ def add_timestamps(attributes, skip_created_at: false)
77
+ return attributes unless @model_class.timestamps_enabled?
78
+
79
+ result = attributes.clone
80
+ timestamp = DateTime.now.in_time_zone(Time.zone)
81
+ result[:created_at] ||= timestamp unless skip_created_at
82
+ result[:updated_at] ||= timestamp
83
+ result
84
+ end
85
+
86
+ def cast_and_dump(name, value)
87
+ options = @model_class.attributes[name]
88
+ value_casted = TypeCasting.cast_field(value, options)
89
+ Dumping.dump_field(value_casted, options)
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end