dynamoid 3.5.0 → 3.6.0

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