aws-record 2.10.1 → 2.12.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +83 -19
  3. data/VERSION +1 -1
  4. data/lib/aws-record/record/attribute.rb +8 -8
  5. data/lib/aws-record/record/attributes.rb +36 -49
  6. data/lib/aws-record/record/batch.rb +13 -12
  7. data/lib/aws-record/record/batch_read.rb +10 -12
  8. data/lib/aws-record/record/batch_write.rb +2 -1
  9. data/lib/aws-record/record/buildable_search.rb +37 -39
  10. data/lib/aws-record/record/client_configuration.rb +14 -14
  11. data/lib/aws-record/record/dirty_tracking.rb +29 -40
  12. data/lib/aws-record/record/errors.rb +11 -2
  13. data/lib/aws-record/record/item_collection.rb +7 -7
  14. data/lib/aws-record/record/item_data.rb +13 -17
  15. data/lib/aws-record/record/item_operations.rb +150 -138
  16. data/lib/aws-record/record/key_attributes.rb +0 -2
  17. data/lib/aws-record/record/marshalers/boolean_marshaler.rb +2 -5
  18. data/lib/aws-record/record/marshalers/date_marshaler.rb +1 -6
  19. data/lib/aws-record/record/marshalers/date_time_marshaler.rb +2 -5
  20. data/lib/aws-record/record/marshalers/epoch_time_marshaler.rb +2 -8
  21. data/lib/aws-record/record/marshalers/float_marshaler.rb +3 -8
  22. data/lib/aws-record/record/marshalers/integer_marshaler.rb +3 -8
  23. data/lib/aws-record/record/marshalers/list_marshaler.rb +4 -7
  24. data/lib/aws-record/record/marshalers/map_marshaler.rb +4 -7
  25. data/lib/aws-record/record/marshalers/numeric_set_marshaler.rb +7 -9
  26. data/lib/aws-record/record/marshalers/string_marshaler.rb +1 -2
  27. data/lib/aws-record/record/marshalers/string_set_marshaler.rb +5 -7
  28. data/lib/aws-record/record/marshalers/time_marshaler.rb +1 -5
  29. data/lib/aws-record/record/model_attributes.rb +17 -29
  30. data/lib/aws-record/record/query.rb +8 -11
  31. data/lib/aws-record/record/secondary_indexes.rb +40 -51
  32. data/lib/aws-record/record/table_config.rb +93 -115
  33. data/lib/aws-record/record/table_migration.rb +56 -72
  34. data/lib/aws-record/record/transactions.rb +40 -43
  35. data/lib/aws-record/record/version.rb +1 -1
  36. data/lib/aws-record/record.rb +36 -44
  37. metadata +13 -8
@@ -3,22 +3,21 @@
3
3
  module Aws
4
4
  module Record
5
5
  class BuildableSearch
6
- SUPPORTED_OPERATIONS = [:query, :scan]
6
+ SUPPORTED_OPERATIONS = %i[query scan].freeze
7
7
 
8
8
  # This should never be called directly, rather it is called by the
9
9
  # #build_query or #build_scan methods of your aws-record model class.
10
10
  def initialize(opts)
11
11
  operation = opts[:operation]
12
12
  model = opts[:model]
13
- if SUPPORTED_OPERATIONS.include?(operation)
14
- @operation = operation
15
- else
16
- raise ArgumentError.new("Unsupported operation: #{operation}")
17
- end
13
+ raise ArgumentError, "Unsupported operation: #{operation}" unless SUPPORTED_OPERATIONS.include?(operation)
14
+
15
+ @operation = operation
16
+
18
17
  @model = model
19
18
  @params = {}
20
- @next_name = "BUILDERA"
21
- @next_value = "buildera"
19
+ @next_name = 'BUILDERA'
20
+ @next_value = 'buildera'
22
21
  end
23
22
 
24
23
  # If you are querying or scanning on an index, you can specify it with
@@ -41,12 +40,11 @@ module Aws
41
40
  # builder method to provide the :total_segments of your parallel scan and
42
41
  # the :segment number of this scan.
43
42
  def parallel_scan(opts)
44
- unless @operation == :scan
45
- raise ArgumentError.new("parallel_scan is only supported for scans")
46
- end
43
+ raise ArgumentError, 'parallel_scan is only supported for scans' unless @operation == :scan
47
44
  unless opts[:total_segments] && opts[:segment]
48
- raise ArgumentError.new("Must specify :total_segments and :segment in a parallel scan.")
45
+ raise ArgumentError, 'Must specify :total_segments and :segment in a parallel scan.'
49
46
  end
47
+
50
48
  @params[:total_segments] = opts[:total_segments]
51
49
  @params[:segment] = opts[:segment]
52
50
  self
@@ -56,9 +54,8 @@ module Aws
56
54
  # ascending or descending order on your range key. By default, a query is
57
55
  # run in ascending order.
58
56
  def scan_ascending(b)
59
- unless @operation == :query
60
- raise ArgumentError.new("scan_ascending is only supported for queries.")
61
- end
57
+ raise ArgumentError, 'scan_ascending is only supported for queries.' unless @operation == :query
58
+
62
59
  @params[:scan_index_forward] = b
63
60
  self
64
61
  end
@@ -90,9 +87,8 @@ module Aws
90
87
  # ).complete!
91
88
  # q.to_a # You can use this like any other query result in aws-record
92
89
  def key_expr(statement_str, *subs)
93
- unless @operation == :query
94
- raise ArgumentError.new("key_expr is only supported for queries.")
95
- end
90
+ raise ArgumentError, 'key_expr is only supported for queries.' unless @operation == :query
91
+
96
92
  names = @params[:expression_attribute_names]
97
93
  if names.nil?
98
94
  @params[:expression_attribute_names] = {}
@@ -145,10 +141,10 @@ module Aws
145
141
 
146
142
  # Allows you to define a projection expression for the values returned by
147
143
  # a query or scan. See
148
- # {https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ProjectionExpressions.html the Amazon DynamoDB Developer Guide}
149
- # for more details on projection expressions. You can use the symbols from
150
- # your aws-record model class in a projection expression. Keys are always
151
- # retrieved.
144
+ # {https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ProjectionExpressions.html
145
+ # the Amazon DynamoDB Developer Guide} for more details on projection expressions.
146
+ # You can use the symbols from your aws-record model class in a projection expression.
147
+ # Keys are always retrieved.
152
148
  #
153
149
  # @example Scan with a projection expression:
154
150
  # # Example model class
@@ -233,44 +229,46 @@ module Aws
233
229
  end
234
230
 
235
231
  private
232
+
236
233
  def _key_pass(statement, names)
237
234
  statement.gsub(/:(\w+)/) do |match|
238
- key = match.gsub(':','').to_sym
235
+ key = match.gsub(':', '').to_sym
239
236
  key_name = @model.attributes.storage_name_for(key)
240
- if key_name
241
- sub_name = _next_name
242
- raise "Substitution collision!" if names[sub_name]
243
- names[sub_name] = key_name
244
- sub_name
245
- else
246
- raise "No such key #{key}"
247
- end
237
+
238
+ raise "No such key #{key}" unless key_name
239
+
240
+ sub_name = _next_name
241
+
242
+ raise 'Substitution collision!' if names[sub_name]
243
+
244
+ names[sub_name] = key_name
245
+ sub_name
248
246
  end
249
247
  end
250
248
 
251
249
  def _apply_values(statement, subs, values)
252
250
  count = 0
253
- statement.gsub(/[?]/) do |match|
251
+ result = statement.gsub(/[?]/) do
254
252
  sub_value = _next_value
255
- raise "Substitution collision!" if values[sub_value]
253
+ raise 'Substitution collision!' if values[sub_value]
254
+
256
255
  values[sub_value] = subs[count]
257
256
  count += 1
258
257
  sub_value
259
- end.tap do
260
- unless count == subs.size
261
- raise "Expected #{count} values in the substitution set, but found #{subs.size}"
262
- end
258
+ end
259
+ result.tap do
260
+ raise "Expected #{count} values in the substitution set, but found #{subs.size}" unless count == subs.size
263
261
  end
264
262
  end
265
263
 
266
264
  def _next_name
267
- ret = "#" + @next_name
265
+ ret = "##{@next_name}"
268
266
  @next_name = @next_name.next
269
267
  ret
270
268
  end
271
269
 
272
270
  def _next_value
273
- ret = ":" + @next_value
271
+ ret = ":#{@next_value}"
274
272
  @next_value = @next_value.next
275
273
  ret
276
274
  end
@@ -10,7 +10,7 @@ module Aws
10
10
  # attempt to perform an operation against the remote end, if you have not
11
11
  # already configured a client. As such, please read and understand the
12
12
  # documentation in the AWS SDK for Ruby around
13
- # {http://docs.aws.amazon.com/sdkforruby/api/index.html#Configuration configuration}
13
+ # {http://docs.aws.amazon.com/sdk-for-ruby/v3/api/index.html#Configuration configuration}
14
14
  # to ensure you understand how default configuration behavior works. When
15
15
  # in doubt, call this method to ensure your client is configured the way
16
16
  # you want it to be configured.
@@ -20,16 +20,19 @@ module Aws
20
20
  # @param [Hash] opts the options you wish to use to create the client.
21
21
  # Note that if you include the option +:client+, all other options
22
22
  # will be ignored. See the documentation for other options in the
23
- # {https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/DynamoDB/Client.html#initialize-instance_method AWS SDK for Ruby}.
23
+ # {https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/DynamoDB/Client.html#initialize-instance_method
24
+ # AWS SDK for Ruby}.
24
25
  # @option opts [Aws::DynamoDB::Client] :client allows you to pass in your
25
26
  # own pre-configured client.
26
27
  def configure_client(opts = {})
27
- if self.class != Module && Aws::Record.extends_record?(self) && opts.empty? &&
28
- self.superclass.instance_variable_get('@dynamodb_client')
29
- @dynamodb_client = self.superclass.instance_variable_get('@dynamodb_client')
30
- else
31
- @dynamodb_client = _build_client(opts)
32
- end
28
+ # rubocop:disable Style/RedundantSelf
29
+ @dynamodb_client = if self.class != Module && Aws::Record.extends_record?(self) && opts.empty? &&
30
+ self.superclass.instance_variable_get('@dynamodb_client')
31
+ self.superclass.instance_variable_get('@dynamodb_client')
32
+ else
33
+ _build_client(opts)
34
+ end
35
+ # rubocop:enable Style/RedundantSelf
33
36
  end
34
37
 
35
38
  # Gets the
@@ -50,12 +53,9 @@ module Aws
50
53
 
51
54
  def _build_client(opts = {})
52
55
  provided_client = opts.delete(:client)
53
- opts[:user_agent_suffix] = _user_agent(opts.delete(:user_agent_suffix))
54
- provided_client || Aws::DynamoDB::Client.new(opts)
55
- end
56
-
57
- def _user_agent(custom)
58
- custom || " aws-record/#{VERSION}"
56
+ client = provided_client || Aws::DynamoDB::Client.new(opts)
57
+ client.config.user_agent_frameworks << 'aws-record'
58
+ client
59
59
  end
60
60
  end
61
61
  end
@@ -3,7 +3,6 @@
3
3
  module Aws
4
4
  module Record
5
5
  module DirtyTracking
6
-
7
6
  def self.included(sub_class)
8
7
  sub_class.extend(DirtyTrackingClassMethods)
9
8
  end
@@ -26,7 +25,7 @@ module Aws
26
25
  # @return [Boolean] +true+ if the specified attribute has any dirty changes, +false+ otherwise.
27
26
  def attribute_dirty?(name)
28
27
  @data.attribute_dirty?(name)
29
- end
28
+ end
30
29
 
31
30
  # Returns the original value of the specified attribute.
32
31
  #
@@ -39,7 +38,7 @@ module Aws
39
38
  #
40
39
  # model.name # => 'Alex'
41
40
  # model.name = 'Nick'
42
- # model.name_was # => 'Alex'
41
+ # model.name_was # => 'Alex'
43
42
  #
44
43
  # @param [String, Symbol] name The name of the attribute to retrieve the original value of.
45
44
  # @return [Object] The original value of the specified attribute.
@@ -47,8 +46,9 @@ module Aws
47
46
  @data.attribute_was(name)
48
47
  end
49
48
 
50
- # Mark that an attribute is changing. This is useful in situations where it is necessary to track that the value of an
51
- # attribute is changing in-place.
49
+ # Mark that an attribute is changing. This is useful in situations
50
+ # where it is necessary to track that the value of an
51
+ # attribute is changing in-place.
52
52
  #
53
53
  # @example
54
54
  # class Model
@@ -63,9 +63,9 @@ module Aws
63
63
  #
64
64
  # model.name << 'i'
65
65
  # model.name # => 'Alexi'
66
- #
67
- # # The change was made in place. Since the String instance representing
68
- # # the value of name is the same as it was originally, the change is not
66
+ #
67
+ # # The change was made in place. Since the String instance representing
68
+ # # the value of name is the same as it was originally, the change is not
69
69
  # # detected.
70
70
  # model.name_dirty? # => false
71
71
  # model.name_was # => 'Alexi'
@@ -79,7 +79,7 @@ module Aws
79
79
  # model.name_dirty? # => true
80
80
  # model.name_was # => 'Alexi'
81
81
  #
82
- # @param [String, Symbol] name The name of the attribute to mark as
82
+ # @param [String, Symbol] name The name of the attribute to mark as
83
83
  # changing.
84
84
  def attribute_dirty!(name)
85
85
  @data.attribute_dirty!(name)
@@ -118,7 +118,7 @@ module Aws
118
118
  # model.name = 'Nick'
119
119
  # model.dirty? # => true
120
120
  #
121
- # @return [Boolean] +true+ if any attributes have dirty changes, +false+
121
+ # @return [Boolean] +true+ if any attributes have dirty changes, +false+
122
122
  # otherwise.
123
123
  def dirty?
124
124
  @data.dirty?
@@ -140,7 +140,7 @@ module Aws
140
140
  # model.delete!
141
141
  # model.persisted? # => false
142
142
  #
143
- # @return [Boolean] +true+ if the model is not new and has not been deleted, +false+
143
+ # @return [Boolean] +true+ if the model is not new and has not been deleted, +false+
144
144
  # otherwise.
145
145
  def persisted?
146
146
  @data.persisted?
@@ -160,7 +160,7 @@ module Aws
160
160
  # model.save
161
161
  # model.new_record? # => false
162
162
  #
163
- # @return [Boolean] +true+ if the model is newly initialized, +false+
163
+ # @return [Boolean] +true+ if the model is newly initialized, +false+
164
164
  # otherwise.
165
165
  def new_record?
166
166
  @data.new_record?
@@ -179,35 +179,33 @@ module Aws
179
179
  # model.destroyed? # => false
180
180
  # model.save
181
181
  # model.destroyed? # => false
182
- # model.delete!
182
+ # model.delete!
183
183
  # model.destroyed? # => true
184
184
  #
185
- # @return [Boolean] +true+ if the model has been destroyed, +false+
185
+ # @return [Boolean] +true+ if the model has been destroyed, +false+
186
186
  # otherwise.
187
187
  def destroyed?
188
188
  @data.destroyed?
189
189
  end
190
190
 
191
- # Fetches attributes for this instance of an item from Amazon DynamoDB
191
+ # Fetches attributes for this instance of an item from Amazon DynamoDB
192
192
  # using its primary key and the +find(*)+ class method.
193
193
  #
194
- # @raise [Aws::Record::Errors::NotFound] if no record exists in the
194
+ # @raise [Aws::Record::Errors::NotFound] if no record exists in the
195
195
  # database matching the primary key of the item instance.
196
- #
196
+
197
197
  # @return [self] Returns the item instance.
198
198
  def reload!
199
- primary_key = self.class.keys.values.inject({}) do |memo, key|
199
+ primary_key = self.class.keys.values.each_with_object({}) do |key, memo|
200
200
  memo[key] = send(key)
201
- memo
201
+ memo
202
202
  end
203
203
 
204
204
  record = self.class.find(primary_key)
205
205
 
206
- unless record.nil?
207
- @data = record.instance_variable_get("@data")
208
- else
209
- raise Errors::NotFound.new("No record found")
210
- end
206
+ raise Errors::NotFound, 'No record found' unless record.present?
207
+
208
+ @data = record.instance_variable_get('@data')
211
209
 
212
210
  clean!
213
211
 
@@ -247,7 +245,7 @@ module Aws
247
245
  # model.rollback!
248
246
  # model.name # => 'Alex'
249
247
  #
250
- # @param [Array, String, Symbol] names The names of attributes to restore.
248
+ # @param [Array, String, Symbol] names The names of attributes to restore.
251
249
  def rollback!(names = dirty)
252
250
  Array(names).each { |name| rollback_attribute!(name) }
253
251
  end
@@ -258,48 +256,39 @@ module Aws
258
256
  end
259
257
 
260
258
  # @private
261
- #
262
- # @override save(*)
263
259
  def save(*)
264
260
  super.tap { clean! }
265
261
  end
266
262
 
267
263
  module DirtyTrackingClassMethods
268
-
269
264
  private
270
265
 
271
266
  # @private
272
- #
273
- # @override build_item_from_resp(*)
274
267
  def build_item_from_resp(*)
275
- super.tap { |item| item.clean! }
268
+ super.tap(&:clean!)
276
269
  end
277
270
 
278
271
  # @private
279
- #
280
- # @override define_attr_methods(*)
281
272
  def _define_attr_methods(name)
282
- super.tap do
283
- define_method("#{name}_dirty?") do
273
+ super.tap do
274
+ define_method("#{name}_dirty?") do
284
275
  attribute_dirty?(name)
285
276
  end
286
277
 
287
- define_method("#{name}_dirty!") do
278
+ define_method("#{name}_dirty!") do
288
279
  attribute_dirty!(name)
289
280
  end
290
281
 
291
- define_method("#{name}_was") do
282
+ define_method("#{name}_was") do
292
283
  attribute_was(name)
293
284
  end
294
285
 
295
- define_method("rollback_#{name}!") do
286
+ define_method("rollback_#{name}!") do
296
287
  rollback_attribute!(name)
297
288
  end
298
289
  end
299
290
  end
300
-
301
291
  end
302
-
303
292
  end
304
293
  end
305
294
  end
@@ -3,7 +3,6 @@
3
3
  module Aws
4
4
  module Record
5
5
  module Errors
6
-
7
6
  # RecordErrors relate to the persistence of items. They include both
8
7
  # client errors and certain validation errors.
9
8
  class RecordError < RuntimeError; end
@@ -17,7 +16,17 @@ module Aws
17
16
  class NotFound < RecordError; end
18
17
 
19
18
  # Raised when a conditional write fails.
20
- class ConditionalWriteFailed < RecordError; end
19
+ # Provides access to the original ConditionalCheckFailedException error
20
+ # which may have item data if the return values option was used.
21
+ class ConditionalWriteFailed < RecordError
22
+ def initialize(message, original_error)
23
+ @original_error = original_error
24
+ super(message)
25
+ end
26
+
27
+ # @return [Aws::DynamoDB::Errors::ConditionalCheckFailedException]
28
+ attr_reader :original_error
29
+ end
21
30
 
22
31
  # Raised when a validation hook call to +:valid?+ fails.
23
32
  class ValidationError < RecordError; end
@@ -27,12 +27,11 @@ module Aws
27
27
  # into items on your behalf.
28
28
  def each(&block)
29
29
  return enum_for(:each) unless block_given?
30
+
30
31
  items.each_page do |page|
31
32
  @last_evaluated_key = page.last_evaluated_key
32
33
  items_array = _build_items_from_response(page.items, @model)
33
- items_array.each do |item|
34
- yield item
35
- end
34
+ items_array.each(&block)
36
35
  end
37
36
  end
38
37
 
@@ -72,19 +71,21 @@ module Aws
72
71
  # otherwise.
73
72
  def empty?
74
73
  items.each_page do |page|
75
- return false if !page.items.empty?
74
+ return false unless page.items.empty?
76
75
  end
77
76
  true
78
77
  end
79
78
 
80
79
  private
80
+
81
81
  def _build_items_from_response(items, model)
82
82
  ret = []
83
83
  items.each do |item|
84
84
  model_class = @model_filter ? @model_filter.call(item) : model
85
85
  next unless model_class
86
+
86
87
  record = model_class.new
87
- data = record.instance_variable_get("@data")
88
+ data = record.instance_variable_get('@data')
88
89
  model_class.attributes.attributes.each do |name, attr|
89
90
  data.set_attribute(name, attr.extract(item))
90
91
  end
@@ -96,9 +97,8 @@ module Aws
96
97
  end
97
98
 
98
99
  def items
99
- @_items ||= @client.send(@search_method, @search_params)
100
+ @items ||= @client.send(@search_method, @search_params)
100
101
  end
101
-
102
102
  end
103
103
  end
104
104
  end
@@ -2,7 +2,6 @@
2
2
 
3
3
  module Aws
4
4
  module Record
5
-
6
5
  # @api private
7
6
  class ItemData
8
7
  def initialize(model_attributes, opts)
@@ -17,7 +16,6 @@ module Aws
17
16
 
18
17
  populate_default_values
19
18
  end
20
-
21
19
  attr_accessor :new_record, :destroyed
22
20
 
23
21
  def get_attribute(name)
@@ -49,11 +47,11 @@ module Aws
49
47
  @model_attributes.attributes.each_key do |name|
50
48
  populate_default_values
51
49
  value = get_attribute(name)
52
- if @track_mutations
53
- @clean_copies[name] = _deep_copy(value)
54
- else
55
- @clean_copies[name] = value
56
- end
50
+ @clean_copies[name] = if @track_mutations
51
+ _deep_copy(value)
52
+ else
53
+ value
54
+ end
57
55
  end
58
56
  end
59
57
 
@@ -75,14 +73,14 @@ module Aws
75
73
  end
76
74
 
77
75
  def dirty
78
- @model_attributes.attributes.keys.inject([]) do |acc, name|
76
+ @model_attributes.attributes.keys.each_with_object([]) do |name, acc|
79
77
  acc << name if attribute_dirty?(name)
80
78
  acc
81
79
  end
82
80
  end
83
81
 
84
82
  def dirty?
85
- dirty.empty? ? false : true
83
+ !dirty.empty?
86
84
  end
87
85
 
88
86
  def rollback_attribute!(name)
@@ -98,7 +96,7 @@ module Aws
98
96
  end
99
97
 
100
98
  def build_save_hash
101
- @data.inject({}) do |acc, name_value_pair|
99
+ @data.each_with_object({}) do |name_value_pair, acc|
102
100
  attr_name, raw_value = name_value_pair
103
101
  attribute = @model_attributes.attribute_for(attr_name)
104
102
  if !raw_value.nil? || attribute.persist_nil?
@@ -111,20 +109,18 @@ module Aws
111
109
 
112
110
  def populate_default_values
113
111
  @model_attributes.attributes.each do |name, attribute|
114
- unless (default_value = attribute.default_value).nil?
115
- if @data[name].nil? && @data[name].nil?
116
- @data[name] = default_value
117
- end
118
- end
112
+ next if (default_value = attribute.default_value).nil?
113
+ next unless @data[name].nil?
114
+
115
+ @data[name] = default_value
119
116
  end
120
117
  end
121
118
 
122
119
  private
120
+
123
121
  def _deep_copy(obj)
124
122
  Marshal.load(Marshal.dump(obj))
125
123
  end
126
-
127
124
  end
128
-
129
125
  end
130
126
  end