dynamoid 3.8.0 → 3.9.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.
- 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
|