dynamoid 3.7.1 → 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 +246 -244
- data/LICENSE.txt +2 -2
- data/README.md +134 -55
- data/SECURITY.md +17 -0
- data/dynamoid.gemspec +66 -0
- data/lib/dynamoid/adapter.rb +7 -9
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/batch_get_item.rb +2 -2
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/execute_statement.rb +62 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +29 -15
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/limit.rb +3 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +73 -59
- data/lib/dynamoid/associations/single_association.rb +28 -9
- data/lib/dynamoid/components.rb +2 -3
- data/lib/dynamoid/criteria/chain.rb +13 -9
- data/lib/dynamoid/criteria.rb +6 -7
- data/lib/dynamoid/dirty.rb +60 -63
- data/lib/dynamoid/document.rb +42 -12
- data/lib/dynamoid/errors.rb +2 -0
- data/lib/dynamoid/fields.rb +19 -37
- data/lib/dynamoid/finders.rb +9 -4
- data/lib/dynamoid/indexes.rb +45 -40
- data/lib/dynamoid/loadable.rb +6 -1
- 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_fields.rb +1 -1
- data/lib/dynamoid/persistence/update_validations.rb +1 -1
- data/lib/dynamoid/persistence/upsert.rb +1 -1
- data/lib/dynamoid/persistence.rb +149 -61
- data/lib/dynamoid/undumping.rb +18 -0
- data/lib/dynamoid/validations.rb +6 -0
- data/lib/dynamoid/version.rb +1 -1
- data/lib/dynamoid.rb +1 -0
- metadata +30 -50
data/lib/dynamoid/document.rb
CHANGED
@@ -17,12 +17,6 @@ module Dynamoid
|
|
17
17
|
end
|
18
18
|
|
19
19
|
module ClassMethods
|
20
|
-
# @private
|
21
|
-
def table(options = {})
|
22
|
-
self.options = options
|
23
|
-
super if defined? super
|
24
|
-
end
|
25
|
-
|
26
20
|
def attr_readonly(*read_only_attributes)
|
27
21
|
self.read_only_attributes.concat read_only_attributes.map(&:to_s)
|
28
22
|
end
|
@@ -77,10 +71,10 @@ module Dynamoid
|
|
77
71
|
|
78
72
|
# Return the count of items for this class.
|
79
73
|
#
|
80
|
-
# It returns
|
81
|
-
# updates it
|
74
|
+
# It returns approximate value based on DynamoDB statistic. DynamoDB
|
75
|
+
# updates it periodically so the value can be no accurate.
|
82
76
|
#
|
83
|
-
# It's a
|
77
|
+
# It's a reletively cheap operation and doesn't read all the items in a
|
84
78
|
# table. It makes just one HTTP request to DynamoDB.
|
85
79
|
#
|
86
80
|
# @return [Integer] items count in a table
|
@@ -166,6 +160,22 @@ module Dynamoid
|
|
166
160
|
end
|
167
161
|
end
|
168
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
|
+
|
169
179
|
# @private
|
170
180
|
def deep_subclasses
|
171
181
|
subclasses + subclasses.map(&:deep_subclasses).flatten
|
@@ -173,7 +183,7 @@ module Dynamoid
|
|
173
183
|
|
174
184
|
# @private
|
175
185
|
def choose_right_class(attrs)
|
176
|
-
attrs[inheritance_field] ? attrs[inheritance_field]
|
186
|
+
attrs[inheritance_field] ? sti_class_for(attrs[inheritance_field]) : self
|
177
187
|
end
|
178
188
|
end
|
179
189
|
|
@@ -212,7 +222,7 @@ module Dynamoid
|
|
212
222
|
load(attrs_with_defaults.merge(attrs_virtual))
|
213
223
|
|
214
224
|
if block
|
215
|
-
|
225
|
+
yield(self)
|
216
226
|
end
|
217
227
|
end
|
218
228
|
end
|
@@ -220,7 +230,7 @@ module Dynamoid
|
|
220
230
|
# Check equality of two models.
|
221
231
|
#
|
222
232
|
# A model is equal to another model only if their primary keys (hash key
|
223
|
-
# and
|
233
|
+
# and optionally range key) are equal.
|
224
234
|
#
|
225
235
|
# @return [true|false]
|
226
236
|
# @since 0.2.0
|
@@ -284,6 +294,26 @@ module Dynamoid
|
|
284
294
|
end
|
285
295
|
end
|
286
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
|
+
|
287
317
|
private
|
288
318
|
|
289
319
|
def dumped_range_value
|
data/lib/dynamoid/errors.rb
CHANGED
data/lib/dynamoid/fields.rb
CHANGED
@@ -8,17 +8,6 @@ module Dynamoid
|
|
8
8
|
module Fields
|
9
9
|
extend ActiveSupport::Concern
|
10
10
|
|
11
|
-
# @private
|
12
|
-
# Types allowed in indexes:
|
13
|
-
PERMITTED_KEY_TYPES = %i[
|
14
|
-
number
|
15
|
-
integer
|
16
|
-
string
|
17
|
-
date
|
18
|
-
datetime
|
19
|
-
serialized
|
20
|
-
].freeze
|
21
|
-
|
22
11
|
# Initialize the attributes we know the class has, in addition to our magic attributes: id, created_at, and updated_at.
|
23
12
|
included do
|
24
13
|
class_attribute :attributes, instance_accessor: false
|
@@ -220,20 +209,26 @@ module Dynamoid
|
|
220
209
|
#
|
221
210
|
# @since 0.4.0
|
222
211
|
def table(options)
|
212
|
+
self.options = options
|
213
|
+
|
223
214
|
# a default 'id' column is created when Dynamoid::Document is included
|
224
215
|
unless attributes.key? hash_key
|
225
216
|
remove_field :id
|
226
217
|
field(hash_key)
|
227
218
|
end
|
228
219
|
|
220
|
+
# The created_at/updated_at fields are declared in the `included` callback first.
|
221
|
+
# At that moment the only known setting is `Dynamoid::Config.timestamps`.
|
222
|
+
# Now `options[:timestamps]` may override the global setting for a model.
|
223
|
+
# So we need to make decision again and declare the fields or rollback thier declaration.
|
224
|
+
#
|
225
|
+
# Do not replace with `#timestamps_enabled?`.
|
229
226
|
if options[:timestamps] && !Dynamoid::Config.timestamps
|
230
|
-
#
|
231
|
-
# are disabled globaly
|
227
|
+
# The fields weren't declared in `included` callback because they are disabled globaly
|
232
228
|
field :created_at, :datetime
|
233
229
|
field :updated_at, :datetime
|
234
230
|
elsif options[:timestamps] == false && Dynamoid::Config.timestamps
|
235
|
-
#
|
236
|
-
# disabled for a table
|
231
|
+
# The fields were declared in `included` callback but they are disabled for a table
|
237
232
|
remove_field :created_at
|
238
233
|
remove_field :updated_at
|
239
234
|
end
|
@@ -290,25 +285,28 @@ module Dynamoid
|
|
290
285
|
#
|
291
286
|
# @param name [Symbol] the name of the field
|
292
287
|
# @param value [Object] the value to assign to that field
|
288
|
+
# @return [Dynamoid::Document] self
|
293
289
|
#
|
294
290
|
# @since 0.2.0
|
295
291
|
def write_attribute(name, value)
|
296
292
|
name = name.to_sym
|
293
|
+
old_value = read_attribute(name)
|
297
294
|
|
298
295
|
unless attribute_is_present_on_model?(name)
|
299
|
-
raise Dynamoid::Errors::UnknownAttribute
|
296
|
+
raise Dynamoid::Errors::UnknownAttribute, "Attribute #{name} is not part of the model"
|
300
297
|
end
|
301
298
|
|
302
299
|
if association = @associations[name]
|
303
300
|
association.reset
|
304
301
|
end
|
305
302
|
|
306
|
-
attribute_will_change!(name) # Dirty API
|
307
|
-
|
308
303
|
@attributes_before_type_cast[name] = value
|
309
304
|
|
310
305
|
value_casted = TypeCasting.cast_field(value, self.class.attributes[name])
|
306
|
+
attribute_will_change!(name) if old_value != value_casted # Dirty API
|
307
|
+
|
311
308
|
attributes[name] = value_casted
|
309
|
+
self
|
312
310
|
end
|
313
311
|
alias []= write_attribute
|
314
312
|
|
@@ -356,23 +354,6 @@ module Dynamoid
|
|
356
354
|
|
357
355
|
private
|
358
356
|
|
359
|
-
# Automatically called during the created callback to set the created_at time.
|
360
|
-
#
|
361
|
-
# @since 0.2.0
|
362
|
-
def set_created_at
|
363
|
-
self.created_at ||= DateTime.now.in_time_zone(Time.zone) if self.class.timestamps_enabled?
|
364
|
-
end
|
365
|
-
|
366
|
-
# Automatically called during the save callback to set the updated_at time.
|
367
|
-
#
|
368
|
-
# @since 0.2.0
|
369
|
-
def set_updated_at
|
370
|
-
# @_touch_record=false means explicit disabling
|
371
|
-
if self.class.timestamps_enabled? && !updated_at_changed? && @_touch_record != false
|
372
|
-
self.updated_at = DateTime.now.in_time_zone(Time.zone)
|
373
|
-
end
|
374
|
-
end
|
375
|
-
|
376
357
|
def set_expires_field
|
377
358
|
options = self.class.options[:expires]
|
378
359
|
|
@@ -388,11 +369,12 @@ module Dynamoid
|
|
388
369
|
|
389
370
|
def set_inheritance_field
|
390
371
|
# actually it does only following logic:
|
391
|
-
# self.type ||= self.class.
|
372
|
+
# self.type ||= self.class.sti_name if self.class.attributes[:type]
|
373
|
+
return if self.class.abstract_class?
|
392
374
|
|
393
375
|
type = self.class.inheritance_field
|
394
376
|
if self.class.attributes[type] && send(type).nil?
|
395
|
-
send("#{type}=", self.class.
|
377
|
+
send("#{type}=", self.class.sti_name)
|
396
378
|
end
|
397
379
|
end
|
398
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
@@ -4,6 +4,15 @@ module Dynamoid
|
|
4
4
|
module Indexes
|
5
5
|
extend ActiveSupport::Concern
|
6
6
|
|
7
|
+
# @private
|
8
|
+
# @see https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.CoreComponents.html#HowItWorks.CoreComponents.PrimaryKey
|
9
|
+
# Types allowed in indexes
|
10
|
+
PERMITTED_KEY_DYNAMODB_TYPES = %i[
|
11
|
+
string
|
12
|
+
binary
|
13
|
+
number
|
14
|
+
].freeze
|
15
|
+
|
7
16
|
included do
|
8
17
|
class_attribute :local_secondary_indexes, instance_accessor: false
|
9
18
|
class_attribute :global_secondary_indexes, instance_accessor: false
|
@@ -20,17 +29,17 @@ module Dynamoid
|
|
20
29
|
#
|
21
30
|
# field :category
|
22
31
|
#
|
23
|
-
#
|
32
|
+
# global_secondary_index hash_key: :category
|
24
33
|
# end
|
25
34
|
#
|
26
35
|
# The full example with all the options being specified:
|
27
36
|
#
|
28
|
-
#
|
29
|
-
#
|
30
|
-
#
|
31
|
-
#
|
32
|
-
#
|
33
|
-
#
|
37
|
+
# global_secondary_index hash_key: :category,
|
38
|
+
# range_key: :created_at,
|
39
|
+
# name: 'posts_category_created_at_index',
|
40
|
+
# projected_attributes: :all,
|
41
|
+
# read_capacity: 100,
|
42
|
+
# write_capacity: 20
|
34
43
|
#
|
35
44
|
# Global secondary index should be declared after fields for mentioned
|
36
45
|
# hash key and optional range key are declared (with method +field+)
|
@@ -86,14 +95,14 @@ module Dynamoid
|
|
86
95
|
# range :created_at, :datetime
|
87
96
|
# field :author_id
|
88
97
|
#
|
89
|
-
#
|
98
|
+
# local_secondary_index range_key: :author_id
|
90
99
|
# end
|
91
100
|
#
|
92
101
|
# The full example with all the options being specified:
|
93
102
|
#
|
94
|
-
#
|
95
|
-
#
|
96
|
-
#
|
103
|
+
# local_secondary_index range_key: :created_at,
|
104
|
+
# name: 'posts_created_at_index',
|
105
|
+
# projected_attributes: :all
|
97
106
|
#
|
98
107
|
# Local secondary index should be declared after fields for mentioned
|
99
108
|
# hash key and optional range key are declared (with method +field+) as
|
@@ -160,10 +169,9 @@ module Dynamoid
|
|
160
169
|
# @return [Dynamoid::Indexes::Index, nil] index object or nil if it isn't found
|
161
170
|
def find_index_by_name(name)
|
162
171
|
string_name = name.to_s
|
163
|
-
indexes.each_value.detect{ |i| i.name.to_s == string_name }
|
172
|
+
indexes.each_value.detect { |i| i.name.to_s == string_name }
|
164
173
|
end
|
165
174
|
|
166
|
-
|
167
175
|
# Returns true iff the provided hash[,range] key combo is a local
|
168
176
|
# secondary index.
|
169
177
|
#
|
@@ -290,39 +298,36 @@ module Dynamoid
|
|
290
298
|
end
|
291
299
|
end
|
292
300
|
|
301
|
+
def validate_hash_key
|
302
|
+
validate_index_key(:hash_key, @hash_key)
|
303
|
+
end
|
304
|
+
|
293
305
|
def validate_range_key
|
294
|
-
|
295
|
-
range_field_attributes = @dynamoid_class.attributes[@range_key]
|
296
|
-
if range_field_attributes.present?
|
297
|
-
range_key_type = range_field_attributes[:type]
|
298
|
-
if Dynamoid::Fields::PERMITTED_KEY_TYPES.include?(range_key_type)
|
299
|
-
@range_key_schema = {
|
300
|
-
@range_key => PrimaryKeyTypeMapping.dynamodb_type(range_key_type, range_field_attributes)
|
301
|
-
}
|
302
|
-
else
|
303
|
-
errors.add(:range_key, 'Index :range_key is not a valid key type')
|
304
|
-
end
|
305
|
-
else
|
306
|
-
errors.add(:range_key, "No such field #{@range_key} defined on table")
|
307
|
-
end
|
308
|
-
end
|
306
|
+
validate_index_key(:range_key, @range_key)
|
309
307
|
end
|
310
308
|
|
311
|
-
def
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
309
|
+
def validate_index_key(key_param, key_val)
|
310
|
+
return if key_val.blank?
|
311
|
+
|
312
|
+
key_field_attributes = @dynamoid_class.attributes[key_val]
|
313
|
+
if key_field_attributes.blank?
|
314
|
+
errors.add(key_param, "No such field #{key_val} defined on table")
|
315
|
+
return
|
316
|
+
end
|
317
|
+
|
318
|
+
key_dynamodb_type = dynamodb_type(key_field_attributes[:type], key_field_attributes)
|
319
|
+
if PERMITTED_KEY_DYNAMODB_TYPES.include?(key_dynamodb_type)
|
320
|
+
send("#{key_param}_schema=", { key_val => key_dynamodb_type })
|
322
321
|
else
|
323
|
-
errors.add(
|
322
|
+
errors.add(key_param, "Index :#{key_param} is not a valid key type")
|
324
323
|
end
|
325
324
|
end
|
325
|
+
|
326
|
+
def dynamodb_type(field_type, options)
|
327
|
+
PrimaryKeyTypeMapping.dynamodb_type(field_type, options)
|
328
|
+
rescue Errors::UnsupportedKeyType
|
329
|
+
field_type
|
330
|
+
end
|
326
331
|
end
|
327
332
|
end
|
328
333
|
end
|
data/lib/dynamoid/loadable.rb
CHANGED
@@ -8,12 +8,14 @@ module Dynamoid
|
|
8
8
|
attrs.each do |key, value|
|
9
9
|
send("#{key}=", value) if respond_to?("#{key}=")
|
10
10
|
end
|
11
|
+
|
12
|
+
self
|
11
13
|
end
|
12
14
|
|
13
15
|
# Reload an object from the database -- if you suspect the object has changed in the data store and you need those
|
14
16
|
# changes to be reflected immediately, you would call this method. This is a consistent read.
|
15
17
|
#
|
16
|
-
# @return [Dynamoid::Document]
|
18
|
+
# @return [Dynamoid::Document] self
|
17
19
|
#
|
18
20
|
# @since 0.2.0
|
19
21
|
def reload
|
@@ -24,7 +26,10 @@ module Dynamoid
|
|
24
26
|
end
|
25
27
|
|
26
28
|
self.attributes = self.class.find(hash_key, **options).attributes
|
29
|
+
|
27
30
|
@associations.values.each(&:reset)
|
31
|
+
@new_record = false
|
32
|
+
|
28
33
|
self
|
29
34
|
end
|
30
35
|
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
|