dynamoid 3.4.0 → 3.7.1

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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +94 -3
  3. data/README.md +52 -26
  4. data/lib/dynamoid.rb +1 -0
  5. data/lib/dynamoid/adapter.rb +15 -6
  6. data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +48 -36
  7. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/batch_get_item.rb +13 -1
  8. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb +9 -8
  9. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +5 -4
  10. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/backoff.rb +2 -2
  11. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/limit.rb +2 -3
  12. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/start_key.rb +2 -2
  13. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +4 -2
  14. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +6 -3
  15. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/table.rb +1 -0
  16. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/until_past_table_status.rb +2 -1
  17. data/lib/dynamoid/application_time_zone.rb +1 -0
  18. data/lib/dynamoid/associations.rb +182 -19
  19. data/lib/dynamoid/associations/association.rb +10 -2
  20. data/lib/dynamoid/associations/belongs_to.rb +2 -1
  21. data/lib/dynamoid/associations/has_and_belongs_to_many.rb +2 -1
  22. data/lib/dynamoid/associations/has_many.rb +2 -1
  23. data/lib/dynamoid/associations/has_one.rb +2 -1
  24. data/lib/dynamoid/associations/many_association.rb +68 -23
  25. data/lib/dynamoid/associations/single_association.rb +31 -4
  26. data/lib/dynamoid/components.rb +1 -0
  27. data/lib/dynamoid/config.rb +5 -5
  28. data/lib/dynamoid/config/backoff_strategies/constant_backoff.rb +1 -0
  29. data/lib/dynamoid/config/backoff_strategies/exponential_backoff.rb +1 -0
  30. data/lib/dynamoid/config/options.rb +1 -0
  31. data/lib/dynamoid/criteria.rb +9 -1
  32. data/lib/dynamoid/criteria/chain.rb +422 -46
  33. data/lib/dynamoid/criteria/ignored_conditions_detector.rb +3 -3
  34. data/lib/dynamoid/criteria/key_fields_detector.rb +32 -11
  35. data/lib/dynamoid/criteria/nonexistent_fields_detector.rb +3 -2
  36. data/lib/dynamoid/criteria/overwritten_conditions_detector.rb +1 -1
  37. data/lib/dynamoid/dirty.rb +119 -64
  38. data/lib/dynamoid/document.rb +125 -43
  39. data/lib/dynamoid/dumping.rb +9 -0
  40. data/lib/dynamoid/dynamodb_time_zone.rb +1 -0
  41. data/lib/dynamoid/errors.rb +2 -0
  42. data/lib/dynamoid/fields.rb +217 -36
  43. data/lib/dynamoid/fields/declare.rb +86 -0
  44. data/lib/dynamoid/finders.rb +69 -32
  45. data/lib/dynamoid/identity_map.rb +6 -0
  46. data/lib/dynamoid/indexes.rb +86 -17
  47. data/lib/dynamoid/loadable.rb +2 -2
  48. data/lib/dynamoid/log/formatter.rb +26 -0
  49. data/lib/dynamoid/middleware/identity_map.rb +1 -0
  50. data/lib/dynamoid/persistence.rb +496 -104
  51. data/lib/dynamoid/persistence/import.rb +1 -0
  52. data/lib/dynamoid/persistence/save.rb +1 -0
  53. data/lib/dynamoid/persistence/update_fields.rb +5 -2
  54. data/lib/dynamoid/persistence/update_validations.rb +18 -0
  55. data/lib/dynamoid/persistence/upsert.rb +5 -3
  56. data/lib/dynamoid/primary_key_type_mapping.rb +1 -0
  57. data/lib/dynamoid/railtie.rb +1 -0
  58. data/lib/dynamoid/tasks.rb +3 -1
  59. data/lib/dynamoid/tasks/database.rb +1 -0
  60. data/lib/dynamoid/type_casting.rb +12 -2
  61. data/lib/dynamoid/undumping.rb +8 -0
  62. data/lib/dynamoid/validations.rb +6 -1
  63. data/lib/dynamoid/version.rb +1 -1
  64. metadata +48 -74
  65. data/.coveralls.yml +0 -1
  66. data/.document +0 -5
  67. data/.gitignore +0 -74
  68. data/.rspec +0 -2
  69. data/.rubocop.yml +0 -71
  70. data/.rubocop_todo.yml +0 -55
  71. data/.travis.yml +0 -44
  72. data/Appraisals +0 -22
  73. data/Gemfile +0 -8
  74. data/Rakefile +0 -46
  75. data/Vagrantfile +0 -29
  76. data/docker-compose.yml +0 -7
  77. data/dynamoid.gemspec +0 -57
  78. data/gemfiles/rails_4_2.gemfile +0 -9
  79. data/gemfiles/rails_5_0.gemfile +0 -8
  80. data/gemfiles/rails_5_1.gemfile +0 -8
  81. data/gemfiles/rails_5_2.gemfile +0 -8
  82. data/gemfiles/rails_6_0.gemfile +0 -8
@@ -10,7 +10,7 @@ module Dynamoid
10
10
  end
11
11
  end
12
12
 
13
- # Reload an object from the database -- if you suspect the object has changed in the datastore and you need those
13
+ # Reload an object from the database -- if you suspect the object has changed in the data store and you need those
14
14
  # changes to be reflected immediately, you would call this method. This is a consistent read.
15
15
  #
16
16
  # @return [Dynamoid::Document] the document this method was called on
@@ -23,7 +23,7 @@ module Dynamoid
23
23
  options[:range_key] = range_value
24
24
  end
25
25
 
26
- self.attributes = self.class.find(hash_key, options).attributes
26
+ self.attributes = self.class.find(hash_key, **options).attributes
27
27
  @associations.values.each(&:reset)
28
28
  self
29
29
  end
@@ -0,0 +1,26 @@
1
+ module Dynamoid
2
+ module Log
3
+ 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
+ class Debug
9
+ def format(response)
10
+ bold = "\x1b[1m"
11
+ color = "\x1b[34m"
12
+ reset = "\x1b[0m"
13
+
14
+ [
15
+ response.context.operation.name,
16
+ "#{bold}#{color}\nRequest:\n#{reset}#{bold}",
17
+ JSON.pretty_generate(JSON.parse(response.context.http_request.body.string)),
18
+ "#{bold}#{color}\nResponse:\n#{reset}#{bold}",
19
+ JSON.pretty_generate(JSON.parse(response.context.http_response.body.string)),
20
+ reset
21
+ ].join("\n")
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dynamoid
4
+ # @private
4
5
  module Middleware
5
6
  class IdentityMap
6
7
  def initialize(app)
@@ -8,10 +8,11 @@ require 'dynamoid/persistence/import'
8
8
  require 'dynamoid/persistence/update_fields'
9
9
  require 'dynamoid/persistence/upsert'
10
10
  require 'dynamoid/persistence/save'
11
+ require 'dynamoid/persistence/update_validations'
11
12
 
12
13
  # encoding: utf-8
13
14
  module Dynamoid
14
- # Persistence is responsible for dumping objects to and marshalling objects from the datastore. It tries to reserialize
15
+ # Persistence is responsible for dumping objects to and marshalling objects from the data store. It tries to reserialize
15
16
  # values to be of the same type as when they were passed in, based on the fields in the class.
16
17
  module Persistence
17
18
  extend ActiveSupport::Concern
@@ -19,6 +20,7 @@ module Dynamoid
19
20
  attr_accessor :new_record
20
21
  alias new_record? new_record
21
22
 
23
+ # @private
22
24
  UNIX_EPOCH_DATE = Date.new(1970, 1, 1).freeze
23
25
 
24
26
  module ClassMethods
@@ -30,13 +32,64 @@ module Dynamoid
30
32
 
31
33
  # Create a table.
32
34
  #
33
- # @param [Hash] options options to pass for table creation
34
- # @option options [Symbol] :id the id field for the table
35
- # @option options [Symbol] :table_name the actual name for the table
36
- # @option options [Integer] :read_capacity set the read capacity for the table; does not work on existing tables
37
- # @option options [Integer] :write_capacity set the write capacity for the table; does not work on existing tables
38
- # @option options [Hash] {range_key => :type} a hash of the name of the range key and a symbol of its type
39
- # @option options [Symbol] :hash_key_type the dynamo type of the hash key (:string or :number)
35
+ # Uses a configuration specified in a model class (with the +table+
36
+ # method) e.g. table name, schema (hash and range keys), global and local
37
+ # secondary indexes, billing mode and write/read capacity.
38
+ #
39
+ # For instance here
40
+ #
41
+ # class User
42
+ # include Dynamoid::Document
43
+ #
44
+ # table key: :uuid
45
+ # range :last_name
46
+ #
47
+ # field :first_name
48
+ # field :last_name
49
+ # end
50
+ #
51
+ # User.create_table
52
+ #
53
+ # +create_table+ method call will create a table +dynamoid_users+ with
54
+ # hash key +uuid+ and range key +name+, DynamoDB default billing mode and
55
+ # Dynamoid default read/write capacity units (100/20).
56
+ #
57
+ # All the configuration can be overridden with +options+ argument.
58
+ #
59
+ # User.create_table(table_name: 'users', read_capacity: 200, write_capacity: 40)
60
+ #
61
+ # Dynamoid creates a table synchronously by default. DynamoDB table
62
+ # creation is an asynchronous operation and a client should wait until a
63
+ # table status changes to +ACTIVE+ and a table becomes available. That's
64
+ # why Dynamoid is polling a table status and returns results only when a
65
+ # table becomes available.
66
+ #
67
+ # Polling is configured with +Dynamoid::Config.sync_retry_max_times+ and
68
+ # +Dynamoid::Config.sync_retry_wait_seconds+ configuration options. If
69
+ # table creation takes more time than configured waiting time then
70
+ # Dynamoid stops polling and returns +true+.
71
+ #
72
+ # In order to return back asynchronous behaviour and not to wait until a
73
+ # table is created the +sync: false+ option should be specified.
74
+ #
75
+ # User.create_table(sync: false)
76
+ #
77
+ # Subsequent method calls for the same table will be ignored.
78
+ #
79
+ # @param options [Hash]
80
+ #
81
+ # @option options [Symbol] :table_name name of the table
82
+ # @option options [Symbol] :id hash key name of the table
83
+ # @option options [Symbol] :hash_key_type Dynamoid type of the hash key - +:string+, +:integer+ or any other scalar type
84
+ # @option options [Hash] :range_key a Hash with range key name and type in format +{ <name> => <type> }+ e.g. +{ last_name: :string }+
85
+ # @option options [String] :billing_mode billing mode of a table - either +PROVISIONED+ (default) or +PAY_PER_REQUEST+ (for On-Demand Mode)
86
+ # @option options [Integer] :read_capacity read capacity units for the table; does not work on existing tables and is ignored when billing mode is +PAY_PER_REQUEST+
87
+ # @option options [Integer] :write_capacity write capacity units for the table; does not work on existing tables and is ignored when billing mode is +PAY_PER_REQUEST+
88
+ # @option options [Hash] :local_secondary_indexes
89
+ # @option options [Hash] :global_secondary_indexes
90
+ # @option options [true|false] :sync specifies should the method call be synchronous and wait until a table is completely created
91
+ #
92
+ # @return [true|false] Whether a table created successfully
40
93
  # @since 0.4.0
41
94
  def create_table(options = {})
42
95
  range_key_hash = if range_key
@@ -59,15 +112,21 @@ module Dynamoid
59
112
 
60
113
  if created_successfuly && self.options[:expires]
61
114
  attribute = self.options[:expires][:field]
62
- Dynamoid.adapter.update_time_to_live(table_name: table_name, attribute: attribute)
115
+ Dynamoid.adapter.update_time_to_live(table_name, attribute)
63
116
  end
64
117
  end
65
118
 
66
- # Deletes the table for the model
119
+ # Deletes the table for the model.
120
+ #
121
+ # Dynamoid deletes a table asynchronously and doesn't wait until a table
122
+ # is deleted completely.
123
+ #
124
+ # Subsequent method calls for the same table will be ignored.
67
125
  def delete_table
68
126
  Dynamoid.adapter.delete_table(table_name)
69
127
  end
70
128
 
129
+ # @private
71
130
  def from_database(attrs = {})
72
131
  klass = choose_right_class(attrs)
73
132
  attrs_undumped = Undumping.undump_attributes(attrs, klass.attributes)
@@ -76,95 +135,156 @@ module Dynamoid
76
135
 
77
136
  # Create several models at once.
78
137
  #
79
- # Neither callbacks nor validations run.
80
- # It works efficiently because of using `BatchWriteItem` API call.
81
- # Return array of models.
82
- # Uses backoff specified by `Dynamoid::Config.backoff` config option
138
+ # users = User.import([{ name: 'a' }, { name: 'b' }])
83
139
  #
84
- # @param [Array<Hash>] array_of_attributes
140
+ # +import+ is a relatively low-level method and bypasses some
141
+ # mechanisms like callbacks and validation.
85
142
  #
86
- # @example
87
- # User.import([{ name: 'a' }, { name: 'b' }])
143
+ # It sets timestamp fields +created_at+ and +updated_at+ if they are
144
+ # blank. It sets a hash key field as well if it's blank. It expects that
145
+ # the hash key field is +string+ and sets a random UUID value if the field
146
+ # value is blank. All the field values are type casted to the declared
147
+ # types.
148
+ #
149
+ # It works efficiently and uses the `BatchWriteItem` operation. In order
150
+ # to cope with throttling it uses a backoff strategy if it's specified with
151
+ # `Dynamoid::Config.backoff` configuration option.
152
+ #
153
+ # Because of the nature of DynamoDB and its limits only 25 models can be
154
+ # saved at once. So multiple HTTP requests can be sent to DynamoDB.
155
+ #
156
+ # @param array_of_attributes [Array<Hash>]
157
+ # @return [Array] Created models
88
158
  def import(array_of_attributes)
89
159
  Import.call(self, array_of_attributes)
90
160
  end
91
161
 
92
162
  # Create a model.
93
163
  #
94
- # Initializes a new object and immediately saves it to the database.
95
- # Validates model and runs callbacks: before_create, before_save, after_save and after_create.
164
+ # Initializes a new model and immediately saves it to DynamoDB.
165
+ #
166
+ # User.create(first_name: 'Mark', last_name: 'Tyler')
167
+ #
96
168
  # Accepts both Hash and Array of Hashes and can create several models.
97
169
  #
98
- # @param [Hash|Array[Hash]] attrs Attributes with which to create the object.
170
+ # User.create([{ first_name: 'Alice' }, { first_name: 'Bob' }])
99
171
  #
100
- # @return [Dynamoid::Document] the saved document
172
+ # Creates a model and pass it into a block to set other attributes.
101
173
  #
174
+ # User.create(first_name: 'Mark') do |u|
175
+ # u.age = 21
176
+ # end
177
+ #
178
+ # Validates model and runs callbacks.
179
+ #
180
+ # @param attrs [Hash|Array[Hash]] Attributes of the models
181
+ # @param block [Proc] Block to process a document after initialization
182
+ # @return [Dynamoid::Document] The created document
102
183
  # @since 0.2.0
103
- def create(attrs = {})
184
+ def create(attrs = {}, &block)
104
185
  if attrs.is_a?(Array)
105
- attrs.map { |attr| create(attr) }
186
+ attrs.map { |attr| create(attr, &block) }
106
187
  else
107
- build(attrs).tap(&:save)
188
+ build(attrs, &block).tap(&:save)
108
189
  end
109
190
  end
110
191
 
111
- # Create new model.
112
- #
113
- # Initializes a new object and immediately saves it to the database.
114
- # Raises an exception if validation failed.
115
- # Accepts both Hash and Array of Hashes and can create several models.
116
- #
117
- # @param [Hash|Array[Hash]] attrs Attributes with which to create the object.
192
+ # Create a model.
118
193
  #
119
- # @return [Dynamoid::Document] the saved document
194
+ # Initializes a new object and immediately saves it to the Dynamoid.
195
+ # Raises an exception +Dynamoid::Errors::DocumentNotValid+ if validation
196
+ # failed. Accepts both Hash and Array of Hashes and can create several
197
+ # models.
120
198
  #
199
+ # @param attrs [Hash|Array[Hash]] Attributes with which to create the object.
200
+ # @param block [Proc] Block to process a document after initialization
201
+ # @return [Dynamoid::Document] The created document
121
202
  # @since 0.2.0
122
- def create!(attrs = {})
203
+ def create!(attrs = {}, &block)
123
204
  if attrs.is_a?(Array)
124
- attrs.map { |attr| create!(attr) }
205
+ attrs.map { |attr| create!(attr, &block) }
125
206
  else
126
- build(attrs).tap(&:save!)
207
+ build(attrs, &block).tap(&:save!)
127
208
  end
128
209
  end
129
210
 
130
211
  # Update document with provided attributes.
131
212
  #
132
- # Instantiates document and saves changes.
133
- # Runs validations and callbacks.
213
+ # Instantiates document and saves changes. Runs validations and
214
+ # callbacks. Don't save changes if validation fails.
134
215
  #
135
- # @param [Scalar value] partition key
136
- # @param [Scalar value] sort key, optional
137
- # @param [Hash] attributes
216
+ # User.update('1', age: 26)
138
217
  #
139
- # @return [Dynamoid::Doument] updated document
218
+ # If range key is declared for a model it should be passed as well:
140
219
  #
141
- # @example Update document
142
- # Post.update(101, title: 'New title')
220
+ # User.update('1', 'Tylor', age: 26)
221
+ #
222
+ # @param hash_key [Scalar value] hash key
223
+ # @param range_key_value [Scalar value] range key (optional)
224
+ # @param attrs [Hash]
225
+ # @return [Dynamoid::Document] Updated document
143
226
  def update(hash_key, range_key_value = nil, attrs)
144
227
  model = find(hash_key, range_key: range_key_value, consistent_read: true)
145
228
  model.update_attributes(attrs)
146
229
  model
147
230
  end
148
231
 
232
+ # Update document with provided attributes.
233
+ #
234
+ # Instantiates document and saves changes. Runs validations and
235
+ # callbacks.
236
+ #
237
+ # User.update!('1', age: 26)
238
+ #
239
+ # If range key is declared for a model it should be passed as well:
240
+ #
241
+ # User.update('1', 'Tylor', age: 26)
242
+ #
243
+ # Raises +Dynamoid::Errors::DocumentNotValid+ exception if validation fails.
244
+ #
245
+ # @param hash_key [Scalar value] hash key
246
+ # @param range_key_value [Scalar value] range key (optional)
247
+ # @param attrs [Hash]
248
+ # @return [Dynamoid::Document] Updated document
249
+ def update!(hash_key, range_key_value = nil, attrs)
250
+ model = find(hash_key, range_key: range_key_value, consistent_read: true)
251
+ model.update_attributes!(attrs)
252
+ model
253
+ end
254
+
149
255
  # Update document.
150
256
  #
151
- # Uses efficient low-level `UpdateItem` API call.
152
- # Changes attibutes and loads new document version with one API call.
153
- # Doesn't run validations and callbacks. Can make conditional update.
154
- # If a document doesn't exist or specified conditions failed - returns `nil`
257
+ # Doesn't run validations and callbacks.
258
+ #
259
+ # User.update_fields('1', age: 26)
260
+ #
261
+ # If range key is declared for a model it should be passed as well:
262
+ #
263
+ # User.update_fields('1', 'Tylor', age: 26)
155
264
  #
156
- # @param [Scalar value] partition key
157
- # @param [Scalar value] sort key (optional)
158
- # @param [Hash] attributes
159
- # @param [Hash] conditions
265
+ # Can make a conditional update so a document will be updated only if it
266
+ # meets the specified conditions. Conditions can be specified as a +Hash+
267
+ # with +:if+ key:
160
268
  #
161
- # @return [Dynamoid::Document|nil] updated document
269
+ # User.update_fields('1', { age: 26 }, if: { version: 1 })
162
270
  #
163
- # @example Update document
164
- # Post.update_fields(101, read: true)
271
+ # Here +User+ model has an integer +version+ field and the document will
272
+ # be updated only if the +version+ attribute currently has value 1.
165
273
  #
166
- # @example Update document with condition
167
- # Post.update_fields(101, { title: 'New title' }, if: { version: 1 })
274
+ # If a document with specified hash and range keys doesn't exist or
275
+ # conditions were specified and failed the method call returns +nil+.
276
+ #
277
+ # +update_fields+ uses the +UpdateItem+ operation so it saves changes and
278
+ # loads an updated document back with one HTTP request.
279
+ #
280
+ # Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
281
+ # attributes is not on the model
282
+ #
283
+ # @param hash_key_value [Scalar value] hash key
284
+ # @param range_key_value [Scalar value] range key (optional)
285
+ # @param attrs [Hash]
286
+ # @param conditions [Hash] (optional)
287
+ # @return [Dynamoid::Document|nil] Updated document
168
288
  def update_fields(hash_key_value, range_key_value = nil, attrs = {}, conditions = {})
169
289
  optional_params = [range_key_value, attrs, conditions].compact
170
290
  if optional_params.first.is_a?(Hash)
@@ -182,25 +302,40 @@ module Dynamoid
182
302
  conditions: conditions)
183
303
  end
184
304
 
185
- # Update existing document or create new one.
305
+ # Update an existing document or create a new one.
306
+ #
307
+ # If a document with specified hash and range keys doesn't exist it
308
+ # creates a new document with specified attributes. Doesn't run
309
+ # validations and callbacks.
310
+ #
311
+ # User.upsert('1', age: 26)
312
+ #
313
+ # If range key is declared for a model it should be passed as well:
314
+ #
315
+ # User.upsert('1', 'Tylor', age: 26)
316
+ #
317
+ # Can make a conditional update so a document will be updated only if it
318
+ # meets the specified conditions. Conditions can be specified as a +Hash+
319
+ # with +:if+ key:
186
320
  #
187
- # Similar to `.update_fields`.
188
- # The only diffirence is - it creates new document in case the document doesn't exist.
321
+ # User.upsert('1', { age: 26 }, if: { version: 1 })
189
322
  #
190
- # Uses efficient low-level `UpdateItem` API call.
191
- # Changes attibutes and loads new document version with one API call.
192
- # Doesn't run validations and callbacks. Can make conditional update.
193
- # If specified conditions failed - returns `nil`.
323
+ # Here +User+ model has an integer +version+ field and the document will
324
+ # be updated only if the +version+ attribute currently has value 1.
194
325
  #
195
- # @param [Scalar value] partition key
196
- # @param [Scalar value] sort key (optional)
197
- # @param [Hash] attributes
198
- # @param [Hash] conditions
326
+ # If conditions were specified and failed the method call returns +nil+.
199
327
  #
200
- # @return [Dynamoid::Document/nil] updated document
328
+ # +upsert+ uses the +UpdateItem+ operation so it saves changes and loads
329
+ # an updated document back with one HTTP request.
201
330
  #
202
- # @example Update document
203
- # Post.upsert(101, title: 'New title')
331
+ # Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
332
+ # attributes is not on the model
333
+ #
334
+ # @param hash_key_value [Scalar value] hash key
335
+ # @param range_key_value [Scalar value] range key (optional)
336
+ # @param attrs [Hash]
337
+ # @param conditions [Hash] (optional)
338
+ # @return [Dynamoid::Document|nil] Updated document
204
339
  def upsert(hash_key_value, range_key_value = nil, attrs = {}, conditions = {})
205
340
  optional_params = [range_key_value, attrs, conditions].compact
206
341
  if optional_params.first.is_a?(Hash)
@@ -218,19 +353,27 @@ module Dynamoid
218
353
  conditions: conditions)
219
354
  end
220
355
 
221
- # Increase numeric field by specified value.
356
+ # Increase a numeric field by specified value.
357
+ #
358
+ # User.inc('1', age: 2)
222
359
  #
223
360
  # Can update several fields at once.
224
- # Uses efficient low-level `UpdateItem` API call.
225
361
  #
226
- # @param [Scalar value] hash_key_value partition key
227
- # @param [Scalar value] range_key_value sort key (optional)
228
- # @param [Hash] counters value to increase by
362
+ # User.inc('1', age: 2, version: 1)
363
+ #
364
+ # If range key is declared for a model it should be passed as well:
365
+ #
366
+ # User.inc('1', 'Tylor', age: 2)
367
+ #
368
+ # Uses efficient low-level +UpdateItem+ operation and does only one HTTP
369
+ # request.
229
370
  #
230
- # @return [Dynamoid::Document/nil] updated document
371
+ # Doesn't run validations and callbacks. Doesn't update +created_at+ and
372
+ # +updated_at+ as well.
231
373
  #
232
- # @example Update document
233
- # Post.inc(101, views_counter: 2, downloads: 10)
374
+ # @param hash_key_value [Scalar value] hash key
375
+ # @param range_key_value [Scalar value] range key (optional)
376
+ # @param counters [Hash] value to increase by
234
377
  def inc(hash_key_value, range_key_value = nil, counters)
235
378
  options = if range_key
236
379
  value_casted = TypeCasting.cast_field(range_key_value, attributes[range_key])
@@ -251,8 +394,17 @@ module Dynamoid
251
394
  end
252
395
  end
253
396
 
254
- # Set updated_at and any passed in field to current DateTime. Useful for things like last_login_at, etc.
397
+ # Update document timestamps.
398
+ #
399
+ # Set +updated_at+ attribute to current DateTime.
400
+ #
401
+ # post.touch
402
+ #
403
+ # Can update another field in addition with the same timestamp if it's name passed as argument.
404
+ #
405
+ # user.touch(:last_login_at)
255
406
  #
407
+ # @param name [Symbol] attribute name to update (optional)
256
408
  def touch(name = nil)
257
409
  now = DateTime.now
258
410
  self.updated_at = now
@@ -260,18 +412,72 @@ module Dynamoid
260
412
  save
261
413
  end
262
414
 
263
- # Is this object persisted in the datastore? Required for some ActiveModel integration stuff.
415
+ # Is this object persisted in DynamoDB?
264
416
  #
417
+ # user = User.new
418
+ # user.persisted? # => false
419
+ #
420
+ # user.save
421
+ # user.persisted? # => true
422
+ #
423
+ # @return [true|false]
265
424
  # @since 0.2.0
266
425
  def persisted?
267
- !new_record?
426
+ !(new_record? || @destroyed)
268
427
  end
269
428
 
270
- # Run the callbacks and then persist this object in the datastore.
429
+ # Create new model or persist changes.
430
+ #
431
+ # Run the validation and callbacks. Returns +true+ if saving is successful
432
+ # and +false+ otherwise.
433
+ #
434
+ # user = User.new
435
+ # user.save # => true
436
+ #
437
+ # user.age = 26
438
+ # user.save # => true
439
+ #
440
+ # Validation can be skipped with +validate: false+ option:
441
+ #
442
+ # user = User.new(age: -1)
443
+ # user.save(validate: false) # => true
444
+ #
445
+ # +save+ by default sets timestamps attributes - +created_at+ and
446
+ # +updated_at+ when creates new model and updates +updated_at+ attribute
447
+ # when update already existing one.
271
448
  #
449
+ # Changing +updated_at+ attribute at updating a model can be skipped with
450
+ # +touch: false+ option:
451
+ #
452
+ # user.save(touch: false)
453
+ #
454
+ # If a model is new and hash key (+id+ by default) is not assigned yet
455
+ # it was assigned implicitly with random UUID value.
456
+ #
457
+ # If +lock_version+ attribute is declared it will be incremented. If it's blank then it will be initialized with 1.
458
+ #
459
+ # +save+ method call raises +Dynamoid::Errors::RecordNotUnique+ exception
460
+ # if primary key (hash key + optional range key) already exists in a
461
+ # table.
462
+ #
463
+ # +save+ method call raises +Dynamoid::Errors::StaleObjectError+ exception
464
+ # if there is +lock_version+ attribute and the document in a table was
465
+ # already changed concurrently and +lock_version+ was consequently
466
+ # increased.
467
+ #
468
+ # When a table is not created yet the first +save+ method call will create
469
+ # a table. It's useful in test environment to avoid explicit table
470
+ # creation.
471
+ #
472
+ # @param options [Hash] (optional)
473
+ # @option options [true|false] :validate validate a model or not - +true+ by default (optional)
474
+ # @option options [true|false] :touch update tiemstamps fields or not - +true+ by default (optional)
475
+ # @return [true|false] Whether saving successful or not
272
476
  # @since 0.2.0
273
- def save(_options = {})
274
- self.class.create_table
477
+ def save(options = {})
478
+ self.class.create_table(sync: true)
479
+
480
+ @_touch_record = options[:touch]
275
481
 
276
482
  if new_record?
277
483
  run_callbacks(:create) do
@@ -286,20 +492,35 @@ module Dynamoid
286
492
  end
287
493
  end
288
494
 
289
- # Updates multiple attributes at once, saving the object once the updates are complete.
495
+ # Update multiple attributes at once, saving the object once the updates
496
+ # are complete. Returns +true+ if saving is successful and +false+
497
+ # otherwise.
498
+ #
499
+ # user.update_attributes(age: 27, last_name: 'Tylor')
290
500
  #
291
- # @param [Hash] attributes a hash of attributes to update
501
+ # Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
502
+ # attributes is not on the model
292
503
  #
504
+ # @param attributes [Hash] a hash of attributes to update
505
+ # @return [true|false] Whether updating successful or not
293
506
  # @since 0.2.0
294
507
  def update_attributes(attributes)
295
508
  attributes.each { |attribute, value| write_attribute(attribute, value) }
296
509
  save
297
510
  end
298
511
 
299
- # Updates multiple attributes at once, saving the object once the updates are complete.
300
- # Raises a Dynamoid::Errors::DocumentNotValid exception if there is vaidation and it fails.
512
+ # Update multiple attributes at once, saving the object once the updates
513
+ # are complete.
301
514
  #
302
- # @param [Hash] attributes a hash of attributes to update
515
+ # user.update_attributes!(age: 27, last_name: 'Tylor')
516
+ #
517
+ # Raises a +Dynamoid::Errors::DocumentNotValid+ exception if some vaidation
518
+ # fails.
519
+ #
520
+ # Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
521
+ # attributes is not on the model
522
+ #
523
+ # @param attributes [Hash] a hash of attributes to update
303
524
  def update_attributes!(attributes)
304
525
  attributes.each { |attribute, value| write_attribute(attribute, value) }
305
526
  save!
@@ -307,20 +528,71 @@ module Dynamoid
307
528
 
308
529
  # Update a single attribute, saving the object afterwards.
309
530
  #
310
- # @param [Symbol] attribute the attribute to update
311
- # @param [Object] value the value to assign it
531
+ # Returns +true+ if saving is successful and +false+ otherwise.
532
+ #
533
+ # user.update_attribute(:last_name, 'Tylor')
312
534
  #
535
+ # Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
536
+ # attributes is not on the model
537
+ #
538
+ # @param attribute [Symbol] attribute name to update
539
+ # @param value [Object] the value to assign it
540
+ # @return [Dynamoid::Document] self
313
541
  # @since 0.2.0
314
542
  def update_attribute(attribute, value)
315
543
  write_attribute(attribute, value)
316
544
  save
317
545
  end
318
546
 
547
+ # Update a model.
548
+ #
549
+ # Runs validation and callbacks. Reloads all attribute values.
550
+ #
551
+ # Accepts mandatory block in order to specify operations which will modify
552
+ # attributes. Supports following operations: +add+, +delete+ and +set+.
553
+ #
554
+ # Operation +add+ just adds a value for numeric attributes and join
555
+ # collections if attribute is a collection (one of +array+, +set+ or
556
+ # +map+).
319
557
  #
320
- # update!() will increment the lock_version if the table has the column, but will not check it. Thus, a concurrent save will
321
- # never cause an update! to fail, but an update! may cause a concurrent save to fail.
558
+ # user.update do |t|
559
+ # t.add(age: 1, followers_count: 5)
560
+ # t.add(hobbies: ['skying', 'climbing'])
561
+ # end
322
562
  #
563
+ # Operation +delete+ is applied to collection attribute types and
564
+ # substructs one collection from another.
323
565
  #
566
+ # user.update do |t|
567
+ # t.delete(hobbies: ['skying'])
568
+ # end
569
+ #
570
+ # Operation +set+ just changes an attribute value:
571
+ #
572
+ # user.update do |t|
573
+ # t.set(age: 21)
574
+ # end
575
+ #
576
+ # All the operations works like +ADD+, +DELETE+ and +PUT+ actions supported
577
+ # by +AttributeUpdates+
578
+ # {parameter}[https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/LegacyConditionalParameters.AttributeUpdates.html]
579
+ # of +UpdateItem+ operation.
580
+ #
581
+ # Can update a model conditionaly:
582
+ #
583
+ # user.update(if: { age: 20 }) do |t|
584
+ # t.add(age: 1)
585
+ # end
586
+ #
587
+ # If a document doesn't meet conditions it raises
588
+ # +Dynamoid::Errors::StaleObjectError+ exception.
589
+ #
590
+ # It will increment the +lock_version+ attribute if a table has the column,
591
+ # but will not check it. Thus, a concurrent +save+ call will never cause an
592
+ # +update!+ to fail, but an +update!+ may cause a concurrent +save+ to
593
+ # fail.
594
+ #
595
+ # @param conditions [Hash] Conditions on model attributes to make a conditional update (optional)
324
596
  def update!(conditions = {})
325
597
  run_callbacks(:update) do
326
598
  options = range_key ? { range_key: Dumping.dump_field(read_attribute(range_key), self.class.attributes[range_key]) } : {}
@@ -342,9 +614,56 @@ module Dynamoid
342
614
  raise Dynamoid::Errors::StaleObjectError.new(self, 'update')
343
615
  end
344
616
  end
345
-
346
617
  end
347
618
 
619
+ # Update a model.
620
+ #
621
+ # Runs validation and callbacks. Reloads all attribute values.
622
+ #
623
+ # Accepts mandatory block in order to specify operations which will modify
624
+ # attributes. Supports following operations: +add+, +delete+ and +set+.
625
+ #
626
+ # Operation +add+ just adds a value for numeric attributes and join
627
+ # collections if attribute is a collection (one of +array+, +set+ or
628
+ # +map+).
629
+ #
630
+ # user.update do |t|
631
+ # t.add(age: 1, followers_count: 5)
632
+ # t.add(hobbies: ['skying', 'climbing'])
633
+ # end
634
+ #
635
+ # Operation +delete+ is applied to collection attribute types and
636
+ # substructs one collection from another.
637
+ #
638
+ # user.update do |t|
639
+ # t.delete(hobbies: ['skying'])
640
+ # end
641
+ #
642
+ # Operation +set+ just changes an attribute value:
643
+ #
644
+ # user.update do |t|
645
+ # t.set(age: 21)
646
+ # end
647
+ #
648
+ # All the operations works like +ADD+, +DELETE+ and +PUT+ actions supported
649
+ # by +AttributeUpdates+
650
+ # {parameter}[https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/LegacyConditionalParameters.AttributeUpdates.html]
651
+ # of +UpdateItem+ operation.
652
+ #
653
+ # Can update a model conditionaly:
654
+ #
655
+ # user.update(if: { age: 20 }) do |t|
656
+ # t.add(age: 1)
657
+ # end
658
+ #
659
+ # If a document doesn't meet conditions it just returns +false+. Otherwise it returns +true+.
660
+ #
661
+ # It will increment the +lock_version+ attribute if a table has the column,
662
+ # but will not check it. Thus, a concurrent +save+ call will never cause an
663
+ # +update!+ to fail, but an +update!+ may cause a concurrent +save+ to
664
+ # fail.
665
+ #
666
+ # @param conditions [Hash] Conditions on model attributes to make a conditional update (optional)
348
667
  def update(conditions = {}, &block)
349
668
  update!(conditions, &block)
350
669
  true
@@ -352,51 +671,117 @@ module Dynamoid
352
671
  false
353
672
  end
354
673
 
355
- # Initializes attribute to zero if nil and adds the value passed as by (default is 1).
356
- # Only makes sense for number-based attributes. Returns self.
674
+ # Change numeric attribute value.
675
+ #
676
+ # Initializes attribute to zero if +nil+ and adds the specified value (by
677
+ # default is 1). Only makes sense for number-based attributes.
678
+ #
679
+ # user.increment(:followers_count)
680
+ # user.increment(:followers_count, 2)
681
+ #
682
+ # @param attribute [Symbol] attribute name
683
+ # @param by [Numeric] value to add (optional)
684
+ # @return [Dynamoid::Document] self
357
685
  def increment(attribute, by = 1)
358
686
  self[attribute] ||= 0
359
687
  self[attribute] += by
360
688
  self
361
689
  end
362
690
 
363
- # Wrapper around increment that saves the record.
364
- # Returns true if the record could be saved.
691
+ # Change numeric attribute value and save a model.
692
+ #
693
+ # Initializes attribute to zero if +nil+ and adds the specified value (by
694
+ # default is 1). Only makes sense for number-based attributes.
695
+ #
696
+ # user.increment!(:followers_count)
697
+ # user.increment!(:followers_count, 2)
698
+ #
699
+ # Returns +true+ if a model was saved and +false+ otherwise.
700
+ #
701
+ # @param attribute [Symbol] attribute name
702
+ # @param by [Numeric] value to add (optional)
703
+ # @return [true|false] whether saved model successfully
365
704
  def increment!(attribute, by = 1)
366
705
  increment(attribute, by)
367
706
  save
368
707
  end
369
708
 
370
- # Initializes attribute to zero if nil and subtracts the value passed as by (default is 1).
371
- # Only makes sense for number-based attributes. Returns self.
709
+ # Change numeric attribute value.
710
+ #
711
+ # Initializes attribute to zero if +nil+ and subtracts the specified value
712
+ # (by default is 1). Only makes sense for number-based attributes.
713
+ #
714
+ # user.decrement(:followers_count)
715
+ # user.decrement(:followers_count, 2)
716
+ #
717
+ # @param attribute [Symbol] attribute name
718
+ # @param by [Numeric] value to subtract (optional)
719
+ # @return [Dynamoid::Document] self
372
720
  def decrement(attribute, by = 1)
373
721
  self[attribute] ||= 0
374
722
  self[attribute] -= by
375
723
  self
376
724
  end
377
725
 
378
- # Wrapper around decrement that saves the record.
379
- # Returns true if the record could be saved.
726
+ # Change numeric attribute value and save a model.
727
+ #
728
+ # Initializes attribute to zero if +nil+ and subtracts the specified value
729
+ # (by default is 1). Only makes sense for number-based attributes.
730
+ #
731
+ # user.decrement!(:followers_count)
732
+ # user.decrement!(:followers_count, 2)
733
+ #
734
+ # Returns +true+ if a model was saved and +false+ otherwise.
735
+ #
736
+ # @param attribute [Symbol] attribute name
737
+ # @param by [Numeric] value to subtract (optional)
738
+ # @return [true|false] whether saved model successfully
380
739
  def decrement!(attribute, by = 1)
381
740
  decrement(attribute, by)
382
741
  save
383
742
  end
384
743
 
385
- # Delete this object, but only after running callbacks for it.
744
+ # Delete a model.
745
+ #
746
+ # Runs callbacks.
747
+ #
748
+ # Supports optimistic locking with the +lock_version+ attribute and doesn't
749
+ # delete a model if it's already changed.
386
750
  #
751
+ # Returns +true+ if deleted successfully and +false+ otherwise.
752
+ #
753
+ # @return [true|false] whether deleted successfully
387
754
  # @since 0.2.0
388
755
  def destroy
389
756
  ret = run_callbacks(:destroy) do
390
757
  delete
391
758
  end
759
+
760
+ @destroyed = true
761
+
392
762
  ret == false ? false : self
393
763
  end
394
764
 
765
+ # Delete a model.
766
+ #
767
+ # Runs callbacks.
768
+ #
769
+ # Supports optimistic locking with the +lock_version+ attribute and doesn't
770
+ # delete a model if it's already changed.
771
+ #
772
+ # Raises +Dynamoid::Errors::RecordNotDestroyed+ exception if model deleting
773
+ # failed.
395
774
  def destroy!
396
775
  destroy || (raise Dynamoid::Errors::RecordNotDestroyed, self)
397
776
  end
398
777
 
399
- # Delete this object from the datastore.
778
+ # Delete a model.
779
+ #
780
+ # Supports optimistic locking with the +lock_version+ attribute and doesn't
781
+ # delete a model if it's already changed.
782
+ #
783
+ # Raises +Dynamoid::Errors::StaleObjectError+ exception if cannot delete a
784
+ # model.
400
785
  #
401
786
  # @since 0.2.0
402
787
  def delete
@@ -413,7 +798,14 @@ module Dynamoid
413
798
  end
414
799
  options[:conditions] = conditions
415
800
  end
801
+
802
+ @destroyed = true
803
+
416
804
  Dynamoid.adapter.delete(self.class.table_name, hash_key, options)
805
+
806
+ self.class.associations.each do |name, options|
807
+ send(name).disassociate_source
808
+ end
417
809
  rescue Dynamoid::Errors::ConditionalCheckFailedException
418
810
  raise Dynamoid::Errors::StaleObjectError.new(self, 'delete')
419
811
  end