dynamoid 3.8.0 → 3.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +45 -0
- data/README.md +96 -42
- data/SECURITY.md +17 -0
- data/dynamoid.gemspec +66 -0
- data/lib/dynamoid/adapter.rb +2 -1
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/execute_statement.rb +62 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +9 -1
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/limit.rb +3 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +29 -8
- data/lib/dynamoid/components.rb +2 -3
- data/lib/dynamoid/criteria/chain.rb +7 -6
- data/lib/dynamoid/dirty.rb +56 -56
- data/lib/dynamoid/document.rb +38 -2
- data/lib/dynamoid/errors.rb +2 -0
- data/lib/dynamoid/fields.rb +4 -20
- data/lib/dynamoid/finders.rb +9 -4
- data/lib/dynamoid/indexes.rb +2 -4
- data/lib/dynamoid/log/formatter.rb +19 -4
- data/lib/dynamoid/persistence/import.rb +4 -1
- data/lib/dynamoid/persistence/inc.rb +66 -0
- data/lib/dynamoid/persistence/save.rb +52 -5
- data/lib/dynamoid/persistence/update_validations.rb +1 -1
- data/lib/dynamoid/persistence.rb +91 -46
- data/lib/dynamoid/version.rb +1 -1
- metadata +27 -50
@@ -187,10 +187,10 @@ module Dynamoid
|
|
187
187
|
def first(*args)
|
188
188
|
n = args.first || 1
|
189
189
|
|
190
|
-
return
|
190
|
+
return dup.scan_limit(n).to_a.first(*args) if @query.blank?
|
191
191
|
return super if @key_fields_detector.non_key_present?
|
192
192
|
|
193
|
-
|
193
|
+
dup.record_limit(n).to_a.first(*args)
|
194
194
|
end
|
195
195
|
|
196
196
|
# Returns the last item matching the criteria.
|
@@ -487,7 +487,7 @@ module Dynamoid
|
|
487
487
|
def pluck(*args)
|
488
488
|
fields = args.map(&:to_sym)
|
489
489
|
|
490
|
-
scope =
|
490
|
+
scope = dup
|
491
491
|
scope.project(*fields)
|
492
492
|
|
493
493
|
if fields.many?
|
@@ -525,6 +525,7 @@ module Dynamoid
|
|
525
525
|
def pages
|
526
526
|
raw_pages.lazy.map do |items, options|
|
527
527
|
models = items.map { |i| source.from_database(i) }
|
528
|
+
models.each { |m| m.run_callbacks :find }
|
528
529
|
[models, options]
|
529
530
|
end.each
|
530
531
|
end
|
@@ -787,9 +788,9 @@ module Dynamoid
|
|
787
788
|
condition = {}
|
788
789
|
type = @source.inheritance_field
|
789
790
|
|
790
|
-
if @source.attributes.key?(type)
|
791
|
-
|
792
|
-
condition[:"#{type}.in"] =
|
791
|
+
if @source.attributes.key?(type) && !@source.abstract_class?
|
792
|
+
sti_names = @source.deep_subclasses.map(&:sti_name) << @source.sti_name
|
793
|
+
condition[:"#{type}.in"] = sti_names
|
793
794
|
end
|
794
795
|
|
795
796
|
condition
|
data/lib/dynamoid/dirty.rb
CHANGED
@@ -26,20 +26,18 @@ module Dynamoid
|
|
26
26
|
module ClassMethods
|
27
27
|
def update_fields(*)
|
28
28
|
super.tap do |model|
|
29
|
-
model.
|
29
|
+
model.clear_changes_information if model
|
30
30
|
end
|
31
31
|
end
|
32
32
|
|
33
33
|
def upsert(*)
|
34
34
|
super.tap do |model|
|
35
|
-
model.
|
35
|
+
model.clear_changes_information if model
|
36
36
|
end
|
37
37
|
end
|
38
38
|
|
39
39
|
def from_database(*)
|
40
|
-
super.tap
|
41
|
-
m.send(:clear_changes_information)
|
42
|
-
end
|
40
|
+
super.tap(&:clear_changes_information)
|
43
41
|
end
|
44
42
|
end
|
45
43
|
|
@@ -110,7 +108,7 @@ module Dynamoid
|
|
110
108
|
#
|
111
109
|
# @return [ActiveSupport::HashWithIndifferentAccess]
|
112
110
|
def changes
|
113
|
-
ActiveSupport::HashWithIndifferentAccess[changed.map { |
|
111
|
+
ActiveSupport::HashWithIndifferentAccess[changed.map { |name| [name, attribute_change(name)] }]
|
114
112
|
end
|
115
113
|
|
116
114
|
# Returns a hash of attributes that were changed before the model was saved.
|
@@ -137,6 +135,25 @@ module Dynamoid
|
|
137
135
|
@changed_attributes ||= ActiveSupport::HashWithIndifferentAccess.new
|
138
136
|
end
|
139
137
|
|
138
|
+
# Clear all dirty data: current changes and previous changes.
|
139
|
+
def clear_changes_information
|
140
|
+
@previously_changed = ActiveSupport::HashWithIndifferentAccess.new
|
141
|
+
@changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
|
142
|
+
end
|
143
|
+
|
144
|
+
# Clears dirty data and moves +changes+ to +previous_changes+.
|
145
|
+
def changes_applied
|
146
|
+
@previously_changed = changes
|
147
|
+
@changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
|
148
|
+
end
|
149
|
+
|
150
|
+
# Remove changes information for the provided attributes.
|
151
|
+
#
|
152
|
+
# @param attributes [Array[String]] - a list of attributes to clear changes for
|
153
|
+
def clear_attribute_changes(names)
|
154
|
+
attributes_changed_by_setter.except!(*names)
|
155
|
+
end
|
156
|
+
|
140
157
|
# Handle <tt>*_changed?</tt> for +method_missing+.
|
141
158
|
#
|
142
159
|
# person.attribute_changed?(:name) # => true
|
@@ -145,14 +162,14 @@ module Dynamoid
|
|
145
162
|
# person.attribute_changed?(:name, from: 'Alice', to: 'Bod')
|
146
163
|
#
|
147
164
|
# @private
|
148
|
-
# @param
|
165
|
+
# @param name [Symbol] attribute name
|
149
166
|
# @param options [Hash] conditions on +from+ and +to+ value (optional)
|
150
167
|
# @option options [Symbol] :from previous attribute value
|
151
168
|
# @option options [Symbol] :to current attribute value
|
152
|
-
def attribute_changed?(
|
153
|
-
result = changes_include?(
|
154
|
-
result &&= options[:to] ==
|
155
|
-
result &&= options[:from] == changed_attributes[
|
169
|
+
def attribute_changed?(name, options = {})
|
170
|
+
result = changes_include?(name)
|
171
|
+
result &&= options[:to] == read_attribute(name) if options.key?(:to)
|
172
|
+
result &&= options[:from] == changed_attributes[name] if options.key?(:from)
|
156
173
|
result
|
157
174
|
end
|
158
175
|
|
@@ -163,16 +180,16 @@ module Dynamoid
|
|
163
180
|
# person.attribute_was(:name) # => "Alice"
|
164
181
|
#
|
165
182
|
# @private
|
166
|
-
# @param
|
167
|
-
def attribute_was(
|
168
|
-
attribute_changed?(
|
183
|
+
# @param name [Symbol] attribute name
|
184
|
+
def attribute_was(name)
|
185
|
+
attribute_changed?(name) ? changed_attributes[name] : read_attribute(name)
|
169
186
|
end
|
170
187
|
|
171
188
|
# Restore all previous data of the provided attributes.
|
172
189
|
#
|
173
190
|
# @param attributes [Array[Symbol]] a list of attribute names
|
174
|
-
def restore_attributes(
|
175
|
-
|
191
|
+
def restore_attributes(names = changed)
|
192
|
+
names.each { |name| restore_attribute! name }
|
176
193
|
end
|
177
194
|
|
178
195
|
# Handles <tt>*_previously_changed?</tt> for +method_missing+.
|
@@ -183,10 +200,10 @@ module Dynamoid
|
|
183
200
|
# person.attribute_changed?(:name) # => true
|
184
201
|
#
|
185
202
|
# @private
|
186
|
-
# @param
|
203
|
+
# @param name [Symbol] attribute name
|
187
204
|
# @return [true|false]
|
188
|
-
def attribute_previously_changed?(
|
189
|
-
previous_changes_include?(
|
205
|
+
def attribute_previously_changed?(name)
|
206
|
+
previous_changes_include?(name)
|
190
207
|
end
|
191
208
|
|
192
209
|
# Handles <tt>*_previous_change</tt> for +method_missing+.
|
@@ -197,61 +214,49 @@ module Dynamoid
|
|
197
214
|
# person.attribute_previously_changed(:name) # => ["Alice", "Bob"]
|
198
215
|
#
|
199
216
|
# @private
|
200
|
-
# @param
|
217
|
+
# @param name [Symbol]
|
201
218
|
# @return [Array]
|
202
|
-
def attribute_previous_change(
|
203
|
-
previous_changes[
|
219
|
+
def attribute_previous_change(name)
|
220
|
+
previous_changes[name] if attribute_previously_changed?(name)
|
204
221
|
end
|
205
222
|
|
206
223
|
private
|
207
224
|
|
208
|
-
def changes_include?(
|
209
|
-
attributes_changed_by_setter.include?(
|
225
|
+
def changes_include?(name)
|
226
|
+
attributes_changed_by_setter.include?(name)
|
210
227
|
end
|
211
228
|
alias attribute_changed_by_setter? changes_include?
|
212
229
|
|
213
|
-
# Removes current changes and makes them accessible through +previous_changes+.
|
214
|
-
def changes_applied # :doc:
|
215
|
-
@previously_changed = changes
|
216
|
-
@changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
|
217
|
-
end
|
218
|
-
|
219
|
-
# Clear all dirty data: current changes and previous changes.
|
220
|
-
def clear_changes_information # :doc:
|
221
|
-
@previously_changed = ActiveSupport::HashWithIndifferentAccess.new
|
222
|
-
@changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
|
223
|
-
end
|
224
|
-
|
225
230
|
# Handle <tt>*_change</tt> for +method_missing+.
|
226
|
-
def attribute_change(
|
227
|
-
[changed_attributes[
|
231
|
+
def attribute_change(name)
|
232
|
+
[changed_attributes[name], read_attribute(name)] if attribute_changed?(name)
|
228
233
|
end
|
229
234
|
|
230
235
|
# Handle <tt>*_will_change!</tt> for +method_missing+.
|
231
|
-
def attribute_will_change!(
|
232
|
-
return if attribute_changed?(
|
236
|
+
def attribute_will_change!(name)
|
237
|
+
return if attribute_changed?(name)
|
233
238
|
|
234
239
|
begin
|
235
|
-
value =
|
240
|
+
value = read_attribute(name)
|
236
241
|
value = value.duplicable? ? value.clone : value
|
237
242
|
rescue TypeError, NoMethodError
|
238
243
|
end
|
239
244
|
|
240
|
-
set_attribute_was(
|
245
|
+
set_attribute_was(name, value)
|
241
246
|
end
|
242
247
|
|
243
248
|
# Handle <tt>restore_*!</tt> for +method_missing+.
|
244
|
-
def restore_attribute!(
|
245
|
-
if attribute_changed?(
|
246
|
-
|
247
|
-
clear_attribute_changes([
|
249
|
+
def restore_attribute!(name)
|
250
|
+
if attribute_changed?(name)
|
251
|
+
write_attribute(name, changed_attributes[name])
|
252
|
+
clear_attribute_changes([name])
|
248
253
|
end
|
249
254
|
end
|
250
255
|
|
251
|
-
# Returns +true+ if
|
256
|
+
# Returns +true+ if name were changed before the model was saved,
|
252
257
|
# +false+ otherwise.
|
253
|
-
def previous_changes_include?(
|
254
|
-
previous_changes.include?(
|
258
|
+
def previous_changes_include?(name)
|
259
|
+
previous_changes.include?(name)
|
255
260
|
end
|
256
261
|
|
257
262
|
# This is necessary because `changed_attributes` might be overridden in
|
@@ -259,13 +264,8 @@ module Dynamoid
|
|
259
264
|
alias attributes_changed_by_setter changed_attributes
|
260
265
|
|
261
266
|
# Force an attribute to have a particular "before" value
|
262
|
-
def set_attribute_was(
|
263
|
-
attributes_changed_by_setter[
|
264
|
-
end
|
265
|
-
|
266
|
-
# Remove changes information for the provided attributes.
|
267
|
-
def clear_attribute_changes(attributes)
|
268
|
-
attributes_changed_by_setter.except!(*attributes)
|
267
|
+
def set_attribute_was(name, old_value)
|
268
|
+
attributes_changed_by_setter[name] = old_value
|
269
269
|
end
|
270
270
|
end
|
271
271
|
end
|
data/lib/dynamoid/document.rb
CHANGED
@@ -160,6 +160,22 @@ module Dynamoid
|
|
160
160
|
end
|
161
161
|
end
|
162
162
|
|
163
|
+
attr_accessor :abstract_class
|
164
|
+
|
165
|
+
def abstract_class?
|
166
|
+
defined?(@abstract_class) && @abstract_class == true
|
167
|
+
end
|
168
|
+
|
169
|
+
def sti_name
|
170
|
+
name
|
171
|
+
end
|
172
|
+
|
173
|
+
def sti_class_for(type_name)
|
174
|
+
type_name.constantize
|
175
|
+
rescue NameError
|
176
|
+
raise Errors::SubclassNotFound, "STI subclass does not found. Subclass: '#{type_name}'"
|
177
|
+
end
|
178
|
+
|
163
179
|
# @private
|
164
180
|
def deep_subclasses
|
165
181
|
subclasses + subclasses.map(&:deep_subclasses).flatten
|
@@ -167,7 +183,7 @@ module Dynamoid
|
|
167
183
|
|
168
184
|
# @private
|
169
185
|
def choose_right_class(attrs)
|
170
|
-
attrs[inheritance_field] ? attrs[inheritance_field]
|
186
|
+
attrs[inheritance_field] ? sti_class_for(attrs[inheritance_field]) : self
|
171
187
|
end
|
172
188
|
end
|
173
189
|
|
@@ -206,7 +222,7 @@ module Dynamoid
|
|
206
222
|
load(attrs_with_defaults.merge(attrs_virtual))
|
207
223
|
|
208
224
|
if block
|
209
|
-
|
225
|
+
yield(self)
|
210
226
|
end
|
211
227
|
end
|
212
228
|
end
|
@@ -278,6 +294,26 @@ module Dynamoid
|
|
278
294
|
end
|
279
295
|
end
|
280
296
|
|
297
|
+
def inspect
|
298
|
+
# attributes order is:
|
299
|
+
# - partition key
|
300
|
+
# - sort key
|
301
|
+
# - user defined attributes
|
302
|
+
# - timestamps - created_at/updated_at
|
303
|
+
names = [self.class.hash_key]
|
304
|
+
names << self.class.range_key if self.class.range_key
|
305
|
+
names += self.class.attributes.keys - names - %i[created_at updated_at]
|
306
|
+
names << :created_at if self.class.attributes.key?(:created_at)
|
307
|
+
names << :updated_at if self.class.attributes.key?(:updated_at)
|
308
|
+
|
309
|
+
inspection = names.map do |name|
|
310
|
+
value = read_attribute(name)
|
311
|
+
"#{name}: #{value.inspect}"
|
312
|
+
end.join(', ')
|
313
|
+
|
314
|
+
"#<#{self.class.name} #{inspection}>"
|
315
|
+
end
|
316
|
+
|
281
317
|
private
|
282
318
|
|
283
319
|
def dumped_range_value
|
data/lib/dynamoid/errors.rb
CHANGED
data/lib/dynamoid/fields.rb
CHANGED
@@ -293,7 +293,7 @@ module Dynamoid
|
|
293
293
|
old_value = read_attribute(name)
|
294
294
|
|
295
295
|
unless attribute_is_present_on_model?(name)
|
296
|
-
raise Dynamoid::Errors::UnknownAttribute
|
296
|
+
raise Dynamoid::Errors::UnknownAttribute, "Attribute #{name} is not part of the model"
|
297
297
|
end
|
298
298
|
|
299
299
|
if association = @associations[name]
|
@@ -354,23 +354,6 @@ module Dynamoid
|
|
354
354
|
|
355
355
|
private
|
356
356
|
|
357
|
-
# Automatically called during the created callback to set the created_at time.
|
358
|
-
#
|
359
|
-
# @since 0.2.0
|
360
|
-
def set_created_at
|
361
|
-
self.created_at ||= DateTime.now.in_time_zone(Time.zone) if self.class.timestamps_enabled?
|
362
|
-
end
|
363
|
-
|
364
|
-
# Automatically called during the save callback to set the updated_at time.
|
365
|
-
#
|
366
|
-
# @since 0.2.0
|
367
|
-
def set_updated_at
|
368
|
-
# @_touch_record=false means explicit disabling
|
369
|
-
if self.class.timestamps_enabled? && changed? && !updated_at_changed? && @_touch_record != false
|
370
|
-
self.updated_at = DateTime.now.in_time_zone(Time.zone)
|
371
|
-
end
|
372
|
-
end
|
373
|
-
|
374
357
|
def set_expires_field
|
375
358
|
options = self.class.options[:expires]
|
376
359
|
|
@@ -386,11 +369,12 @@ module Dynamoid
|
|
386
369
|
|
387
370
|
def set_inheritance_field
|
388
371
|
# actually it does only following logic:
|
389
|
-
# self.type ||= self.class.
|
372
|
+
# self.type ||= self.class.sti_name if self.class.attributes[:type]
|
373
|
+
return if self.class.abstract_class?
|
390
374
|
|
391
375
|
type = self.class.inheritance_field
|
392
376
|
if self.class.attributes[type] && send(type).nil?
|
393
|
-
send("#{type}=", self.class.
|
377
|
+
send("#{type}=", self.class.sti_name)
|
394
378
|
end
|
395
379
|
end
|
396
380
|
|
data/lib/dynamoid/finders.rb
CHANGED
@@ -118,7 +118,7 @@ module Dynamoid
|
|
118
118
|
|
119
119
|
# @private
|
120
120
|
def _find_all(ids, options = {})
|
121
|
-
raise Errors::MissingRangeKey if range_key && ids.any? { |
|
121
|
+
raise Errors::MissingRangeKey if range_key && ids.any? { |_pk, sk| sk.nil? }
|
122
122
|
|
123
123
|
if range_key
|
124
124
|
ids = ids.map do |pk, sk|
|
@@ -151,7 +151,9 @@ module Dynamoid
|
|
151
151
|
end
|
152
152
|
|
153
153
|
if items.size == ids.size || !options[:raise_error]
|
154
|
-
items ? items.map { |i| from_database(i) } : []
|
154
|
+
models = items ? items.map { |i| from_database(i) } : []
|
155
|
+
models.each { |m| m.run_callbacks :find }
|
156
|
+
models
|
155
157
|
else
|
156
158
|
ids_list = range_key ? ids.map { |pk, sk| "(#{pk},#{sk})" } : ids.map(&:to_s)
|
157
159
|
message = "Couldn't find all #{name.pluralize} with primary keys [#{ids_list.join(', ')}] "
|
@@ -173,7 +175,9 @@ module Dynamoid
|
|
173
175
|
end
|
174
176
|
|
175
177
|
if item = Dynamoid.adapter.read(table_name, id, options.slice(:range_key, :consistent_read))
|
176
|
-
from_database(item)
|
178
|
+
model = from_database(item)
|
179
|
+
model.run_callbacks :find
|
180
|
+
model
|
177
181
|
elsif options[:raise_error]
|
178
182
|
primary_key = range_key ? "(#{id},#{options[:range_key]})" : id
|
179
183
|
message = "Couldn't find #{name} with primary key #{primary_key}"
|
@@ -294,7 +298,8 @@ module Dynamoid
|
|
294
298
|
# @private
|
295
299
|
# @since 0.2.0
|
296
300
|
def method_missing(method, *args)
|
297
|
-
|
301
|
+
# Cannot use Symbol#start_with? because it was introduced in Ruby 2.7, but we support Ruby >= 2.3
|
302
|
+
if method.to_s.start_with?('find')
|
298
303
|
ActiveSupport::Deprecation.warn("[Dynamoid] .#{method} is deprecated! Call .where instead of")
|
299
304
|
|
300
305
|
finder = method.to_s.split('_by_').first
|
data/lib/dynamoid/indexes.rb
CHANGED
@@ -169,10 +169,9 @@ module Dynamoid
|
|
169
169
|
# @return [Dynamoid::Indexes::Index, nil] index object or nil if it isn't found
|
170
170
|
def find_index_by_name(name)
|
171
171
|
string_name = name.to_s
|
172
|
-
indexes.each_value.detect{ |i| i.name.to_s == string_name }
|
172
|
+
indexes.each_value.detect { |i| i.name.to_s == string_name }
|
173
173
|
end
|
174
174
|
|
175
|
-
|
176
175
|
# Returns true iff the provided hash[,range] key combo is a local
|
177
176
|
# secondary index.
|
178
177
|
#
|
@@ -299,7 +298,6 @@ module Dynamoid
|
|
299
298
|
end
|
300
299
|
end
|
301
300
|
|
302
|
-
|
303
301
|
def validate_hash_key
|
304
302
|
validate_index_key(:hash_key, @hash_key)
|
305
303
|
end
|
@@ -319,7 +317,7 @@ module Dynamoid
|
|
319
317
|
|
320
318
|
key_dynamodb_type = dynamodb_type(key_field_attributes[:type], key_field_attributes)
|
321
319
|
if PERMITTED_KEY_DYNAMODB_TYPES.include?(key_dynamodb_type)
|
322
|
-
|
320
|
+
send("#{key_param}_schema=", { key_val => key_dynamodb_type })
|
323
321
|
else
|
324
322
|
errors.add(key_param, "Index :#{key_param} is not a valid key type")
|
325
323
|
end
|
@@ -1,10 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Dynamoid
|
2
4
|
module Log
|
5
|
+
# https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/Log/Formatter.html
|
6
|
+
# https://docs.aws.amazon.com/sdk-for-ruby/v2/api/Seahorse/Client/Response.html
|
7
|
+
# https://aws.amazon.com/ru/blogs/developer/logging-requests/
|
3
8
|
module Formatter
|
4
|
-
|
5
|
-
# https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/Log/Formatter.html
|
6
|
-
# https://docs.aws.amazon.com/sdk-for-ruby/v2/api/Seahorse/Client/Response.html
|
7
|
-
# https://aws.amazon.com/ru/blogs/developer/logging-requests/
|
8
9
|
class Debug
|
9
10
|
def format(response)
|
10
11
|
bold = "\x1b[1m"
|
@@ -21,6 +22,20 @@ module Dynamoid
|
|
21
22
|
].join("\n")
|
22
23
|
end
|
23
24
|
end
|
25
|
+
|
26
|
+
class Compact
|
27
|
+
def format(response)
|
28
|
+
bold = "\x1b[1m"
|
29
|
+
reset = "\x1b[0m"
|
30
|
+
|
31
|
+
[
|
32
|
+
response.context.operation.name,
|
33
|
+
bold,
|
34
|
+
response.context.http_request.body.string,
|
35
|
+
reset
|
36
|
+
].join(' ')
|
37
|
+
end
|
38
|
+
end
|
24
39
|
end
|
25
40
|
end
|
26
41
|
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dynamoid
|
4
|
+
module Persistence
|
5
|
+
# @private
|
6
|
+
class Inc
|
7
|
+
def self.call(model_class, hash_key, range_key = nil, counters)
|
8
|
+
new(model_class, hash_key, range_key, counters).call
|
9
|
+
end
|
10
|
+
|
11
|
+
# rubocop:disable Style/OptionalArguments
|
12
|
+
def initialize(model_class, hash_key, range_key = nil, counters)
|
13
|
+
@model_class = model_class
|
14
|
+
@hash_key = hash_key
|
15
|
+
@range_key = range_key
|
16
|
+
@counters = counters
|
17
|
+
end
|
18
|
+
# rubocop:enable Style/OptionalArguments
|
19
|
+
|
20
|
+
def call
|
21
|
+
touch = @counters.delete(:touch)
|
22
|
+
|
23
|
+
Dynamoid.adapter.update_item(@model_class.table_name, @hash_key, update_item_options) do |t|
|
24
|
+
@counters.each do |name, value|
|
25
|
+
t.add(name => cast_and_dump_attribute_value(name, value))
|
26
|
+
end
|
27
|
+
|
28
|
+
if touch
|
29
|
+
value = DateTime.now.in_time_zone(Time.zone)
|
30
|
+
|
31
|
+
timestamp_attributes_to_touch(touch).each do |name|
|
32
|
+
t.set(name => cast_and_dump_attribute_value(name, value))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def update_item_options
|
41
|
+
if @model_class.range_key
|
42
|
+
range_key_options = @model_class.attributes[@model_class.range_key]
|
43
|
+
value_casted = TypeCasting.cast_field(@range_key, range_key_options)
|
44
|
+
value_dumped = Dumping.dump_field(value_casted, range_key_options)
|
45
|
+
{ range_key: value_dumped }
|
46
|
+
else
|
47
|
+
{}
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def cast_and_dump_attribute_value(name, value)
|
52
|
+
value_casted = TypeCasting.cast_field(value, @model_class.attributes[name])
|
53
|
+
Dumping.dump_field(value_casted, @model_class.attributes[name])
|
54
|
+
end
|
55
|
+
|
56
|
+
def timestamp_attributes_to_touch(touch)
|
57
|
+
return [] unless touch
|
58
|
+
|
59
|
+
names = []
|
60
|
+
names << :updated_at if @model_class.timestamps_enabled?
|
61
|
+
names += Array.wrap(touch) if touch != true
|
62
|
+
names
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -4,24 +4,44 @@ module Dynamoid
|
|
4
4
|
module Persistence
|
5
5
|
# @private
|
6
6
|
class Save
|
7
|
-
def self.call(model)
|
8
|
-
new(model).call
|
7
|
+
def self.call(model, **options)
|
8
|
+
new(model, **options).call
|
9
9
|
end
|
10
10
|
|
11
|
-
def initialize(model)
|
11
|
+
def initialize(model, touch: nil)
|
12
12
|
@model = model
|
13
|
+
@touch = touch # touch=false means explicit disabling of updating the `updated_at` attribute
|
13
14
|
end
|
14
15
|
|
15
16
|
def call
|
16
17
|
@model.hash_key = SecureRandom.uuid if @model.hash_key.blank?
|
17
18
|
|
19
|
+
return true unless @model.changed?
|
20
|
+
|
21
|
+
@model.created_at ||= DateTime.now.in_time_zone(Time.zone) if @model.class.timestamps_enabled?
|
22
|
+
|
23
|
+
if @model.class.timestamps_enabled? && !@model.updated_at_changed? && !(@touch == false && @model.persisted?)
|
24
|
+
@model.updated_at = DateTime.now.in_time_zone(Time.zone)
|
25
|
+
end
|
26
|
+
|
18
27
|
# Add an optimistic locking check if the lock_version column exists
|
19
28
|
if @model.class.attributes[:lock_version]
|
20
29
|
@model.lock_version = (@model.lock_version || 0) + 1
|
21
30
|
end
|
22
31
|
|
23
|
-
|
24
|
-
|
32
|
+
if @model.new_record?
|
33
|
+
attributes_dumped = Dumping.dump_attributes(@model.attributes, @model.class.attributes)
|
34
|
+
Dynamoid.adapter.write(@model.class.table_name, attributes_dumped, conditions_for_write)
|
35
|
+
else
|
36
|
+
attributes_to_persist = @model.attributes.slice(*@model.changed.map(&:to_sym))
|
37
|
+
|
38
|
+
Dynamoid.adapter.update_item(@model.class.table_name, @model.hash_key, options_to_update_item) do |t|
|
39
|
+
attributes_to_persist.each do |name, value|
|
40
|
+
value_dumped = Dumping.dump_field(value, @model.class.attributes[name])
|
41
|
+
t.set(name => value_dumped)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
25
45
|
|
26
46
|
@model.new_record = false
|
27
47
|
true
|
@@ -59,6 +79,33 @@ module Dynamoid
|
|
59
79
|
|
60
80
|
conditions
|
61
81
|
end
|
82
|
+
|
83
|
+
def options_to_update_item
|
84
|
+
options = {}
|
85
|
+
|
86
|
+
if @model.class.range_key
|
87
|
+
value_dumped = Dumping.dump_field(@model.range_value, @model.class.attributes[@model.class.range_key])
|
88
|
+
options[:range_key] = value_dumped
|
89
|
+
end
|
90
|
+
|
91
|
+
conditions = {}
|
92
|
+
conditions[:if_exists] ||= {}
|
93
|
+
conditions[:if_exists][@model.class.hash_key] = @model.hash_key
|
94
|
+
|
95
|
+
# Add an optimistic locking check if the lock_version column exists
|
96
|
+
if @model.class.attributes[:lock_version]
|
97
|
+
# Uses the original lock_version value from Dirty API
|
98
|
+
# in case user changed 'lock_version' manually
|
99
|
+
if @model.changes[:lock_version][0]
|
100
|
+
conditions[:if] ||= {}
|
101
|
+
conditions[:if][:lock_version] = @model.changes[:lock_version][0]
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
options[:conditions] = conditions
|
106
|
+
|
107
|
+
options
|
108
|
+
end
|
62
109
|
end
|
63
110
|
end
|
64
111
|
end
|
@@ -9,7 +9,7 @@ module Dynamoid
|
|
9
9
|
|
10
10
|
attributes.each do |attr_name, _|
|
11
11
|
unless model_attributes.include?(attr_name)
|
12
|
-
raise Dynamoid::Errors::UnknownAttribute
|
12
|
+
raise Dynamoid::Errors::UnknownAttribute, "Attribute #{attr_name} does not exist in #{model_class}"
|
13
13
|
end
|
14
14
|
end
|
15
15
|
end
|