dynamoid 3.7.1 → 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 +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
|