aws-record 2.7.0 → 2.10.1

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 (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