dynamoid 3.2.0 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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