dynamoid 3.2.0 → 3.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +111 -1
  3. data/README.md +580 -241
  4. data/lib/dynamoid.rb +2 -0
  5. data/lib/dynamoid/adapter.rb +15 -15
  6. data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +82 -102
  7. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/batch_get_item.rb +108 -0
  8. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb +29 -16
  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 +2 -2
  11. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/limit.rb +2 -3
  12. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/start_key.rb +2 -2
  13. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +15 -6
  14. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +15 -5
  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 +5 -3
  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 +8 -3
  27. data/lib/dynamoid/config.rb +16 -3
  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 +2 -1
  32. data/lib/dynamoid/criteria/chain.rb +418 -46
  33. data/lib/dynamoid/criteria/ignored_conditions_detector.rb +3 -3
  34. data/lib/dynamoid/criteria/key_fields_detector.rb +109 -32
  35. data/lib/dynamoid/criteria/nonexistent_fields_detector.rb +3 -2
  36. data/lib/dynamoid/criteria/overwritten_conditions_detector.rb +1 -1
  37. data/lib/dynamoid/dirty.rb +239 -32
  38. data/lib/dynamoid/document.rb +130 -251
  39. data/lib/dynamoid/dumping.rb +9 -0
  40. data/lib/dynamoid/dynamodb_time_zone.rb +1 -0
  41. data/lib/dynamoid/fields.rb +246 -20
  42. data/lib/dynamoid/finders.rb +69 -32
  43. data/lib/dynamoid/identity_map.rb +6 -0
  44. data/lib/dynamoid/indexes.rb +76 -17
  45. data/lib/dynamoid/loadable.rb +31 -0
  46. data/lib/dynamoid/log/formatter.rb +26 -0
  47. data/lib/dynamoid/middleware/identity_map.rb +1 -0
  48. data/lib/dynamoid/persistence.rb +592 -122
  49. data/lib/dynamoid/persistence/import.rb +73 -0
  50. data/lib/dynamoid/persistence/save.rb +64 -0
  51. data/lib/dynamoid/persistence/update_fields.rb +63 -0
  52. data/lib/dynamoid/persistence/upsert.rb +60 -0
  53. data/lib/dynamoid/primary_key_type_mapping.rb +1 -0
  54. data/lib/dynamoid/railtie.rb +1 -0
  55. data/lib/dynamoid/tasks.rb +3 -1
  56. data/lib/dynamoid/tasks/database.rb +1 -0
  57. data/lib/dynamoid/type_casting.rb +12 -2
  58. data/lib/dynamoid/undumping.rb +8 -0
  59. data/lib/dynamoid/validations.rb +2 -0
  60. data/lib/dynamoid/version.rb +1 -1
  61. metadata +49 -71
  62. data/.coveralls.yml +0 -1
  63. data/.document +0 -5
  64. data/.gitignore +0 -74
  65. data/.rspec +0 -2
  66. data/.rubocop.yml +0 -71
  67. data/.rubocop_todo.yml +0 -55
  68. data/.travis.yml +0 -41
  69. data/Appraisals +0 -28
  70. data/Gemfile +0 -8
  71. data/Rakefile +0 -46
  72. data/Vagrantfile +0 -29
  73. data/docker-compose.yml +0 -7
  74. data/dynamoid.gemspec +0 -57
  75. data/gemfiles/rails_4_2.gemfile +0 -11
  76. data/gemfiles/rails_5_0.gemfile +0 -10
  77. data/gemfiles/rails_5_1.gemfile +0 -10
  78. data/gemfiles/rails_5_2.gemfile +0 -10
@@ -30,6 +30,7 @@ require 'dynamoid/criteria'
30
30
  require 'dynamoid/finders'
31
31
  require 'dynamoid/identity_map'
32
32
  require 'dynamoid/config'
33
+ require 'dynamoid/loadable'
33
34
  require 'dynamoid/components'
34
35
  require 'dynamoid/document'
35
36
  require 'dynamoid/adapter'
@@ -56,6 +57,7 @@ module Dynamoid
56
57
  @included_models ||= []
57
58
  end
58
59
 
60
+ # @private
59
61
  def adapter
60
62
  @adapter ||= Adapter.new
61
63
  end
@@ -3,6 +3,7 @@
3
3
  # require only 'concurrent/atom' once this issue is resolved:
4
4
  # https://github.com/ruby-concurrency/concurrent-ruby/pull/377
5
5
  require 'concurrent'
6
+ require 'dynamoid/adapter_plugin/aws_sdk_v3'
6
7
 
7
8
  # encoding: utf-8
8
9
  module Dynamoid
@@ -10,6 +11,7 @@ module Dynamoid
10
11
  # 1) For the rest of Dynamoid, the gateway to DynamoDB.
11
12
  # 2) Allows switching `config.adapter` to ease development of a new adapter.
12
13
  # 3) Caches the list of tables Dynamoid knows about.
14
+ # @private
13
15
  class Adapter
14
16
  def initialize
15
17
  @adapter_ = Concurrent::Atom.new(nil)
@@ -29,7 +31,7 @@ module Dynamoid
29
31
  def adapter
30
32
  unless @adapter_.value
31
33
  adapter = self.class.adapter_plugin_class.new
32
- adapter.connect! if adapter.respond_to?(:connect!)
34
+ adapter.connect!
33
35
  @adapter_.compare_and_set(nil, adapter)
34
36
  clear_cache!
35
37
  end
@@ -77,8 +79,8 @@ module Dynamoid
77
79
  #
78
80
  # @param [String] table the name of the table to write the object to
79
81
  # @param [Array] ids to fetch, can also be a string of just one id
80
- # @param [Hash] options: Passed to the underlying query. The :range_key option is required whenever the table has a range key,
81
- # unless multiple ids are passed in.
82
+ # @param [Hash] options Passed to the underlying query. The :range_key option is required whenever the table has a range key,
83
+ # unless multiple ids are passed in.
82
84
  #
83
85
  # @since 0.2.0
84
86
  def read(table, ids, options = {}, &blk)
@@ -93,7 +95,9 @@ module Dynamoid
93
95
  #
94
96
  # @param [String] table the name of the table to write the object to
95
97
  # @param [Array] ids to delete, can also be a string of just one id
96
- # @param [Array] range_key of the record to delete, can also be a string of just one range_key
98
+ # @param [Hash] options allowed only +range_key+ - range key or array of
99
+ # range keys of the record to delete, can also be
100
+ # a string of just one range_key, and +conditions+
97
101
  #
98
102
  def delete(table, ids, options = {})
99
103
  range_key = options[:range_key] # array of range keys that matches the ids passed in
@@ -114,7 +118,7 @@ module Dynamoid
114
118
  # Scans a table. Generally quite slow; try to avoid using scan if at all possible.
115
119
  #
116
120
  # @param [String] table the name of the table to write the object to
117
- # @param [Hash] scan_hash a hash of attributes: matching records will be returned by the scan
121
+ # @param [Hash] query a hash of attributes: matching records will be returned by the scan
118
122
  #
119
123
  # @since 0.2.0
120
124
  def scan(table, query = {}, opts = {})
@@ -123,8 +127,12 @@ module Dynamoid
123
127
 
124
128
  def create_table(table_name, key, options = {})
125
129
  unless tables.include?(table_name)
126
- benchmark('Create Table') { adapter.create_table(table_name, key, options) }
130
+ result = nil
131
+ benchmark('Create Table') { result = adapter.create_table(table_name, key, options) }
127
132
  tables << table_name
133
+ result
134
+ else
135
+ false
128
136
  end
129
137
  end
130
138
 
@@ -142,11 +150,7 @@ module Dynamoid
142
150
  #
143
151
  # @since 0.2.0
144
152
  define_method(m) do |*args, &blk|
145
- if blk.present?
146
- benchmark(m.to_s, *args) { adapter.send(m, *args, &blk) }
147
- else
148
- benchmark(m.to_s, *args) { adapter.send(m, *args) }
149
- end
153
+ benchmark(m.to_s, *args) { adapter.send(m, *args, &blk) }
150
154
  end
151
155
  end
152
156
 
@@ -179,10 +183,6 @@ module Dynamoid
179
183
  end
180
184
 
181
185
  def self.adapter_plugin_class
182
- unless Dynamoid.const_defined?(:AdapterPlugin) && Dynamoid::AdapterPlugin.const_defined?(Dynamoid::Config.adapter.camelcase)
183
- require "dynamoid/adapter_plugin/#{Dynamoid::Config.adapter}"
184
- end
185
-
186
186
  Dynamoid::AdapterPlugin.const_get(Dynamoid::Config.adapter.camelcase)
187
187
  end
188
188
  end
@@ -3,11 +3,13 @@
3
3
  require_relative 'aws_sdk_v3/query'
4
4
  require_relative 'aws_sdk_v3/scan'
5
5
  require_relative 'aws_sdk_v3/create_table'
6
+ require_relative 'aws_sdk_v3/batch_get_item'
6
7
  require_relative 'aws_sdk_v3/item_updater'
7
8
  require_relative 'aws_sdk_v3/table'
8
9
  require_relative 'aws_sdk_v3/until_past_table_status'
9
10
 
10
11
  module Dynamoid
12
+ # @private
11
13
  module AdapterPlugin
12
14
  # The AwsSdkV3 adapter provides support for the aws-sdk version 2 for ruby.
13
15
  class AwsSdkV3
@@ -22,8 +24,6 @@ module Dynamoid
22
24
  range_eq: 'EQ'
23
25
  }.freeze
24
26
 
25
- # Don't implement NULL and NOT_NULL because it doesn't make seanse -
26
- # we declare schema in models
27
27
  FIELD_MAP = {
28
28
  eq: 'EQ',
29
29
  ne: 'NE',
@@ -35,7 +35,9 @@ module Dynamoid
35
35
  between: 'BETWEEN',
36
36
  in: 'IN',
37
37
  contains: 'CONTAINS',
38
- not_contains: 'NOT_CONTAINS'
38
+ not_contains: 'NOT_CONTAINS',
39
+ null: 'NULL',
40
+ not_null: 'NOT_NULL',
39
41
  }.freeze
40
42
  HASH_KEY = 'HASH'
41
43
  RANGE_KEY = 'RANGE'
@@ -60,6 +62,25 @@ module Dynamoid
60
62
 
61
63
  attr_reader :table_cache
62
64
 
65
+ # Build an array of values for Condition
66
+ # Is used in ScanFilter and QueryFilter
67
+ # https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Condition.html
68
+ # @param [String] operator value of RANGE_MAP or FIELD_MAP hash, e.g. "EQ", "LT" etc
69
+ # @param [Object] value scalar value or array/set
70
+ def self.attribute_value_list(operator, value)
71
+ # For BETWEEN and IN operators we should keep value as is (it should be already an array)
72
+ # NULL and NOT_NULL require absence of attribute list
73
+ # For all the other operators we wrap the value with array
74
+ # https://docs.aws.amazon.com/en_us/amazondynamodb/latest/developerguide/LegacyConditionalParameters.Conditions.html
75
+ if %w[BETWEEN IN].include?(operator)
76
+ [value].flatten
77
+ elsif %w[NULL NOT_NULL].include?(operator)
78
+ nil
79
+ else
80
+ [value]
81
+ end
82
+ end
83
+
63
84
  # Establish the connection to DynamoDB.
64
85
  #
65
86
  # @return [Aws::DynamoDB::Client] the DynamoDB connection
@@ -74,19 +95,28 @@ module Dynamoid
74
95
  (Dynamoid::Config.settings.compact.keys & CONNECTION_CONFIG_OPTIONS).each do |option|
75
96
  @connection_hash[option] = Dynamoid::Config.send(option)
76
97
  end
77
- if Dynamoid::Config.access_key?
78
- @connection_hash[:access_key_id] = Dynamoid::Config.access_key
79
- end
80
- if Dynamoid::Config.secret_key?
81
- @connection_hash[:secret_access_key] = Dynamoid::Config.secret_key
98
+
99
+ # if credentials are passed, they already contain access key & secret key
100
+ if Dynamoid::Config.credentials?
101
+ @connection_hash[:credentials] = Dynamoid::Config.credentials
102
+ else
103
+ # otherwise, pass access key & secret key for credentials creation
104
+ if Dynamoid::Config.access_key?
105
+ @connection_hash[:access_key_id] = Dynamoid::Config.access_key
106
+ end
107
+ if Dynamoid::Config.secret_key?
108
+ @connection_hash[:secret_access_key] = Dynamoid::Config.secret_key
109
+ end
82
110
  end
83
111
 
84
- # https://github.com/aws/aws-sdk-ruby/blob/master/gems/aws-sdk-core/lib/aws-sdk-core/plugins/logging.rb
85
- # https://github.com/aws/aws-sdk-ruby/blob/master/gems/aws-sdk-core/lib/aws-sdk-core/log/formatter.rb
86
- formatter = Aws::Log::Formatter.new(':operation | Request :http_request_body | Response :http_response_body')
87
112
  @connection_hash[:logger] = Dynamoid::Config.logger
88
113
  @connection_hash[:log_level] = :debug
89
- @connection_hash[:log_formatter] = formatter
114
+
115
+ # https://github.com/aws/aws-sdk-ruby/blob/master/gems/aws-sdk-core/lib/aws-sdk-core/plugins/logging.rb
116
+ # https://github.com/aws/aws-sdk-ruby/blob/master/gems/aws-sdk-core/lib/aws-sdk-core/log/formatter.rb
117
+ if Dynamoid::Config.log_formatter
118
+ @connection_hash[:log_formatter] = Dynamoid::Config.log_formatter
119
+ end
90
120
 
91
121
  @connection_hash
92
122
  end
@@ -114,9 +144,9 @@ module Dynamoid
114
144
  # end
115
145
  #
116
146
  # @param [String] table_name the name of the table
117
- # @param [Array] items to be processed
118
- # @param [Hash] additional options
119
- # @param [Proc] optional block
147
+ # @param [Array] objects to be processed
148
+ # @param [Hash] options additional options
149
+ # @yield [true|false] invokes an optional block with argument - whether there are unprocessed items
120
150
  #
121
151
  # See:
122
152
  # * http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchWriteItem.html
@@ -171,9 +201,9 @@ module Dynamoid
171
201
  # end
172
202
  # end
173
203
  #
174
- # @param [Hash] table_ids the hash of tables and IDs to retrieve
204
+ # @param [Hash] table_names_with_ids the hash of tables and IDs to retrieve
175
205
  # @param [Hash] options to be passed to underlying BatchGet call
176
- # @param [Proc] optional block can be passed to handle each batch of items
206
+ # @param [Proc] block optional block can be passed to handle each batch of items
177
207
  #
178
208
  # @return [Hash] a hash where keys are the table names and the values are the retrieved items
179
209
  #
@@ -183,65 +213,11 @@ module Dynamoid
183
213
  # @since 1.0.0
184
214
  #
185
215
  # @todo: Provide support for passing options to underlying batch_get_item
186
- def batch_get_item(table_ids, options = {})
187
- request_items = Hash.new { |h, k| h[k] = [] }
188
- return request_items if table_ids.all? { |_k, v| v.blank? }
189
-
190
- ret = Hash.new([].freeze) # Default for tables where no rows are returned
191
-
192
- table_ids.each do |t, ids|
193
- next if ids.blank?
194
-
195
- ids = Array(ids).dup
196
- tbl = describe_table(t)
197
- hk = tbl.hash_key.to_s
198
- rng = tbl.range_key.to_s
199
-
200
- while ids.present?
201
- batch = ids.shift(Dynamoid::Config.batch_size)
202
-
203
- request_items = Hash.new { |h, k| h[k] = [] }
204
-
205
- keys = if rng.present?
206
- Array(batch).map do |h, r|
207
- { hk => h, rng => r }
208
- end
209
- else
210
- Array(batch).map do |id|
211
- { hk => id }
212
- end
213
- end
214
-
215
- request_items[t] = {
216
- keys: keys,
217
- consistent_read: options[:consistent_read]
218
- }
219
-
220
- results = client.batch_get_item(
221
- request_items: request_items
222
- )
223
-
224
- if block_given?
225
- batch_results = Hash.new([].freeze)
226
-
227
- results.data[:responses].each do |table, rows|
228
- batch_results[table] += rows.collect { |r| result_item_to_hash(r) }
229
- end
230
-
231
- yield(batch_results, results.unprocessed_keys.present?)
232
- else
233
- results.data[:responses].each do |table, rows|
234
- ret[table] += rows.collect { |r| result_item_to_hash(r) }
235
- end
236
- end
237
-
238
- if results.unprocessed_keys.present?
239
- ids += results.unprocessed_keys[t].keys.map { |h| h[hk] }
240
- end
241
- end
216
+ def batch_get_item(table_names_with_ids, options = {}, &block)
217
+ tables_with_ids = table_names_with_ids.transform_keys do |name|
218
+ describe_table(name)
242
219
  end
243
-
244
- ret unless block_given?
220
+ BatchGetItem.new(client, tables_with_ids, options).call(&block)
245
221
  end
246
222
 
247
223
  # Delete many items at once from DynamoDB. More efficient than delete each item individually.
@@ -299,8 +275,22 @@ module Dynamoid
299
275
  def create_table(table_name, key = :id, options = {})
300
276
  Dynamoid.logger.info "Creating #{table_name} table. This could take a while."
301
277
  CreateTable.new(client, table_name, key, options).call
278
+ true
302
279
  rescue Aws::DynamoDB::Errors::ResourceInUseException => e
303
280
  Dynamoid.logger.error "Table #{table_name} cannot be created as it already exists"
281
+ false
282
+ end
283
+
284
+ def update_time_to_live(table_name:, attribute:)
285
+ request = {
286
+ table_name: table_name,
287
+ time_to_live_specification: {
288
+ attribute_name: attribute,
289
+ enabled: true,
290
+ }
291
+ }
292
+
293
+ client.update_time_to_live(request)
304
294
  end
305
295
 
306
296
  # Create a table on DynamoDB *synchronously*.
@@ -356,9 +346,14 @@ module Dynamoid
356
346
  # @since 1.0.0
357
347
  def delete_table(table_name, options = {})
358
348
  resp = client.delete_table(table_name: table_name)
359
- UntilPastTableStatus.new(table_name, :deleting).call if options[:sync] &&
360
- (status = PARSE_TABLE_STATUS.call(resp, :table_description)) &&
361
- status == TABLE_STATUSES[:deleting]
349
+
350
+ if options[:sync]
351
+ status = PARSE_TABLE_STATUS.call(resp, :table_description)
352
+ if status == TABLE_STATUSES[:deleting]
353
+ UntilPastTableStatus.new(client, table_name, :deleting).call
354
+ end
355
+ end
356
+
362
357
  table_cache.delete(table_name)
363
358
  rescue Aws::DynamoDB::Errors::ResourceInUseException => e
364
359
  Dynamoid.logger.error "Table #{table_name} cannot be deleted as it is in use"
@@ -552,11 +547,11 @@ module Dynamoid
552
547
  hk = table.hash_key
553
548
  rk = table.range_key
554
549
 
555
- scan(table_name, {}, {}).flat_map{ |i| i }.each do |attributes|
556
- opts = {}
557
- opts[:range_key] = attributes[rk.to_sym] if rk
558
- delete_item(table_name, attributes[hk], opts)
550
+ ids = scan(table_name, {}, {}).flat_map { |i| i }.map do |attributes|
551
+ rk ? [attributes[hk], attributes[rk.to_sym]] : attributes[hk]
559
552
  end
553
+
554
+ batch_delete_item(table_name => ids)
560
555
  end
561
556
 
562
557
  def count(table_name)
@@ -614,28 +609,13 @@ module Dynamoid
614
609
  end
615
610
  end
616
611
 
617
- # Build an array of values for Condition
618
- # Is used in ScanFilter and QueryFilter
619
- # https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Condition.html
620
- # @params [String] operator: value of RANGE_MAP or FIELD_MAP hash, e.g. "EQ", "LT" etc
621
- # @params [Object] value: scalar value or array/set
622
- def attribute_value_list(operator, value)
623
- self.class.attribute_value_list(operator, value)
624
- end
625
-
626
- def self.attribute_value_list(operator, value)
627
- # For BETWEEN and IN operators we should keep value as is (it should be already an array)
628
- # For all the other operators we wrap the value with array
629
- if %w[BETWEEN IN].include?(operator)
630
- [value].flatten
631
- else
632
- [value]
633
- end
634
- end
635
-
636
612
  def sanitize_item(attributes)
613
+ config_value = Dynamoid.config.store_attribute_with_nil_value
614
+ store_attribute_with_nil_value = config_value.nil? ? false : !!config_value
615
+
637
616
  attributes.reject do |_, v|
638
- v.nil? || ((v.is_a?(Set) || v.is_a?(String)) && v.empty?)
617
+ ((v.is_a?(Set) || v.is_a?(String)) && v.empty?) ||
618
+ (!store_attribute_with_nil_value && v.nil?)
639
619
  end.transform_values do |v|
640
620
  v.is_a?(Hash) ? v.stringify_keys : v
641
621
  end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dynamoid
4
+ # @private
5
+ module AdapterPlugin
6
+ class AwsSdkV3
7
+ # Documentation
8
+ # https://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Client.html#batch_get_item-instance_method
9
+ class BatchGetItem
10
+ attr_reader :client, :tables_with_ids, :options
11
+
12
+ def initialize(client, tables_with_ids, options = {})
13
+ @client = client
14
+ @tables_with_ids = tables_with_ids
15
+ @options = options
16
+ end
17
+
18
+ def call
19
+ results = {}
20
+
21
+ tables_with_ids.each do |table, ids|
22
+ if ids.blank?
23
+ results[table.name] = []
24
+ next
25
+ end
26
+
27
+ ids = Array(ids).dup
28
+
29
+ while ids.present?
30
+ batch = ids.shift(Dynamoid::Config.batch_size)
31
+ request = build_request(table, batch)
32
+ api_response = client.batch_get_item(request)
33
+ response = Response.new(api_response)
34
+
35
+ if block_given?
36
+ # return batch items as a result
37
+ batch_results = Hash.new([].freeze)
38
+ batch_results.update(response.items_grouped_by_table)
39
+
40
+ yield(batch_results, response.successful_partially?)
41
+ else
42
+ # collect all the batches to return at the end
43
+ results.update(response.items_grouped_by_table) { |_, its1, its2| its1 + its2 }
44
+ end
45
+
46
+ if response.successful_partially?
47
+ ids += response.unprocessed_ids(table)
48
+ end
49
+ end
50
+ end
51
+
52
+ results unless block_given?
53
+ end
54
+
55
+ private
56
+
57
+ def build_request(table, ids)
58
+ ids = Array(ids)
59
+
60
+ keys = if table.range_key.nil?
61
+ ids.map { |hk| { table.hash_key => hk } }
62
+ else
63
+ ids.map { |hk, rk| { table.hash_key => hk, table.range_key => rk } }
64
+ end
65
+
66
+ {
67
+ request_items: {
68
+ table.name => {
69
+ keys: keys,
70
+ consistent_read: options[:consistent_read]
71
+ }
72
+ }
73
+ }
74
+ end
75
+
76
+ # Helper class to work with response
77
+ class Response
78
+ def initialize(api_response)
79
+ @api_response = api_response
80
+ end
81
+
82
+ def successful_partially?
83
+ @api_response.unprocessed_keys.present?
84
+ end
85
+
86
+ def unprocessed_ids(table)
87
+ # unprocessed_keys Hash contains as values instances of
88
+ # Aws::DynamoDB::Types::KeysAndAttributes
89
+ @api_response.unprocessed_keys[table.name].keys.map { |h| h[table.hash_key.to_s] }
90
+ end
91
+
92
+ def items_grouped_by_table
93
+ # data[:responses] is a Hash[table_name -> items]
94
+ @api_response.data[:responses].transform_values do |items|
95
+ items.map(&method(:item_to_hash))
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ def item_to_hash(item)
102
+ item.symbolize_keys
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end