dynamoid 3.5.0 → 3.6.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 (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