aws-record 2.7.0 → 2.10.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +330 -0
  3. data/LICENSE +202 -0
  4. data/VERSION +1 -0
  5. data/lib/aws-record/record/attribute.rb +5 -15
  6. data/lib/aws-record/record/attributes.rb +161 -32
  7. data/lib/aws-record/record/batch.rb +99 -35
  8. data/lib/aws-record/record/batch_read.rb +186 -0
  9. data/lib/aws-record/record/batch_write.rb +9 -14
  10. data/lib/aws-record/record/buildable_search.rb +4 -2
  11. data/lib/aws-record/record/client_configuration.rb +13 -14
  12. data/lib/aws-record/record/dirty_tracking.rb +1 -12
  13. data/lib/aws-record/record/errors.rb +1 -12
  14. data/lib/aws-record/record/item_collection.rb +2 -13
  15. data/lib/aws-record/record/item_data.rb +1 -12
  16. data/lib/aws-record/record/item_operations.rb +47 -12
  17. data/lib/aws-record/record/key_attributes.rb +1 -12
  18. data/lib/aws-record/record/marshalers/boolean_marshaler.rb +1 -12
  19. data/lib/aws-record/record/marshalers/date_marshaler.rb +1 -12
  20. data/lib/aws-record/record/marshalers/date_time_marshaler.rb +1 -12
  21. data/lib/aws-record/record/marshalers/epoch_time_marshaler.rb +1 -12
  22. data/lib/aws-record/record/marshalers/float_marshaler.rb +1 -12
  23. data/lib/aws-record/record/marshalers/integer_marshaler.rb +1 -12
  24. data/lib/aws-record/record/marshalers/list_marshaler.rb +1 -12
  25. data/lib/aws-record/record/marshalers/map_marshaler.rb +1 -12
  26. data/lib/aws-record/record/marshalers/numeric_set_marshaler.rb +1 -12
  27. data/lib/aws-record/record/marshalers/string_marshaler.rb +1 -12
  28. data/lib/aws-record/record/marshalers/string_set_marshaler.rb +1 -12
  29. data/lib/aws-record/record/marshalers/time_marshaler.rb +1 -12
  30. data/lib/aws-record/record/model_attributes.rb +8 -12
  31. data/lib/aws-record/record/query.rb +1 -12
  32. data/lib/aws-record/record/secondary_indexes.rb +24 -12
  33. data/lib/aws-record/record/table_config.rb +1 -12
  34. data/lib/aws-record/record/table_migration.rb +1 -12
  35. data/lib/aws-record/record/transactions.rb +39 -7
  36. data/lib/aws-record/record/version.rb +2 -13
  37. data/lib/aws-record/record.rb +100 -20
  38. data/lib/aws-record.rb +2 -12
  39. metadata +8 -4
@@ -1,15 +1,4 @@
1
- # Copyright 2015-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
- #
3
- # Licensed under the Apache License, Version 2.0 (the "License"). You may not
4
- # use this file except in compliance with the License. A copy of the License is
5
- # located at
6
- #
7
- # http://aws.amazon.com/apache2.0/
8
- #
9
- # or in the "license" file accompanying this file. This file is distributed on
10
- # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11
- # or implied. See the License for the specific language governing permissions
12
- # and limitations under the License.
1
+ # frozen_string_literal: true
13
2
 
14
3
  module Aws
15
4
  module Record
@@ -20,8 +9,20 @@ module Aws
20
9
  model_attributes = ModelAttributes.new(self)
21
10
  sub_class.instance_variable_set("@attributes", model_attributes)
22
11
  sub_class.instance_variable_set("@keys", KeyAttributes.new(model_attributes))
12
+ if Aws::Record.extends_record?(sub_class)
13
+ inherit_attributes(sub_class)
14
+ end
23
15
  end
24
16
 
17
+ # Base initialization method for a new item. Optionally, allows you to
18
+ # provide initial attribute values for the model. You do not need to
19
+ # provide all, or even any, attributes at item creation time.
20
+ #
21
+ # === Inheritance Support
22
+ # Child models will inherit the attributes and keys defined in the parent
23
+ # model. Child models can override attribute keys if defined in their own model.
24
+ #
25
+ # See examples below to see the feature in action.
25
26
  # @example Usage Example
26
27
  # class MyModel
27
28
  # include Aws::Record
@@ -31,11 +32,34 @@ module Aws
31
32
  # end
32
33
  #
33
34
  # item = MyModel.new(id: 1, name: "Quick Create")
35
+ # @example Child model inheriting from Parent model
36
+ # class Animal
37
+ # include Aws::Record
38
+ # string_attr :name, hash_key: true
39
+ # integer_attr :age, default_value: 1
40
+ # end
34
41
  #
35
- # Base initialization method for a new item. Optionally, allows you to
36
- # provide initial attribute values for the model. You do not need to
37
- # provide all, or even any, attributes at item creation time.
42
+ # class Cat < Animal
43
+ # include Aws::Record
44
+ # integer_attr :num_of_wiskers
45
+ # end
38
46
  #
47
+ # cat = Cat.find(name: 'Foo')
48
+ # cat.age # => 1
49
+ # cat.num_of_wiskers = 200
50
+ # @example Child model overrides the hash key
51
+ # class Animal
52
+ # include Aws::Record
53
+ # string_attr :name, hash_key: true
54
+ # integer_attr :age, range_key: true
55
+ # end
56
+ #
57
+ # class Dog < Animal
58
+ # include Aws::Record
59
+ # integer_attr :id, hash_key: true
60
+ # end
61
+ #
62
+ # Dog.keys # => {:hash=>:id, :range=>:age}
39
63
  # @param [Hash] attr_values Attribute symbol/value pairs for any initial
40
64
  # attribute values you wish to set.
41
65
  # @return [Aws::Record] An item instance for your model.
@@ -56,6 +80,27 @@ module Aws
56
80
  @data.hash_copy
57
81
  end
58
82
 
83
+ private
84
+ def self.inherit_attributes(klass)
85
+ superclass_attributes = klass.superclass.instance_variable_get("@attributes")
86
+
87
+ superclass_attributes.attributes.each do |name, attribute|
88
+ subclass_attributes = klass.instance_variable_get("@attributes")
89
+ subclass_attributes.register_superclass_attribute(name, attribute)
90
+ end
91
+
92
+ superclass_keys = klass.superclass.instance_variable_get("@keys")
93
+ subclass_keys = klass.instance_variable_get("@keys")
94
+
95
+ if superclass_keys.hash_key
96
+ subclass_keys.hash_key = superclass_keys.hash_key
97
+ end
98
+
99
+ if superclass_keys.range_key
100
+ subclass_keys.range_key = superclass_keys.range_key
101
+ end
102
+ end
103
+
59
104
  module ClassMethods
60
105
 
61
106
  # Define an attribute for your model, providing your own attribute type.
@@ -82,7 +127,8 @@ module Aws
82
127
  # nil values will be ignored and not persisted. By default, is false.
83
128
  # @option opts [Object] :default_value Optional attribute used to
84
129
  # define a "default value" to be used if the attribute's value on an
85
- # item is nil or not set at persistence time.
130
+ # item is nil or not set at persistence time. Additionally, lambda
131
+ # can be used as a default value.
86
132
  # @option opts [Boolean] :hash_key Set to true if this attribute is
87
133
  # the hash key for the table.
88
134
  # @option opts [Boolean] :range_key Set to true if this attribute is
@@ -108,7 +154,8 @@ module Aws
108
154
  # nil values will be ignored and not persisted. By default, is false.
109
155
  # @option opts [Object] :default_value Optional attribute used to
110
156
  # define a "default value" to be used if the attribute's value on an
111
- # item is nil or not set at persistence time.
157
+ # item is nil or not set at persistence time. Additionally, lambda
158
+ # can be used as a default value.
112
159
  def string_attr(name, opts = {})
113
160
  opts[:dynamodb_type] = "S"
114
161
  attr(name, Marshalers::StringMarshaler.new(opts), opts)
@@ -129,7 +176,8 @@ module Aws
129
176
  # nil values will be ignored and not persisted. By default, is false.
130
177
  # @option opts [Object] :default_value Optional attribute used to
131
178
  # define a "default value" to be used if the attribute's value on an
132
- # item is nil or not set at persistence time.
179
+ # item is nil or not set at persistence time. Additionally, lambda
180
+ # can be used as a default value.
133
181
  def boolean_attr(name, opts = {})
134
182
  opts[:dynamodb_type] = "BOOL"
135
183
  attr(name, Marshalers::BooleanMarshaler.new(opts), opts)
@@ -150,7 +198,8 @@ module Aws
150
198
  # nil values will be ignored and not persisted. By default, is false.
151
199
  # @option opts [Object] :default_value Optional attribute used to
152
200
  # define a "default value" to be used if the attribute's value on an
153
- # item is nil or not set at persistence time.
201
+ # item is nil or not set at persistence time. Additionally, lambda
202
+ # can be used as a default value.
154
203
  def integer_attr(name, opts = {})
155
204
  opts[:dynamodb_type] = "N"
156
205
  attr(name, Marshalers::IntegerMarshaler.new(opts), opts)
@@ -171,7 +220,8 @@ module Aws
171
220
  # nil values will be ignored and not persisted. By default, is false.
172
221
  # @option opts [Object] :default_value Optional attribute used to
173
222
  # define a "default value" to be used if the attribute's value on an
174
- # item is nil or not set at persistence time.
223
+ # item is nil or not set at persistence time. Additionally, lambda
224
+ # can be used as a default value.
175
225
  def float_attr(name, opts = {})
176
226
  opts[:dynamodb_type] = "N"
177
227
  attr(name, Marshalers::FloatMarshaler.new(opts), opts)
@@ -192,7 +242,8 @@ module Aws
192
242
  # nil values will be ignored and not persisted. By default, is false.
193
243
  # @option options [Object] :default_value Optional attribute used to
194
244
  # define a "default value" to be used if the attribute's value on an
195
- # item is nil or not set at persistence time.
245
+ # item is nil or not set at persistence time. Additionally, lambda
246
+ # can be used as a default value.
196
247
  def date_attr(name, opts = {})
197
248
  opts[:dynamodb_type] = "S"
198
249
  attr(name, Marshalers::DateMarshaler.new(opts), opts)
@@ -213,7 +264,8 @@ module Aws
213
264
  # nil values will be ignored and not persisted. By default, is false.
214
265
  # @option opts [Object] :default_value Optional attribute used to
215
266
  # define a "default value" to be used if the attribute's value on an
216
- # item is nil or not set at persistence time.
267
+ # item is nil or not set at persistence time. Additionally, lambda
268
+ # can be used as a default value.
217
269
  def datetime_attr(name, opts = {})
218
270
  opts[:dynamodb_type] = "S"
219
271
  attr(name, Marshalers::DateTimeMarshaler.new(opts), opts)
@@ -234,14 +286,15 @@ module Aws
234
286
  # nil values will be ignored and not persisted. By default, is false.
235
287
  # @option opts [Object] :default_value Optional attribute used to
236
288
  # define a "default value" to be used if the attribute's value on an
237
- # item is nil or not set at persistence time.
289
+ # item is nil or not set at persistence time. Additionally, lambda
290
+ # can be used as a default value.
238
291
  def time_attr(name, opts = {})
239
292
  opts[:dynamodb_type] = "S"
240
293
  attr(name, Marshalers::TimeMarshaler.new(opts), opts)
241
294
  end
242
295
 
243
296
  # Define a time-type attribute for your model which persists as
244
- # epoch-seconds.
297
+ # epoch-seconds.
245
298
  #
246
299
  # @param [Symbol] name Name of this attribute. It should be a name
247
300
  # that is safe to use as a method.
@@ -256,7 +309,8 @@ module Aws
256
309
  # nil values will be ignored and not persisted. By default, is false.
257
310
  # @option opts [Object] :default_value Optional attribute used to
258
311
  # define a "default value" to be used if the attribute's value on an
259
- # item is nil or not set at persistence time.
312
+ # item is nil or not set at persistence time. Additionally, lambda
313
+ # can be used as a default value.
260
314
  def epoch_time_attr(name, opts = {})
261
315
  opts[:dynamodb_type] = "N"
262
316
  attr(name, Marshalers::EpochTimeMarshaler.new(opts), opts)
@@ -265,7 +319,7 @@ module Aws
265
319
  # Define a list-type attribute for your model.
266
320
  #
267
321
  # Lists do not have to be homogeneous, but they do have to be types that
268
- # the AWS SDK for Ruby V2's DynamoDB client knows how to marshal and
322
+ # the AWS SDK for Ruby V3's DynamoDB client knows how to marshal and
269
323
  # unmarshal. Those types are:
270
324
  #
271
325
  # * Hash
@@ -291,7 +345,8 @@ module Aws
291
345
  # the range key for the table.
292
346
  # @option opts [Object] :default_value Optional attribute used to
293
347
  # define a "default value" to be used if the attribute's value on an
294
- # item is nil or not set at persistence time.
348
+ # item is nil or not set at persistence time. Additionally, lambda
349
+ # can be used as a default value.
295
350
  def list_attr(name, opts = {})
296
351
  opts[:dynamodb_type] = "L"
297
352
  attr(name, Marshalers::ListMarshaler.new(opts), opts)
@@ -300,7 +355,7 @@ module Aws
300
355
  # Define a map-type attribute for your model.
301
356
  #
302
357
  # Maps do not have to be homogeneous, but they do have to use types that
303
- # the AWS SDK for Ruby V2's DynamoDB client knows how to marshal and
358
+ # the AWS SDK for Ruby V3's DynamoDB client knows how to marshal and
304
359
  # unmarshal. Those types are:
305
360
  #
306
361
  # * Hash
@@ -326,7 +381,8 @@ module Aws
326
381
  # the range key for the table.
327
382
  # @option opts [Object] :default_value Optional attribute used to
328
383
  # define a "default value" to be used if the attribute's value on an
329
- # item is nil or not set at persistence time.
384
+ # item is nil or not set at persistence time. Additionally, lambda
385
+ # can be used as a default value.
330
386
  def map_attr(name, opts = {})
331
387
  opts[:dynamodb_type] = "M"
332
388
  attr(name, Marshalers::MapMarshaler.new(opts), opts)
@@ -351,7 +407,8 @@ module Aws
351
407
  # the range key for the table.
352
408
  # @option opts [Object] :default_value Optional attribute used to
353
409
  # define a "default value" to be used if the attribute's value on an
354
- # item is nil or not set at persistence time.
410
+ # item is nil or not set at persistence time. Additionally, lambda
411
+ # can be used as a default value.
355
412
  def string_set_attr(name, opts = {})
356
413
  opts[:dynamodb_type] = "SS"
357
414
  attr(name, Marshalers::StringSetMarshaler.new(opts), opts)
@@ -376,18 +433,90 @@ module Aws
376
433
  # the range key for the table.
377
434
  # @option opts [Object] :default_value Optional attribute used to
378
435
  # define a "default value" to be used if the attribute's value on an
379
- # item is nil or not set at persistence time.
436
+ # item is nil or not set at persistence time. Additionally, lambda
437
+ # can be used as a default value.
380
438
  def numeric_set_attr(name, opts = {})
381
439
  opts[:dynamodb_type] = "NS"
382
440
  attr(name, Marshalers::NumericSetMarshaler.new(opts), opts)
383
441
  end
384
442
 
443
+ # Define an atomic counter attribute for your model.
444
+ #
445
+ # Atomic counter are an integer-type attribute that is incremented,
446
+ # unconditionally, without interfering with other write requests.
447
+ # The numeric value increments each time you call +increment_<attr>!+.
448
+ # If a specific numeric value are passed in the call, the attribute will
449
+ # increment by that value.
450
+ #
451
+ # To use +increment_<attr>!+ method, the following condition must be true:
452
+ # * None of the attributes have dirty changes.
453
+ # * If there is a value passed in, it must be an integer.
454
+ # For more information, see
455
+ # {https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/WorkingWithItems.html#WorkingWithItems.AtomicCounters Atomic counter}
456
+ # in the Amazon DynamoDB Developer Guide.
457
+ #
458
+ # @param [Symbol] name Name of this attribute. It should be a name that
459
+ # is safe to use as a method.
460
+ # @param [Hash] opts
461
+ # @option opts [Object] :default_value Optional attribute used to
462
+ # define a "default value" to be used if the attribute's value on an
463
+ # item is nil or not set at persistence time. The "default value" of
464
+ # the attribute starts at 0.
465
+ #
466
+ # @example Usage Example
467
+ # class MyRecord
468
+ # include Aws::Record
469
+ # integer_attr :id, hash_key: true
470
+ # atomic_counter :counter
471
+ # end
472
+ #
473
+ # record = MyRecord.find(id: 1)
474
+ # record.counter #=> 0
475
+ # record.increment_counter! #=> 1
476
+ # record.increment_counter!(2) #=> 3
477
+ # @see #attr #attr method for additional hash options.
478
+ def atomic_counter(name, opts = {})
479
+ opts[:dynamodb_type] = "N"
480
+ opts[:default_value] ||= 0
481
+ attr(name, Marshalers::IntegerMarshaler.new(opts), opts)
482
+
483
+ define_method("increment_#{name}!") do |increment=1|
484
+
485
+ if dirty?
486
+ msg = "Attributes need to be saved before atomic counter can be incremented"
487
+ raise Errors::RecordError, msg
488
+ end
489
+
490
+ unless increment.is_a?(Integer)
491
+ msg = "expected an Integer value, got #{increment.class}"
492
+ raise ArgumentError, msg
493
+ end
494
+
495
+ resp = dynamodb_client.update_item({
496
+ table_name: self.class.table_name,
497
+ key: key_values,
498
+ expression_attribute_values: {
499
+ ":i" => increment
500
+ },
501
+ expression_attribute_names: {
502
+ "#n" => name
503
+ },
504
+ update_expression: "SET #n = #n + :i",
505
+ return_values: "UPDATED_NEW"
506
+ })
507
+ assign_attributes(resp[:attributes])
508
+ @data.clean!
509
+ @data.get_attribute(name)
510
+ end
511
+
512
+ end
513
+
385
514
  # @return [Symbol,nil] The symbolic name of the table's hash key.
386
515
  def hash_key
387
516
  @keys.hash_key
388
517
  end
389
518
 
390
- # @return [Symbol,nil] The symbloc name of the table's range key, or nil if there is no range key.
519
+ # @return [Symbol,nil] The symbolic name of the table's range key, or nil if there is no range key.
391
520
  def range_key
392
521
  @keys.range_key
393
522
  end
@@ -1,15 +1,4 @@
1
- # Copyright 2015-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
- #
3
- # Licensed under the Apache License, Version 2.0 (the "License"). You may not
4
- # use this file except in compliance with the License. A copy of the License is
5
- # located at
6
- #
7
- # http://aws.amazon.com/apache2.0/
8
- #
9
- # or in the "license" file accompanying this file. This file is distributed on
10
- # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11
- # or implied. See the License for the specific language governing permissions
12
- # and limitations under the License.
1
+ # frozen_string_literal: true
13
2
 
14
3
  module Aws
15
4
  module Record
@@ -17,6 +6,28 @@ module Aws
17
6
  extend ClientConfiguration
18
7
 
19
8
  class << self
9
+ # Provides a thin wrapper to the
10
+ # {https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/DynamoDB/Client.html#batch_write_item-instance_method Aws::DynamoDB::Client#batch_write_item}
11
+ # method. Up to 25 +PutItem+ or +DeleteItem+ operations are supported.
12
+ # A single request may write up to 16 MB of data, with each item having a
13
+ # write limit of 400 KB.
14
+ #
15
+ # *Note*: this operation does not support dirty attribute handling,
16
+ # nor does it enforce safe write operations (i.e. update vs new record
17
+ # checks).
18
+ #
19
+ # This call may partially execute write operations. Failed operations
20
+ # are returned as {BatchWrite.unprocessed_items unprocessed_items} (i.e. the
21
+ # table fails to meet requested write capacity). Any unprocessed
22
+ # items may be retried by calling {BatchWrite.execute! .execute!}
23
+ # again. You can determine if the request needs to be retried by calling
24
+ # the {BatchWrite.complete? .complete?} method - which returns +true+
25
+ # when all operations have been completed.
26
+ #
27
+ # Please see
28
+ # {https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Programming.Errors.html#Programming.Errors.BatchOperations Batch Operations and Error Handling}
29
+ # in the DynamoDB Developer Guide for more details.
30
+ #
20
31
  # @example Usage Example
21
32
  # class Breakfast
22
33
  # include Aws::Record
@@ -38,29 +49,7 @@ module Aws
38
49
  # end
39
50
  #
40
51
  # # unprocessed items can be retried by calling Aws::Record::BatchWrite#execute!
41
- # operation.execute! unless operation.complete?
42
- #
43
- # Provides a thin wrapper to the
44
- # {https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/DynamoDB/Client.html#batch_write_item-instance_method Aws::DynamoDB::Client#batch_write_item}
45
- # method. Up to 25 +PutItem+ or +DeleteItem+ operations are supported.
46
- # A single rquest may write up to 16 MB of data, with each item having a
47
- # write limit of 400 KB.
48
- #
49
- # *Note*: this operation does not support dirty attribute handling,
50
- # nor does it enforce safe write operations (i.e. update vs new record
51
- # checks).
52
- #
53
- # This call may partially execute write operations. Failed operations
54
- # are returned as +Aws::Record::BatchWrite#unprocessed_items+ (i.e. the
55
- # table fails to meet requested write capacity). Any unprocessed
56
- # items may be retried by calling +Aws::Record::BatchWrite#execute!+
57
- # again. You can determine if the request needs to be retried by calling
58
- # the +Aws::Record::BatchWrite#complete?+ method - which returns +true+
59
- # when all operations have been completed.
60
- #
61
- # Please see
62
- # {https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Programming.Errors.html#Programming.Errors.BatchOperations Batch Operations and Error Handling}
63
- # in the DynamoDB Developer Guide for more details.
52
+ # operation.execute! until operation.complete?
64
53
  #
65
54
  # @param [Hash] opts the options you wish to use to create the client.
66
55
  # Note that if you include the option +:client+, all other options
@@ -76,6 +65,81 @@ module Aws
76
65
  block.call(batch)
77
66
  batch.execute!
78
67
  end
68
+
69
+ # Provides support for the
70
+ # {https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/DynamoDB/Client.html#batch_get_item-instance_method
71
+ # Aws::DynamoDB::Client#batch_get_item} for aws-record models.
72
+ #
73
+ # +Aws::Record::Batch+ is Enumerable and using Enumerable methods will handle
74
+ # paging through all requested keys automatically. Alternatively, a lower level
75
+ # interface is available. You can determine if there are any unprocessed keys by calling
76
+ # {BatchRead.complete? .complete?} and any unprocessed keys can be processed by
77
+ # calling {BatchRead.execute! .execute!}. You can access all processed items
78
+ # through {BatchRead.items .items}.
79
+ #
80
+ # The +batch_get_item+ supports up to 100 operations in a single call and a single
81
+ # operation can retrieve up to 16 MB of data.
82
+ #
83
+ # +Aws::Record::BatchRead+ can take more than 100 item keys. The first 100 requests
84
+ # will be processed and the remaining requests will be stored.
85
+ # When using Enumerable methods, any pending item keys will be automatically
86
+ # processed and the new items will be added to +items+.
87
+ # Alternately, use {BatchRead.execute! .execute!} to process any pending item keys.
88
+ #
89
+ # All processed operations can be accessed by {BatchRead.items items} - which is an
90
+ # array of modeled items from the response. The items will be unordered since
91
+ # DynamoDB does not return items in any particular order.
92
+ #
93
+ # If a requested item does not exist in the database, it is not returned in the response.
94
+ #
95
+ # If there is a returned item from the call and there's no reference model class
96
+ # to be found, the item will not show up under +items+.
97
+ #
98
+ # @example Usage Example
99
+ # class Lunch
100
+ # include Aws::Record
101
+ # integer_attr :id, hash_key: true
102
+ # string_attr :name, range_key: true
103
+ # end
104
+ #
105
+ # class Dessert
106
+ # include Aws::Record
107
+ # integer_attr :id, hash_key: true
108
+ # string_attr :name, range_key: true
109
+ # end
110
+ #
111
+ # # batch operations
112
+ # operation = Aws::Record::Batch.read do |db|
113
+ # db.find(Lunch, id: 1, name: 'Papaya Salad')
114
+ # db.find(Lunch, id: 2, name: 'BLT Sandwich')
115
+ # db.find(Dessert, id: 1, name: 'Apple Pie')
116
+ # end
117
+ #
118
+ # # BatchRead is enumerable and handles pagination
119
+ # operation.each { |item| item.id }
120
+ #
121
+ # # Alternatively, BatchRead provides a lower level
122
+ # # interface through: execute!, complete? and items.
123
+ # # Unprocessed items can be processed by calling:
124
+ # operation.execute! until operation.complete?
125
+ #
126
+ # @param [Hash] opts the options you wish to use to create the client.
127
+ # Note that if you include the option +:client+, all other options
128
+ # will be ignored. See the documentation for other options in the
129
+ # {https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/DynamoDB/Client.html#initialize-instance_method
130
+ # AWS SDK for Ruby}.
131
+ # @option opts [Aws::DynamoDB::Client] :client allows you to pass in your
132
+ # own pre-configured client.
133
+ # @return [Aws::Record::BatchRead] An instance that contains modeled items
134
+ # from the +BatchGetItem+ result and stores unprocessed keys to be
135
+ # manually processed later.
136
+ def read(opts = {}, &block)
137
+ batch = BatchRead.new(client: _build_client(opts))
138
+ block.call(batch)
139
+ batch.execute!
140
+ batch
141
+ end
142
+
79
143
  end
80
144
  end
81
145
  end
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aws
4
+ module Record
5
+ class BatchRead
6
+ include Enumerable
7
+
8
+ # @api private
9
+ BATCH_GET_ITEM_LIMIT = 100
10
+
11
+ # @param [Aws::DynamoDB::Client] client the DynamoDB SDK client.
12
+ def initialize(opts = {})
13
+ @client = opts[:client]
14
+ end
15
+
16
+ # Append the item keys to a batch read request.
17
+ #
18
+ # See {Batch.read} for example usage.
19
+ # @param [Aws::Record] klass a model class that includes {Aws::Record}
20
+ # @param [Hash] key attribute-value pairs for the key you wish to search for.
21
+ # @raise [Aws::Record::Errors::KeyMissing] if your option parameters
22
+ # do not include all item keys defined in the model.
23
+ # @raise [ArgumentError] if the provided item keys is a duplicate request
24
+ # in the same instance.
25
+ def find(klass, key = {})
26
+ unprocessed_key = format_unprocessed_key(klass, key)
27
+ store_unprocessed_key(klass, unprocessed_key)
28
+ store_item_class(klass, unprocessed_key)
29
+ end
30
+
31
+ # Perform a +batch_get_item+ request.
32
+ #
33
+ # This method processes the first 100 item keys and
34
+ # returns an array of new modeled items.
35
+ #
36
+ # See {Batch.read} for example usage.
37
+ # @return [Array] an array of unordered new items
38
+ def execute!
39
+ operation_keys = unprocessed_keys[0..BATCH_GET_ITEM_LIMIT - 1]
40
+ @unprocessed_keys = unprocessed_keys[BATCH_GET_ITEM_LIMIT..-1] || []
41
+
42
+ operations = build_operations(operation_keys)
43
+ result = @client.batch_get_item(request_items: operations)
44
+ new_items = build_items(result.responses)
45
+ items.concat(new_items)
46
+
47
+ unless result.unprocessed_keys.nil?
48
+ update_unprocessed_keys(result.unprocessed_keys)
49
+ end
50
+
51
+ new_items
52
+ end
53
+
54
+ # Provides an enumeration of the results from the +batch_get_item+ request
55
+ # and handles pagination.
56
+ #
57
+ # Any pending item keys will be automatically processed and be
58
+ # added to the {#items}.
59
+ #
60
+ # See {Batch.read} for example usage.
61
+ # @yieldparam [Aws::Record] item a modeled item
62
+ # @return [Enumerable<BatchRead>] an enumeration over the results of
63
+ # +batch_get_item+ request.
64
+ def each
65
+ return enum_for(:each) unless block_given?
66
+
67
+ @items.each { |item| yield item }
68
+ until complete?
69
+ new_items = execute!
70
+ new_items.each { |new_item| yield new_item }
71
+ end
72
+ end
73
+
74
+ # Indicates if all item keys have been processed.
75
+ #
76
+ # See {Batch.read} for example usage.
77
+ # @return [Boolean] +true+ if all item keys has been processed, +false+ otherwise.
78
+ def complete?
79
+ unprocessed_keys.none?
80
+ end
81
+
82
+ # Returns an array of modeled items. The items are marshaled into classes used in {#find} method.
83
+ # These items will be unordered since DynamoDB does not return items in any particular order.
84
+ #
85
+ # See {Batch.read} for example usage.
86
+ # @return [Array] an array of modeled items from the +batch_get_item+ call.
87
+ def items
88
+ @items ||= []
89
+ end
90
+
91
+ private
92
+
93
+ def unprocessed_keys
94
+ @unprocessed_keys ||= []
95
+ end
96
+
97
+ def item_classes
98
+ @item_classes ||= Hash.new { |h, k| h[k] = [] }
99
+ end
100
+
101
+ def format_unprocessed_key(klass, key)
102
+ item_key = {}
103
+ attributes = klass.attributes
104
+ klass.keys.each_value do |attr_sym|
105
+ unless key[attr_sym]
106
+ raise Errors::KeyMissing, "Missing required key #{attr_sym} in #{key}"
107
+ end
108
+
109
+ attr_name = attributes.storage_name_for(attr_sym)
110
+ item_key[attr_name] = attributes.attribute_for(attr_sym)
111
+ .serialize(key[attr_sym])
112
+ end
113
+ item_key
114
+ end
115
+
116
+ def store_unprocessed_key(klass, unprocessed_key)
117
+ unprocessed_keys << { keys: unprocessed_key, table_name: klass.table_name }
118
+ end
119
+
120
+ def store_item_class(klass, key)
121
+ if item_classes.include?(klass.table_name)
122
+ item_classes[klass.table_name].each do |item|
123
+ if item[:keys] == key && item[:class] != klass
124
+ raise ArgumentError, 'Provided item keys is a duplicate request'
125
+ end
126
+ end
127
+ end
128
+ item_classes[klass.table_name] << { keys: key, class: klass }
129
+ end
130
+
131
+ def build_operations(keys)
132
+ operations = Hash.new { |h, k| h[k] = { keys: [] } }
133
+ keys.each do |key|
134
+ operations[key[:table_name]][:keys] << key[:keys]
135
+ end
136
+ operations
137
+ end
138
+
139
+ def build_items(item_responses)
140
+ new_items = []
141
+ item_responses.each do |table, unprocessed_items|
142
+ unprocessed_items.each do |item|
143
+ item_class = find_item_class(table, item)
144
+ if item_class.nil? && @client.config.logger
145
+ @client.config.logger.warn(
146
+ 'Unexpected response from service.'\
147
+ "Received: #{item}. Skipping above item and continuing"
148
+ )
149
+ else
150
+ new_items << build_item(item, item_class)
151
+ end
152
+ end
153
+ end
154
+ new_items
155
+ end
156
+
157
+ def update_unprocessed_keys(keys)
158
+ keys.each do |table_name, table_values|
159
+ table_values.keys.each do |key|
160
+ unprocessed_keys << { keys: key, table_name: table_name }
161
+ end
162
+ end
163
+ end
164
+
165
+ def find_item_class(table, item)
166
+ selected_item = item_classes[table].find { |item_info| contains_keys?(item, item_info[:keys]) }
167
+ selected_item[:class] if selected_item
168
+ end
169
+
170
+ def contains_keys?(item, keys)
171
+ item.merge(keys) == item
172
+ end
173
+
174
+ def build_item(item, item_class)
175
+ new_item_opts = {}
176
+ item.each do |db_name, value|
177
+ name = item_class.attributes.db_to_attribute_name(db_name)
178
+ new_item_opts[name] = value
179
+ end
180
+ item = item_class.new(new_item_opts)
181
+ item.clean!
182
+ item
183
+ end
184
+ end
185
+ end
186
+ end