dynamoid 3.3.0 → 3.7.0

Sign up to get free protection for your applications and to get access to all the features.
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