dynamoid 3.7.1 → 3.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +246 -244
  3. data/LICENSE.txt +2 -2
  4. data/README.md +134 -55
  5. data/SECURITY.md +17 -0
  6. data/dynamoid.gemspec +66 -0
  7. data/lib/dynamoid/adapter.rb +7 -9
  8. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/batch_get_item.rb +2 -2
  9. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/execute_statement.rb +62 -0
  10. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +29 -15
  11. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/limit.rb +3 -0
  12. data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +73 -59
  13. data/lib/dynamoid/associations/single_association.rb +28 -9
  14. data/lib/dynamoid/components.rb +2 -3
  15. data/lib/dynamoid/criteria/chain.rb +13 -9
  16. data/lib/dynamoid/criteria.rb +6 -7
  17. data/lib/dynamoid/dirty.rb +60 -63
  18. data/lib/dynamoid/document.rb +42 -12
  19. data/lib/dynamoid/errors.rb +2 -0
  20. data/lib/dynamoid/fields.rb +19 -37
  21. data/lib/dynamoid/finders.rb +9 -4
  22. data/lib/dynamoid/indexes.rb +45 -40
  23. data/lib/dynamoid/loadable.rb +6 -1
  24. data/lib/dynamoid/log/formatter.rb +19 -4
  25. data/lib/dynamoid/persistence/import.rb +4 -1
  26. data/lib/dynamoid/persistence/inc.rb +66 -0
  27. data/lib/dynamoid/persistence/save.rb +52 -5
  28. data/lib/dynamoid/persistence/update_fields.rb +1 -1
  29. data/lib/dynamoid/persistence/update_validations.rb +1 -1
  30. data/lib/dynamoid/persistence/upsert.rb +1 -1
  31. data/lib/dynamoid/persistence.rb +149 -61
  32. data/lib/dynamoid/undumping.rb +18 -0
  33. data/lib/dynamoid/validations.rb +6 -0
  34. data/lib/dynamoid/version.rb +1 -1
  35. data/lib/dynamoid.rb +1 -0
  36. metadata +30 -50
@@ -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 aproximate value based on DynamoDB statistic. DynamoDB
81
- # updates it periodicaly so the value can be no accurate.
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 reletivly cheap operation and doesn't read all the items in 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].constantize : self
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
- block.call(self)
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 optionaly range key) are equal.
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
@@ -77,5 +77,7 @@ module Dynamoid
77
77
  class UnsupportedKeyType < Error; end
78
78
 
79
79
  class UnknownAttribute < Error; end
80
+
81
+ class SubclassNotFound < Error; end
80
82
  end
81
83
  end
@@ -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
- # Timestamp fields weren't declared in `included` hook because they
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
- # Timestamp fields were declared in `included` hook but they are
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.new("Attribute #{name} is not part of the model")
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.name if self.class.attributes[:type]
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.name)
377
+ send("#{type}=", self.class.sti_name)
396
378
  end
397
379
  end
398
380
 
@@ -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? { |pk, sk| sk.nil? }
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
- if method =~ /find/
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
@@ -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
- # global_secondary_indexes hash_key: :category
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
- # global_secondary_indexes hash_key: :category,
29
- # range_key: :created_at,
30
- # name: 'posts_category_created_at_index',
31
- # projected_attributes: :all,
32
- # read_capacity: 100,
33
- # write_capacity: 20
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
- # local_secondary_indexes hash_key: :author_id
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
- # local_secondary_indexes range_key: :created_at,
95
- # name: 'posts_created_at_index',
96
- # projected_attributes: :all
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
- if @range_key.present?
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 validate_hash_key
312
- hash_field_attributes = @dynamoid_class.attributes[@hash_key]
313
- if hash_field_attributes.present?
314
- hash_field_type = hash_field_attributes[:type]
315
- if Dynamoid::Fields::PERMITTED_KEY_TYPES.include?(hash_field_type)
316
- @hash_key_schema = {
317
- @hash_key => PrimaryKeyTypeMapping.dynamodb_type(hash_field_type, hash_field_attributes)
318
- }
319
- else
320
- errors.add(:hash_key, 'Index :hash_key is not a valid key type')
321
- end
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(:hash_key, "No such field #{@hash_key} defined on table")
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
@@ -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] the document this method was called on
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
@@ -24,7 +24,10 @@ module Dynamoid
24
24
  import_with_backoff(models)
25
25
  end
26
26
 
27
- models.each { |d| d.new_record = false }
27
+ models.each do |m|
28
+ m.new_record = false
29
+ m.clear_changes_information
30
+ end
28
31
  models
29
32
  end
30
33
 
@@ -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
- attributes_dumped = Dumping.dump_attributes(@model.attributes, @model.class.attributes)
24
- Dynamoid.adapter.write(@model.class.table_name, attributes_dumped, conditions_for_write)
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
@@ -19,7 +19,7 @@ module Dynamoid
19
19
  def call
20
20
  UpdateValidations.validate_attributes_exist(@model_class, @attributes)
21
21
 
22
- if Dynamoid::Config.timestamps
22
+ if @model_class.timestamps_enabled?
23
23
  @attributes[:updated_at] ||= DateTime.now.in_time_zone(Time.zone)
24
24
  end
25
25
 
@@ -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.new("Attribute #{attr_name} does not exist in #{model_class}")
12
+ raise Dynamoid::Errors::UnknownAttribute, "Attribute #{attr_name} does not exist in #{model_class}"
13
13
  end
14
14
  end
15
15
  end
@@ -19,7 +19,7 @@ module Dynamoid
19
19
  def call
20
20
  UpdateValidations.validate_attributes_exist(@model_class, @attributes)
21
21
 
22
- if Dynamoid::Config.timestamps
22
+ if @model_class.timestamps_enabled?
23
23
  @attributes[:updated_at] ||= DateTime.now.in_time_zone(Time.zone)
24
24
  end
25
25