dynamoid 3.3.0 → 3.7.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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +104 -1
  3. data/README.md +146 -52
  4. data/lib/dynamoid.rb +1 -0
  5. data/lib/dynamoid/adapter.rb +20 -7
  6. data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +70 -37
  7. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/batch_get_item.rb +3 -0
  8. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb +20 -12
  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 +4 -2
  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 +2 -0
  27. data/lib/dynamoid/config.rb +15 -3
  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 +421 -46
  33. data/lib/dynamoid/criteria/ignored_conditions_detector.rb +3 -3
  34. data/lib/dynamoid/criteria/key_fields_detector.rb +31 -10
  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 +133 -46
  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 +251 -39
  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 +502 -104
  51. data/lib/dynamoid/persistence/import.rb +2 -1
  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 -75
  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
@@ -46,6 +99,7 @@ module Dynamoid
46
99
  options = {
47
100
  id: hash_key,
48
101
  table_name: table_name,
102
+ billing_mode: capacity_mode,
49
103
  write_capacity: write_capacity,
50
104
  read_capacity: read_capacity,
51
105
  range_key: range_key_hash,
@@ -54,14 +108,25 @@ module Dynamoid
54
108
  global_secondary_indexes: global_secondary_indexes.values
55
109
  }.merge(options)
56
110
 
57
- Dynamoid.adapter.create_table(options[:table_name], options[:id], options)
111
+ created_successfuly = Dynamoid.adapter.create_table(options[:table_name], options[:id], options)
112
+
113
+ if created_successfuly && self.options[:expires]
114
+ attribute = self.options[:expires][:field]
115
+ Dynamoid.adapter.update_time_to_live(table_name, attribute)
116
+ end
58
117
  end
59
118
 
60
- # 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.
61
125
  def delete_table
62
126
  Dynamoid.adapter.delete_table(table_name)
63
127
  end
64
128
 
129
+ # @private
65
130
  def from_database(attrs = {})
66
131
  klass = choose_right_class(attrs)
67
132
  attrs_undumped = Undumping.undump_attributes(attrs, klass.attributes)
@@ -70,95 +135,156 @@ module Dynamoid
70
135
 
71
136
  # Create several models at once.
72
137
  #
73
- # Neither callbacks nor validations run.
74
- # It works efficiently because of using `BatchWriteItem` API call.
75
- # Return array of models.
76
- # Uses backoff specified by `Dynamoid::Config.backoff` config option
138
+ # users = User.import([{ name: 'a' }, { name: 'b' }])
139
+ #
140
+ # +import+ is a relatively low-level method and bypasses some
141
+ # mechanisms like callbacks and validation.
142
+ #
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.
77
152
  #
78
- # @param [Array<Hash>] array_of_attributes
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.
79
155
  #
80
- # @example
81
- # User.import([{ name: 'a' }, { name: 'b' }])
156
+ # @param array_of_attributes [Array<Hash>]
157
+ # @return [Array] Created models
82
158
  def import(array_of_attributes)
83
159
  Import.call(self, array_of_attributes)
84
160
  end
85
161
 
86
162
  # Create a model.
87
163
  #
88
- # Initializes a new object and immediately saves it to the database.
89
- # 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
+ #
90
168
  # Accepts both Hash and Array of Hashes and can create several models.
91
169
  #
92
- # @param [Hash|Array[Hash]] attrs Attributes with which to create the object.
170
+ # User.create([{ first_name: 'Alice' }, { first_name: 'Bob' }])
171
+ #
172
+ # Creates a model and pass it into a block to set other attributes.
173
+ #
174
+ # User.create(first_name: 'Mark') do |u|
175
+ # u.age = 21
176
+ # end
93
177
  #
94
- # @return [Dynamoid::Document] the saved document
178
+ # Validates model and runs callbacks.
95
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
96
183
  # @since 0.2.0
97
- def create(attrs = {})
184
+ def create(attrs = {}, &block)
98
185
  if attrs.is_a?(Array)
99
- attrs.map { |attr| create(attr) }
186
+ attrs.map { |attr| create(attr, &block) }
100
187
  else
101
- build(attrs).tap(&:save)
188
+ build(attrs, &block).tap(&:save)
102
189
  end
103
190
  end
104
191
 
105
- # Create new model.
106
- #
107
- # Initializes a new object and immediately saves it to the database.
108
- # Raises an exception if validation failed.
109
- # Accepts both Hash and Array of Hashes and can create several models.
110
- #
111
- # @param [Hash|Array[Hash]] attrs Attributes with which to create the object.
192
+ # Create a model.
112
193
  #
113
- # @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.
114
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
115
202
  # @since 0.2.0
116
- def create!(attrs = {})
203
+ def create!(attrs = {}, &block)
117
204
  if attrs.is_a?(Array)
118
- attrs.map { |attr| create!(attr) }
205
+ attrs.map { |attr| create!(attr, &block) }
119
206
  else
120
- build(attrs).tap(&:save!)
207
+ build(attrs, &block).tap(&:save!)
121
208
  end
122
209
  end
123
210
 
124
211
  # Update document with provided attributes.
125
212
  #
126
- # Instantiates document and saves changes.
127
- # Runs validations and callbacks.
213
+ # Instantiates document and saves changes. Runs validations and
214
+ # callbacks. Don't save changes if validation fails.
128
215
  #
129
- # @param [Scalar value] partition key
130
- # @param [Scalar value] sort key, optional
131
- # @param [Hash] attributes
216
+ # User.update('1', age: 26)
132
217
  #
133
- # @return [Dynamoid::Doument] updated document
218
+ # If range key is declared for a model it should be passed as well:
134
219
  #
135
- # @example Update document
136
- # 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
137
226
  def update(hash_key, range_key_value = nil, attrs)
138
227
  model = find(hash_key, range_key: range_key_value, consistent_read: true)
139
228
  model.update_attributes(attrs)
140
229
  model
141
230
  end
142
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
+
143
255
  # Update document.
144
256
  #
145
- # Uses efficient low-level `UpdateItem` API call.
146
- # Changes attibutes and loads new document version with one API call.
147
- # Doesn't run validations and callbacks. Can make conditional update.
148
- # 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)
149
260
  #
150
- # @param [Scalar value] partition key
151
- # @param [Scalar value] sort key (optional)
152
- # @param [Hash] attributes
153
- # @param [Hash] conditions
261
+ # If range key is declared for a model it should be passed as well:
154
262
  #
155
- # @return [Dynamoid::Document|nil] updated document
263
+ # User.update_fields('1', 'Tylor', age: 26)
156
264
  #
157
- # @example Update document
158
- # Post.update_fields(101, read: true)
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:
159
268
  #
160
- # @example Update document with condition
161
- # Post.update_fields(101, { title: 'New title' }, if: { version: 1 })
269
+ # User.update_fields('1', { age: 26 }, if: { version: 1 })
270
+ #
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.
273
+ #
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
162
288
  def update_fields(hash_key_value, range_key_value = nil, attrs = {}, conditions = {})
163
289
  optional_params = [range_key_value, attrs, conditions].compact
164
290
  if optional_params.first.is_a?(Hash)
@@ -176,25 +302,40 @@ module Dynamoid
176
302
  conditions: conditions)
177
303
  end
178
304
 
179
- # 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)
180
316
  #
181
- # Similar to `.update_fields`.
182
- # The only diffirence is - it creates new document in case the document doesn't exist.
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:
183
320
  #
184
- # Uses efficient low-level `UpdateItem` API call.
185
- # Changes attibutes and loads new document version with one API call.
186
- # Doesn't run validations and callbacks. Can make conditional update.
187
- # If specified conditions failed - returns `nil`.
321
+ # User.upsert('1', { age: 26 }, if: { version: 1 })
188
322
  #
189
- # @param [Scalar value] partition key
190
- # @param [Scalar value] sort key (optional)
191
- # @param [Hash] attributes
192
- # @param [Hash] conditions
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.
193
325
  #
194
- # @return [Dynamoid::Document/nil] updated document
326
+ # If conditions were specified and failed the method call returns +nil+.
195
327
  #
196
- # @example Update document
197
- # Post.upsert(101, title: 'New title')
328
+ # +upsert+ uses the +UpdateItem+ operation so it saves changes and loads
329
+ # an updated document back with one HTTP request.
330
+ #
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
198
339
  def upsert(hash_key_value, range_key_value = nil, attrs = {}, conditions = {})
199
340
  optional_params = [range_key_value, attrs, conditions].compact
200
341
  if optional_params.first.is_a?(Hash)
@@ -212,19 +353,27 @@ module Dynamoid
212
353
  conditions: conditions)
213
354
  end
214
355
 
215
- # Increase numeric field by specified value.
356
+ # Increase a numeric field by specified value.
357
+ #
358
+ # User.inc('1', age: 2)
216
359
  #
217
360
  # Can update several fields at once.
218
- # Uses efficient low-level `UpdateItem` API call.
219
361
  #
220
- # @param [Scalar value] hash_key_value partition key
221
- # @param [Scalar value] range_key_value sort key (optional)
222
- # @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)
223
367
  #
224
- # @return [Dynamoid::Document/nil] updated document
368
+ # Uses efficient low-level +UpdateItem+ operation and does only one HTTP
369
+ # request.
225
370
  #
226
- # @example Update document
227
- # Post.inc(101, views_counter: 2, downloads: 10)
371
+ # Doesn't run validations and callbacks. Doesn't update +created_at+ and
372
+ # +updated_at+ as well.
373
+ #
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
228
377
  def inc(hash_key_value, range_key_value = nil, counters)
229
378
  options = if range_key
230
379
  value_casted = TypeCasting.cast_field(range_key_value, attributes[range_key])
@@ -245,8 +394,17 @@ module Dynamoid
245
394
  end
246
395
  end
247
396
 
248
- # 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.
249
404
  #
405
+ # user.touch(:last_login_at)
406
+ #
407
+ # @param name [Symbol] attribute name to update (optional)
250
408
  def touch(name = nil)
251
409
  now = DateTime.now
252
410
  self.updated_at = now
@@ -254,18 +412,72 @@ module Dynamoid
254
412
  save
255
413
  end
256
414
 
257
- # Is this object persisted in the datastore? Required for some ActiveModel integration stuff.
415
+ # Is this object persisted in DynamoDB?
416
+ #
417
+ # user = User.new
418
+ # user.persisted? # => false
258
419
  #
420
+ # user.save
421
+ # user.persisted? # => true
422
+ #
423
+ # @return [true|false]
259
424
  # @since 0.2.0
260
425
  def persisted?
261
- !new_record?
426
+ !(new_record? || @destroyed)
262
427
  end
263
428
 
264
- # 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
265
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.
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
266
476
  # @since 0.2.0
267
- def save(_options = {})
268
- self.class.create_table
477
+ def save(options = {})
478
+ self.class.create_table(sync: true)
479
+
480
+ @_touch_record = options[:touch]
269
481
 
270
482
  if new_record?
271
483
  run_callbacks(:create) do
@@ -280,20 +492,35 @@ module Dynamoid
280
492
  end
281
493
  end
282
494
 
283
- # 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.
284
498
  #
285
- # @param [Hash] attributes a hash of attributes to update
499
+ # user.update_attributes(age: 27, last_name: 'Tylor')
286
500
  #
501
+ # Raises a +Dynamoid::Errors::UnknownAttribute+ exception if any of the
502
+ # attributes is not on the model
503
+ #
504
+ # @param attributes [Hash] a hash of attributes to update
505
+ # @return [true|false] Whether updating successful or not
287
506
  # @since 0.2.0
288
507
  def update_attributes(attributes)
289
508
  attributes.each { |attribute, value| write_attribute(attribute, value) }
290
509
  save
291
510
  end
292
511
 
293
- # Updates multiple attributes at once, saving the object once the updates are complete.
294
- # 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.
514
+ #
515
+ # user.update_attributes!(age: 27, last_name: 'Tylor')
516
+ #
517
+ # Raises a +Dynamoid::Errors::DocumentNotValid+ exception if some vaidation
518
+ # fails.
295
519
  #
296
- # @param [Hash] attributes a hash of attributes to update
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
297
524
  def update_attributes!(attributes)
298
525
  attributes.each { |attribute, value| write_attribute(attribute, value) }
299
526
  save!
@@ -301,20 +528,71 @@ module Dynamoid
301
528
 
302
529
  # Update a single attribute, saving the object afterwards.
303
530
  #
304
- # @param [Symbol] attribute the attribute to update
305
- # @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')
306
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
307
541
  # @since 0.2.0
308
542
  def update_attribute(attribute, value)
309
543
  write_attribute(attribute, value)
310
544
  save
311
545
  end
312
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+).
313
557
  #
314
- # update!() will increment the lock_version if the table has the column, but will not check it. Thus, a concurrent save will
315
- # 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
316
562
  #
563
+ # Operation +delete+ is applied to collection attribute types and
564
+ # substructs one collection from another.
317
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)
318
596
  def update!(conditions = {})
319
597
  run_callbacks(:update) do
320
598
  options = range_key ? { range_key: Dumping.dump_field(read_attribute(range_key), self.class.attributes[range_key]) } : {}
@@ -336,9 +614,56 @@ module Dynamoid
336
614
  raise Dynamoid::Errors::StaleObjectError.new(self, 'update')
337
615
  end
338
616
  end
339
-
340
617
  end
341
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)
342
667
  def update(conditions = {}, &block)
343
668
  update!(conditions, &block)
344
669
  true
@@ -346,51 +671,117 @@ module Dynamoid
346
671
  false
347
672
  end
348
673
 
349
- # Initializes attribute to zero if nil and adds the value passed as by (default is 1).
350
- # 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
351
685
  def increment(attribute, by = 1)
352
686
  self[attribute] ||= 0
353
687
  self[attribute] += by
354
688
  self
355
689
  end
356
690
 
357
- # Wrapper around increment that saves the record.
358
- # 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
359
704
  def increment!(attribute, by = 1)
360
705
  increment(attribute, by)
361
706
  save
362
707
  end
363
708
 
364
- # Initializes attribute to zero if nil and subtracts the value passed as by (default is 1).
365
- # 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
366
720
  def decrement(attribute, by = 1)
367
721
  self[attribute] ||= 0
368
722
  self[attribute] -= by
369
723
  self
370
724
  end
371
725
 
372
- # Wrapper around decrement that saves the record.
373
- # 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
374
739
  def decrement!(attribute, by = 1)
375
740
  decrement(attribute, by)
376
741
  save
377
742
  end
378
743
 
379
- # 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.
380
750
  #
751
+ # Returns +true+ if deleted successfully and +false+ otherwise.
752
+ #
753
+ # @return [true|false] whether deleted successfully
381
754
  # @since 0.2.0
382
755
  def destroy
383
756
  ret = run_callbacks(:destroy) do
384
757
  delete
385
758
  end
759
+
760
+ @destroyed = true
761
+
386
762
  ret == false ? false : self
387
763
  end
388
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.
389
774
  def destroy!
390
775
  destroy || (raise Dynamoid::Errors::RecordNotDestroyed, self)
391
776
  end
392
777
 
393
- # 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.
394
785
  #
395
786
  # @since 0.2.0
396
787
  def delete
@@ -407,7 +798,14 @@ module Dynamoid
407
798
  end
408
799
  options[:conditions] = conditions
409
800
  end
801
+
802
+ @destroyed = true
803
+
410
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
411
809
  rescue Dynamoid::Errors::ConditionalCheckFailedException
412
810
  raise Dynamoid::Errors::StaleObjectError.new(self, 'delete')
413
811
  end