aws-record 2.1.2 → 2.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7b1ff4831b48174ce4404f6f24108ee85b11a104da96aa5176a218fdfeb8dcb8
4
- data.tar.gz: 0610b848e4705bbbb013e8833b8b54f701f100a617714ae9f5bc7891f2d63a99
3
+ metadata.gz: c259c949c94ace6740e388e2e37fa10d08d13c8b2b79cfd7d10652bc00fa6e34
4
+ data.tar.gz: 52bcb1b830d09018a77db7e6d19fdabe78d7cc5a324245de6de39fa66ae8231e
5
5
  SHA512:
6
- metadata.gz: fa1408a44611715ae071ce35b937f40d2c8eae43d1dff2c4d4848a000b34e6944591f56478175ca5b5c3fb0e08e0711cb23ef171def537983d250854ff52a31d
7
- data.tar.gz: 73a5ae3dc603b55e2827cd8126219bddc066a61674ff328a6beed41fca855b3c2e3c8161be3fe57fdec6c7d095b0c482495200b87e5a9f5f10672b7e0b0e4a6e
6
+ metadata.gz: b71b3595ada2889fd0120c705d6b901b343b2e72bbb834223e291b7bd624d7b800f831612ccf1df4e4b29fdf478f091c05617375a1e944b0d0a8248ac85df08b
7
+ data.tar.gz: c30b07acabd6604a55468f3ea1dfea5ff1fdaee3aa199d15c594f3864d54ffc7ba3339e86606c881a2c19843fad780f72638f768ebcda0e8ba991e5e77f9e134
@@ -27,6 +27,8 @@ require_relative 'aws-record/record/secondary_indexes'
27
27
  require_relative 'aws-record/record/table_config'
28
28
  require_relative 'aws-record/record/table_migration'
29
29
  require_relative 'aws-record/record/version'
30
+ require_relative 'aws-record/record/transactions'
31
+ require_relative 'aws-record/record/buildable_search'
30
32
  require_relative 'aws-record/record/marshalers/string_marshaler'
31
33
  require_relative 'aws-record/record/marshalers/boolean_marshaler'
32
34
  require_relative 'aws-record/record/marshalers/integer_marshaler'
@@ -48,12 +48,14 @@ module Aws
48
48
  # item is nil or not set at persistence time.
49
49
  def initialize(name, options = {})
50
50
  @name = name
51
- @database_name = options[:database_attribute_name] || name.to_s
51
+ @database_name = (options[:database_attribute_name] || name).to_s
52
52
  @dynamodb_type = options[:dynamodb_type]
53
53
  @marshaler = options[:marshaler] || DefaultMarshaler
54
54
  @persist_nil = options[:persist_nil]
55
55
  dv = options[:default_value]
56
- @default_value_or_lambda = type_cast(dv) unless dv.nil?
56
+ unless dv.nil?
57
+ @default_value_or_lambda = _is_lambda?(dv) ? dv : type_cast(dv)
58
+ end
57
59
  end
58
60
 
59
61
  # Attempts to type cast a raw value into the attribute's type. This call
@@ -92,8 +94,8 @@ module Aws
92
94
 
93
95
  # @api private
94
96
  def default_value
95
- if @default_value_or_lambda.respond_to?(:call)
96
- @default_value_or_lambda.call
97
+ if _is_lambda?(@default_value_or_lambda)
98
+ type_cast(@default_value_or_lambda.call)
97
99
  else
98
100
  _deep_copy(@default_value_or_lambda)
99
101
  end
@@ -104,6 +106,10 @@ module Aws
104
106
  Marshal.load(Marshal.dump(obj))
105
107
  end
106
108
 
109
+ def _is_lambda?(obj)
110
+ obj.respond_to?(:call)
111
+ end
112
+
107
113
  end
108
114
 
109
115
  # This is an identity marshaler, which performs no changes for type casting
@@ -0,0 +1,276 @@
1
+ module Aws
2
+ module Record
3
+ class BuildableSearch
4
+ SUPPORTED_OPERATIONS = [:query, :scan]
5
+
6
+ # This should never be called directly, rather it is called by the
7
+ # #build_query or #build_scan methods of your aws-record model class.
8
+ def initialize(opts)
9
+ operation = opts[:operation]
10
+ model = opts[:model]
11
+ if SUPPORTED_OPERATIONS.include?(operation)
12
+ @operation = operation
13
+ else
14
+ raise ArgumentError.new("Unsupported operation: #{operation}")
15
+ end
16
+ @model = model
17
+ @params = {}
18
+ @next_name = "BUILDERA"
19
+ @next_value = "buildera"
20
+ end
21
+
22
+ # If you are querying or scanning on an index, you can specify it with
23
+ # this builder method. Provide the symbol of your index as defined on your
24
+ # model class.
25
+ def on_index(index)
26
+ @params[:index_name] = index
27
+ self
28
+ end
29
+
30
+ # If true, will perform your query or scan as a consistent read. If false,
31
+ # the query or scan is eventually consistent.
32
+ def consistent_read(b)
33
+ @params[:consistent_read] = b
34
+ self
35
+ end
36
+
37
+ # For the scan operation, you can split your scan into multiple segments
38
+ # to be scanned in parallel. If you wish to do this, you can use this
39
+ # builder method to provide the :total_segments of your parallel scan and
40
+ # the :segment number of this scan.
41
+ def parallel_scan(opts)
42
+ unless @operation == :scan
43
+ raise ArgumentError.new("parallel_scan is only supported for scans")
44
+ end
45
+ unless opts[:total_segments] && opts[:segment]
46
+ raise ArgumentError.new("Must specify :total_segments and :segment in a parallel scan.")
47
+ end
48
+ @params[:total_segments] = opts[:total_segments]
49
+ @params[:segment] = opts[:segment]
50
+ self
51
+ end
52
+
53
+ # For a query operation, you can use this to set if you query is in
54
+ # ascending or descending order on your range key. By default, a query is
55
+ # run in ascending order.
56
+ def scan_ascending(b)
57
+ unless @operation == :query
58
+ raise ArgumentError.new("scan_ascending is only supported for queries.")
59
+ end
60
+ @params[:scan_index_forward] = b
61
+ self
62
+ end
63
+
64
+ # If you have an exclusive start key for your query or scan, you can
65
+ # provide it with this builder method. You should not use this if you are
66
+ # querying or scanning without a set starting point, as the
67
+ # {Aws::Record::ItemCollection} class handles pagination automatically
68
+ # for you.
69
+ def exclusive_start_key(key)
70
+ @params[:exclusive_start_key] = key
71
+ self
72
+ end
73
+
74
+ # Provide a key condition expression for your query using a substitution
75
+ # expression.
76
+ #
77
+ # @example Building a simple query with a key expression:
78
+ # # Example model class
79
+ # class ExampleTable
80
+ # include Aws::Record
81
+ # string_attr :uuid, hash_key: true
82
+ # integer_attr :id, range_key: true
83
+ # string_attr :body
84
+ # end
85
+ #
86
+ # q = ExampleTable.build_query.key_expr(
87
+ # ":uuid = ? AND :id > ?", "smpl-uuid", 100
88
+ # ).complete!
89
+ # q.to_a # You can use this like any other query result in aws-record
90
+ def key_expr(statement_str, *subs)
91
+ unless @operation == :query
92
+ raise ArgumentError.new("key_expr is only supported for queries.")
93
+ end
94
+ names = @params[:expression_attribute_names]
95
+ if names.nil?
96
+ @params[:expression_attribute_names] = {}
97
+ names = @params[:expression_attribute_names]
98
+ end
99
+ values = @params[:expression_attribute_values]
100
+ if values.nil?
101
+ @params[:expression_attribute_values] = {}
102
+ values = @params[:expression_attribute_values]
103
+ end
104
+ _key_pass(statement_str, names)
105
+ _apply_values(statement_str, subs, values)
106
+ @params[:key_condition_expression] = statement_str
107
+ self
108
+ end
109
+
110
+ # Provide a filter expression for your query or scan using a substitution
111
+ # expression.
112
+ #
113
+ # @example Building a simple scan:
114
+ # # Example model class
115
+ # class ExampleTable
116
+ # include Aws::Record
117
+ # string_attr :uuid, hash_key: true
118
+ # integer_attr :id, range_key: true
119
+ # string_attr :body
120
+ # end
121
+ #
122
+ # scan = ExampleTable.build_scan.filter_expr(
123
+ # "contains(:body, ?)",
124
+ # "bacon"
125
+ # ).complete!
126
+ #
127
+ def filter_expr(statement_str, *subs)
128
+ names = @params[:expression_attribute_names]
129
+ if names.nil?
130
+ @params[:expression_attribute_names] = {}
131
+ names = @params[:expression_attribute_names]
132
+ end
133
+ values = @params[:expression_attribute_values]
134
+ if values.nil?
135
+ @params[:expression_attribute_values] = {}
136
+ values = @params[:expression_attribute_values]
137
+ end
138
+ _key_pass(statement_str, names)
139
+ _apply_values(statement_str, subs, values)
140
+ @params[:filter_expression] = statement_str
141
+ self
142
+ end
143
+
144
+ # Allows you to define a projection expression for the values returned by
145
+ # a query or scan. See
146
+ # {https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ProjectionExpressions.html the Amazon DynamoDB Developer Guide}
147
+ # for more details on projection expressions. You can use the symbols from
148
+ # your aws-record model class in a projection expression. Keys are always
149
+ # retrieved.
150
+ #
151
+ # @example Scan with a projection expression:
152
+ # # Example model class
153
+ # class ExampleTable
154
+ # include Aws::Record
155
+ # string_attr :uuid, hash_key: true
156
+ # integer_attr :id, range_key: true
157
+ # string_attr :body
158
+ # map_attr :metadata
159
+ # end
160
+ #
161
+ # scan = ExampleTable.build_scan.projection_expr(
162
+ # ":body"
163
+ # ).complete!
164
+ def projection_expr(statement_str)
165
+ names = @params[:expression_attribute_names]
166
+ if names.nil?
167
+ @params[:expression_attribute_names] = {}
168
+ names = @params[:expression_attribute_names]
169
+ end
170
+ _key_pass(statement_str, names)
171
+ @params[:projection_expression] = statement_str
172
+ self
173
+ end
174
+
175
+ # Allows you to set a page size limit on each query or scan request.
176
+ def limit(size)
177
+ @params[:limit] = size
178
+ self
179
+ end
180
+
181
+ # Allows you to define a callback that will determine the model class
182
+ # to be used for each item, allowing queries to return an ItemCollection
183
+ # with mixed models. The provided block must return the model class based on
184
+ # any logic on the raw item attributes or `nil` if no model applies and
185
+ # the item should be skipped. Note: The block only has access to raw item
186
+ # data so attributes must be accessed using their names as defined in the
187
+ # table, not as the symbols defined in the model class(s).
188
+ #
189
+ # @example Scan with heterogeneous results:
190
+ # # Example model classes
191
+ # class Model_A
192
+ # include Aws::Record
193
+ # set_table_name(TABLE_NAME)
194
+ #
195
+ # string_attr :uuid, hash_key: true
196
+ # string_attr :class_name, range_key: true
197
+ #
198
+ # string_attr :attr_a
199
+ # end
200
+ #
201
+ # class Model_B
202
+ # include Aws::Record
203
+ # set_table_name(TABLE_NAME)
204
+ #
205
+ # string_attr :uuid, hash_key: true
206
+ # string_attr :class_name, range_key: true
207
+ #
208
+ # string_attr :attr_b
209
+ # end
210
+ #
211
+ # # use multi_model_filter to create a query on TABLE_NAME
212
+ # items = Model_A.build_scan.multi_model_filter do |raw_item_attributes|
213
+ # case raw_item_attributes['class_name']
214
+ # when "A" then Model_A
215
+ # when "B" then Model_B
216
+ # else
217
+ # nil
218
+ # end
219
+ # end.complete!
220
+ def multi_model_filter(proc = nil, &block)
221
+ @params[:model_filter] = proc || block
222
+ self
223
+ end
224
+
225
+ # You must call this method at the end of any query or scan you build.
226
+ #
227
+ # @return [Aws::Record::ItemCollection] The item collection lazy
228
+ # enumerable.
229
+ def complete!
230
+ @model.send(@operation, @params)
231
+ end
232
+
233
+ private
234
+ def _key_pass(statement, names)
235
+ statement.gsub!(/:(\w+)/) do |match|
236
+ key = match.gsub!(':','').to_sym
237
+ key_name = @model.attributes.storage_name_for(key)
238
+ if key_name
239
+ sub_name = _next_name
240
+ raise "Substitution collision!" if names[sub_name]
241
+ names[sub_name] = key_name
242
+ sub_name
243
+ else
244
+ raise "No such key #{key}"
245
+ end
246
+ end
247
+ end
248
+
249
+ def _apply_values(statement, subs, values)
250
+ count = 0
251
+ statement.gsub!(/[?]/) do |match|
252
+ sub_value = _next_value
253
+ raise "Substitution collision!" if values[sub_value]
254
+ values[sub_value] = subs[count]
255
+ count += 1
256
+ sub_value
257
+ end
258
+ unless count == subs.size
259
+ raise "Expected #{count} values in the substitution set, but found #{subs.size}"
260
+ end
261
+ end
262
+
263
+ def _next_name
264
+ ret = "#" + @next_name
265
+ @next_name.next!
266
+ ret
267
+ end
268
+
269
+ def _next_value
270
+ ret = ":" + @next_value
271
+ @next_value.next!
272
+ ret
273
+ end
274
+ end
275
+ end
276
+ end
@@ -54,6 +54,14 @@ module Aws
54
54
  class TableDoesNotExist < RuntimeError; end
55
55
 
56
56
  class MissingRequiredConfiguration < RuntimeError; end
57
+
58
+ # Raised when you attempt to combine your own condition expression with
59
+ # the auto-generated condition expression from a "safe put" from saving
60
+ # a new item in a transactional write operation. The path forward until
61
+ # this case is supported is to use a plain "put" call, and to include
62
+ # the key existance check yourself in your condition expression if you
63
+ # wish to do so.
64
+ class TransactionalSaveConditionCollision < RuntimeError; end
57
65
  end
58
66
  end
59
67
  end
@@ -19,6 +19,7 @@ module Aws
19
19
  def initialize(search_method, search_params, model, client)
20
20
  @search_method = search_method
21
21
  @search_params = search_params
22
+ @model_filter = @search_params.delete(:model_filter)
22
23
  @model = model
23
24
  @client = client
24
25
  end
@@ -91,9 +92,11 @@ module Aws
91
92
  def _build_items_from_response(items, model)
92
93
  ret = []
93
94
  items.each do |item|
94
- record = model.new
95
+ model_class = @model_filter ? @model_filter.call(item) : model
96
+ next unless model_class
97
+ record = model_class.new
95
98
  data = record.instance_variable_get("@data")
96
- model.attributes.attributes.each do |name, attr|
99
+ model_class.attributes.attributes.each do |name, attr|
97
100
  data.set_attribute(name, attr.extract(item))
98
101
  end
99
102
  data.clean!
@@ -122,9 +122,9 @@ module Aws
122
122
 
123
123
  def populate_default_values
124
124
  @model_attributes.attributes.each do |name, attribute|
125
- unless attribute.default_value.nil?
125
+ unless (default_value = attribute.default_value).nil?
126
126
  if @data[name].nil? && @data[name].nil?
127
- @data[name] = attribute.default_value
127
+ @data[name] = default_value
128
128
  end
129
129
  end
130
130
  end
@@ -82,8 +82,7 @@ module Aws
82
82
  end
83
83
 
84
84
 
85
- # Deletes the item instance that matches the key values of this item
86
- # instance in Amazon DynamoDB.
85
+ # Assigns the attributes provided onto the model.
87
86
  #
88
87
  # @example Usage Example
89
88
  # class MyModel
@@ -329,6 +328,112 @@ module Aws
329
328
 
330
329
  module ItemOperationsClassMethods
331
330
 
331
+ # @example Usage Example
332
+ # check_exp = Model.transact_check_expression(
333
+ # key: { uuid: "foo" },
334
+ # condition_expression: "size(#T) <= :v",
335
+ # expression_attribute_names: {
336
+ # "#T" => "body"
337
+ # },
338
+ # expression_attribute_values: {
339
+ # ":v" => 1024
340
+ # }
341
+ # )
342
+ #
343
+ # Allows you to build a "check" expression for use in transactional
344
+ # write operations.
345
+ #
346
+ # @param [Hash] opts Options matching the :condition_check contents in
347
+ # the
348
+ # {https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/DynamoDB/Client.html#transact_write_items-instance_method Aws::DynamoDB::Client#transact_write_items}
349
+ # API, with the exception that keys will be marshalled for you, and
350
+ # the table name will be provided for you by the operation.
351
+ # @return [Hash] Options suitable to be used as a check expression when
352
+ # calling the +#transact_write+ operation.
353
+ def transact_check_expression(opts)
354
+ # need to transform the key, and add the table name
355
+ opts = opts.dup
356
+ key = opts.delete(:key)
357
+ check_key = {}
358
+ @keys.keys.each_value do |attr_sym|
359
+ unless key[attr_sym]
360
+ raise Errors::KeyMissing.new(
361
+ "Missing required key #{attr_sym} in #{key}"
362
+ )
363
+ end
364
+ attr_name = attributes.storage_name_for(attr_sym)
365
+ check_key[attr_name] = attributes.attribute_for(attr_sym).
366
+ serialize(key[attr_sym])
367
+ end
368
+ opts[:key] = check_key
369
+ opts[:table_name] = table_name
370
+ opts
371
+ end
372
+
373
+ def tfind_opts(opts)
374
+ opts = opts.dup
375
+ key = opts.delete(:key)
376
+ request_key = {}
377
+ @keys.keys.each_value do |attr_sym|
378
+ unless key[attr_sym]
379
+ raise Errors::KeyMissing.new(
380
+ "Missing required key #{attr_sym} in #{key}"
381
+ )
382
+ end
383
+ attr_name = attributes.storage_name_for(attr_sym)
384
+ request_key[attr_name] = attributes.attribute_for(attr_sym).
385
+ serialize(key[attr_sym])
386
+ end
387
+ # this is a :get item used by #transact_get_items, with the exception
388
+ # of :model_class which needs to be removed before passing along
389
+ opts[:key] = request_key
390
+ opts[:table_name] = table_name
391
+ {
392
+ model_class: self,
393
+ get: opts
394
+ }
395
+ end
396
+
397
+ # @example Usage Example
398
+ # class Table
399
+ # include Aws::Record
400
+ # string_attr :hk, hash_key: true
401
+ # string_attr :rk, range_key: true
402
+ # end
403
+ #
404
+ # results = Table.transact_find(
405
+ # transact_items: [
406
+ # {key: { hk: "hk1", rk: "rk1"}},
407
+ # {key: { hk: "hk2", rk: "rk2"}}
408
+ # ]
409
+ # ) # => results.responses contains nil or instances of Table
410
+ #
411
+ # Provides a way to run a transactional find across multiple DynamoDB
412
+ # items, including transactions which get items across multiple actual
413
+ # or virtual tables.
414
+ #
415
+ # @param [Hash] opts Options to pass through to
416
+ # {https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/DynamoDB/Client.html#transact_get_items-instance_method Aws::DynamoDB::Client#transact_get_items},
417
+ # with the exception of the :transact_items array, which uses the
418
+ # +#tfind_opts+ operation on your model class to provide extra
419
+ # metadata used to marshal your items after retrieval.
420
+ # @option opts [Array] :transact_items A set of options describing
421
+ # instances of the model class to return.
422
+ # @return [OpenStruct] Structured like the client API response from
423
+ # +#transact_get_items+, except that the +responses+ member contains
424
+ # +Aws::Record+ items marshaled into the model class used to call
425
+ # this method. See the usage example.
426
+ def transact_find(opts)
427
+ opts = opts.dup
428
+ transact_items = opts.delete(:transact_items)
429
+ global_transact_items = transact_items.map do |topts|
430
+ tfind_opts(topts)
431
+ end
432
+ opts[:transact_items] = global_transact_items
433
+ opts[:client] = dynamodb_client
434
+ Transactions.transact_find(opts)
435
+ end
436
+
332
437
  # @example Usage Example
333
438
  # class MyModel
334
439
  # include Aws::Record
@@ -58,7 +58,7 @@ module Aws
58
58
  if item.is_a?(Numeric)
59
59
  item
60
60
  else
61
- BigDecimal.new(item.to_s)
61
+ BigDecimal(item.to_s)
62
62
  end
63
63
  end
64
64
  end
@@ -102,6 +102,54 @@ module Aws
102
102
  scan_opts = opts.merge(table_name: table_name)
103
103
  ItemCollection.new(:scan, scan_opts, self, dynamodb_client)
104
104
  end
105
+
106
+ # This method allows you to build a query using the {Aws::Record::BuildableSearch} DSL.
107
+ #
108
+ # @example Building a simple query:
109
+ # # Example model class
110
+ # class ExampleTable
111
+ # include Aws::Record
112
+ # string_attr :uuid, hash_key: true
113
+ # integer_attr :id, range_key: true
114
+ # string_attr :body
115
+ # end
116
+ #
117
+ # q = ExampleTable.build_query.key_expr(
118
+ # ":uuid = ? AND :id > ?", "smpl-uuid", 100
119
+ # ).scan_ascending(false).complete!
120
+ # q.to_a # You can use this like any other query result in aws-record
121
+ def build_query
122
+ BuildableSearch.new(
123
+ operation: :query,
124
+ model: self
125
+ )
126
+ end
127
+
128
+ # This method allows you to build a scan using the {Aws::Record::BuildableSearch} DSL.
129
+ #
130
+ # @example Building a simple scan:
131
+ # # Example model class
132
+ # class ExampleTable
133
+ # include Aws::Record
134
+ # string_attr :uuid, hash_key: true
135
+ # integer_attr :id, range_key: true
136
+ # string_attr :body
137
+ # end
138
+ #
139
+ # segment_2_scan = ExampleTable.build_scan.filter_expr(
140
+ # "contains(:body, ?)",
141
+ # "bacon"
142
+ # ).scan_ascending(false).parallel_scan(
143
+ # total_segments: 5,
144
+ # segment: 2
145
+ # ).complete!
146
+ # segment_2_scan.to_a # You can use this like any other query result in aws-record
147
+ def build_scan
148
+ BuildableSearch.new(
149
+ operation: :scan,
150
+ model: self
151
+ )
152
+ end
105
153
  end
106
154
 
107
155
  end
@@ -33,6 +33,17 @@ module Aws
33
33
  # t.write_capacity_units 5
34
34
  # end
35
35
  #
36
+ # @example A basic model with pay per request billing.
37
+ # class Model
38
+ # include Aws::Record
39
+ # string_attr :uuid, hash_key: true
40
+ # end
41
+ #
42
+ # table_config = Aws::Record::TableConfig.define do |t|
43
+ # t.model_class Model
44
+ # t.billing_mode "PAY_PER_REQUEST"
45
+ # end
46
+ #
36
47
  # @example Running a conditional migration on a basic model.
37
48
  # table_config = Aws::Record::TableConfig.define do |t|
38
49
  # t.model_class Model
@@ -59,7 +70,9 @@ module Aws
59
70
  # :title,
60
71
  # hash_key: :forum_uuid,
61
72
  # range_key: :post_title,
62
- # projection_type: "ALL"
73
+ # projection: {
74
+ # projection_type: "ALL"
75
+ # }
63
76
  # )
64
77
  # end
65
78
  #
@@ -106,7 +119,12 @@ module Aws
106
119
  # * +#write_capacity_units+ Sets the write capacity units for the
107
120
  # index.
108
121
  # * +#ttl_attribute+ Sets the attribute ID to be used as the TTL
109
- # attribute, and if present, TTL will be enabled for the table.
122
+ # attribute, and if present, TTL will be enabled for the table.
123
+ # * +#billing_mode+ Sets the billing mode, with the current supported
124
+ # options being "PROVISIONED" and "PAY_PER_REQUEST". If using
125
+ # "PAY_PER_REQUEST" you must not set provisioned throughput values,
126
+ # and if using "PROVISIONED" you must set provisioned throughput
127
+ # values. Default assumption is "PROVISIONED".
110
128
  #
111
129
  # @example Defining a migration with a GSI.
112
130
  # class Forum
@@ -151,6 +169,7 @@ module Aws
151
169
  def initialize
152
170
  @client_options = {}
153
171
  @global_secondary_indexes = {}
172
+ @billing_mode = "PROVISIONED" # default
154
173
  end
155
174
 
156
175
  # @api private
@@ -195,6 +214,11 @@ module Aws
195
214
  end
196
215
  end
197
216
 
217
+ # @api private
218
+ def billing_mode(mode)
219
+ @billing_mode = mode
220
+ end
221
+
198
222
  # Performs a migration, if needed, against the remote table. If
199
223
  # +#compatible?+ would return true, the remote table already has the same
200
224
  # throughput, key schema, attribute definitions, and global secondary
@@ -209,13 +233,7 @@ module Aws
209
233
  else
210
234
  # Gotcha: You need separate migrations for indexes and throughput
211
235
  unless _throughput_equal(resp)
212
- @client.update_table(
213
- table_name: @model_class.table_name,
214
- provisioned_throughput: {
215
- read_capacity_units: @read_capacity_units,
216
- write_capacity_units: @write_capacity_units
217
- }
218
- )
236
+ @client.update_table(_update_throughput_opts(resp))
219
237
  @client.wait_until(
220
238
  :table_exists,
221
239
  table_name: @model_class.table_name
@@ -327,12 +345,19 @@ module Aws
327
345
 
328
346
  def _create_table_opts
329
347
  opts = {
330
- table_name: @model_class.table_name,
331
- provisioned_throughput: {
348
+ table_name: @model_class.table_name
349
+ }
350
+ if @billing_mode == "PROVISIONED"
351
+ opts[:provisioned_throughput] = {
332
352
  read_capacity_units: @read_capacity_units,
333
353
  write_capacity_units: @write_capacity_units
334
354
  }
335
- }
355
+ elsif @billing_mode == "PAY_PER_REQUEST"
356
+ opts[:billing_mode] = @billing_mode
357
+ else
358
+ raise ArgumentError, "Unsupported billing mode #{@billing_mode}"
359
+ end
360
+
336
361
  opts[:key_schema] = _key_schema
337
362
  opts[:attribute_definitions] = _attribute_definitions
338
363
  gsi = _global_secondary_indexes
@@ -342,6 +367,54 @@ module Aws
342
367
  opts
343
368
  end
344
369
 
370
+ def _add_global_secondary_index_throughput(opts, resp_gsis)
371
+ gsis = resp_gsis.map do |g|
372
+ g.index_name
373
+ end
374
+ gsi_updates = []
375
+ gsis.each do |index_name|
376
+ lgsi = @global_secondary_indexes[index_name.to_sym]
377
+ gsi_updates << {
378
+ update: {
379
+ index_name: index_name,
380
+ provisioned_throughput: lgsi.provisioned_throughput
381
+ }
382
+ }
383
+ end
384
+ opts[:global_secondary_index_updates] = gsi_updates
385
+ true
386
+ end
387
+
388
+ def _update_throughput_opts(resp)
389
+ if @billing_mode == "PROVISIONED"
390
+ opts = {
391
+ table_name: @model_class.table_name,
392
+ provisioned_throughput: {
393
+ read_capacity_units: @read_capacity_units,
394
+ write_capacity_units: @write_capacity_units
395
+ }
396
+ }
397
+ # special case: we have global secondary indexes existing, and they
398
+ # need provisioned capacity to be set within this call
399
+ if !resp.table.billing_mode_summary.nil? &&
400
+ resp.table.billing_mode_summary.billing_mode == "PAY_PER_REQUEST"
401
+ opts[:billing_mode] = @billing_mode
402
+ if resp.table.global_secondary_indexes
403
+ resp_gsis = resp.table.global_secondary_indexes
404
+ _add_global_secondary_index_throughput(opts, resp_gsis)
405
+ end
406
+ end # else don't include billing mode
407
+ opts
408
+ elsif @billing_mode == "PAY_PER_REQUEST"
409
+ {
410
+ table_name: @model_class.table_name,
411
+ billing_mode: "PAY_PER_REQUEST"
412
+ }
413
+ else
414
+ raise ArgumentError, "Unsupported billing mode #{@billing_mode}"
415
+ end
416
+ end
417
+
345
418
  def _update_index_opts(resp)
346
419
  gsi_updates, attribute_definitions = _gsi_updates(resp)
347
420
  opts = {
@@ -369,22 +442,25 @@ module Aws
369
442
  gsi[:key_schema].each do |k|
370
443
  attributes_referenced.add(k[:attribute_name])
371
444
  end
372
- # This may be a problem, check if I can maintain symbols.
373
- lgsi = @global_secondary_indexes[index_name.to_sym]
374
- gsi[:provisioned_throughput] = lgsi.provisioned_throughput
445
+ if @billing_mode == "PROVISIONED"
446
+ lgsi = @global_secondary_indexes[index_name.to_sym]
447
+ gsi[:provisioned_throughput] = lgsi.provisioned_throughput
448
+ end
375
449
  gsi_updates << {
376
450
  create: gsi
377
451
  }
378
452
  end
379
- update_candidates.each do |index_name|
380
- # This may be a problem, check if I can maintain symbols.
381
- lgsi = @global_secondary_indexes[index_name.to_sym]
382
- gsi_updates << {
383
- update: {
384
- index_name: index_name,
385
- provisioned_throughput: lgsi.provisioned_throughput
453
+ # we don't currently update anything other than throughput
454
+ if @billing_mode == "PROVISIONED"
455
+ update_candidates.each do |index_name|
456
+ lgsi = @global_secondary_indexes[index_name.to_sym]
457
+ gsi_updates << {
458
+ update: {
459
+ index_name: index_name,
460
+ provisioned_throughput: lgsi.provisioned_throughput
461
+ }
386
462
  }
387
- }
463
+ end
388
464
  end
389
465
  attribute_definitions = _attribute_definitions
390
466
  incremental_attributes = attributes_referenced.map do |attr_name|
@@ -438,13 +514,18 @@ module Aws
438
514
  end
439
515
 
440
516
  def _throughput_equal(resp)
441
- expected = resp.table.provisioned_throughput.to_h
442
- actual = {
443
- read_capacity_units: @read_capacity_units,
444
- write_capacity_units: @write_capacity_units
445
- }
446
- actual.all? do |k,v|
447
- expected[k] == v
517
+ if @billing_mode == "PAY_PER_REQUEST"
518
+ !resp.table.billing_mode_summary.nil? &&
519
+ resp.table.billing_mode_summary.billing_mode == "PAY_PER_REQUEST"
520
+ else
521
+ expected = resp.table.provisioned_throughput.to_h
522
+ actual = {
523
+ read_capacity_units: @read_capacity_units,
524
+ write_capacity_units: @write_capacity_units
525
+ }
526
+ actual.all? do |k,v|
527
+ expected[k] == v
528
+ end
448
529
  end
449
530
  end
450
531
 
@@ -498,10 +579,17 @@ module Aws
498
579
  remote_key_schema = rgsi.key_schema.map { |i| i.to_h }
499
580
  ks_match = _array_unsorted_eql(remote_key_schema, lgsi[:key_schema])
500
581
 
582
+ # Throughput Check: Dependent on Billing Mode
501
583
  rpt = rgsi.provisioned_throughput.to_h
502
584
  lpt = lgsi[:provisioned_throughput]
503
- pt_match = lpt.all? do |k,v|
504
- rpt[k] == v
585
+ if @billing_mode == "PROVISIONED"
586
+ pt_match = lpt.all? do |k,v|
587
+ rpt[k] == v
588
+ end
589
+ elsif @billing_mode == "PAY_PER_REQUEST"
590
+ pt_match = lpt.nil? ? true : false
591
+ else
592
+ raise ArgumentError, "Unsupported billing mode #{@billing_mode}"
505
593
  end
506
594
 
507
595
  rp = rgsi.projection.to_h
@@ -537,10 +625,13 @@ module Aws
537
625
  if model_gsis
538
626
  model_gsis.each do |mgsi|
539
627
  config = gsi_config[mgsi[:index_name]]
540
- # Validate throughput exists? Validate each throughput is in model?
541
- gsis << mgsi.merge(
542
- provisioned_throughput: config.provisioned_throughput
543
- )
628
+ if @billing_mode == "PROVISIONED"
629
+ gsis << mgsi.merge(
630
+ provisioned_throughput: config.provisioned_throughput
631
+ )
632
+ else
633
+ gsis << mgsi
634
+ end
544
635
  end
545
636
  end
546
637
  gsis
@@ -553,8 +644,14 @@ module Aws
553
644
  def _validate_required_configuration
554
645
  missing_config = []
555
646
  missing_config << 'model_class' unless @model_class
556
- missing_config << 'read_capacity_units' unless @read_capacity_units
557
- missing_config << 'write_capacity_units' unless @write_capacity_units
647
+ if @billing_mode == "PROVISIONED"
648
+ missing_config << 'read_capacity_units' unless @read_capacity_units
649
+ missing_config << 'write_capacity_units' unless @write_capacity_units
650
+ else
651
+ if @read_capacity_units || @write_capacity_units
652
+ raise ArgumentError.new("Cannot have billing mode #{@billing_mode} with provisioned capacity.")
653
+ end
654
+ end
558
655
  unless missing_config.empty?
559
656
  msg = missing_config.join(', ')
560
657
  raise Errors::MissingRequiredConfiguration, 'Missing: ' + msg
@@ -0,0 +1,332 @@
1
+ module Aws
2
+ module Record
3
+ module Transactions
4
+ class << self
5
+
6
+ # @example Usage Example
7
+ # class TableOne
8
+ # include Aws::Record
9
+ # string_attr :uuid, hash_key: true
10
+ # end
11
+ #
12
+ # class TableTwo
13
+ # include Aws::Record
14
+ # string_attr :hk, hash_key: true
15
+ # string_attr :rk, range_key: true
16
+ # end
17
+ #
18
+ # results = Aws::Record::Transactions.transact_find(
19
+ # transact_items: [
20
+ # TableOne.tfind_opts(key: { uuid: "uuid1234" }),
21
+ # TableTwo.tfind_opts(key: { hk: "hk1", rk: "rk1"}),
22
+ # TableTwo.tfind_opts(key: { hk: "hk2", rk: "rk2"})
23
+ # ]
24
+ # ) # => results.responses contains nil or marshalled items
25
+ # results.responses.map { |r| r.class } # [TableOne, TableTwo, TableTwo]
26
+ #
27
+ # Provides a way to run a transactional find across multiple DynamoDB
28
+ # items, including transactions which get items across multiple actual
29
+ # or virtual tables.
30
+ #
31
+ # @param [Hash] opts Options to pass through to
32
+ # {https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/DynamoDB/Client.html#transact_get_items-instance_method Aws::DynamoDB::Client#transact_get_items},
33
+ # with the exception of the :transact_items array, which uses the
34
+ # +#tfind_opts+ operation on your model class to provide extra
35
+ # metadata used to marshal your items after retrieval.
36
+ # @option opts [Array] :transact_items A set of +#tfind_opts+ results,
37
+ # such as those created by the usage example.
38
+ # @option opts [Aws::DynamoDB::Client] :client Optionally, you can pass
39
+ # in your own client to use for the transaction calls.
40
+ # @return [OpenStruct] Structured like the client API response from
41
+ # +#transact_get_items+, except that the +responses+ member contains
42
+ # +Aws::Record+ items marshaled into the classes used to call
43
+ # +#tfind_opts+ in each positional member. See the usage example.
44
+ def transact_find(opts)
45
+ opts = opts.dup
46
+ client = opts.delete(:client) || dynamodb_client
47
+ transact_items = opts.delete(:transact_items) # add nil check?
48
+ model_classes = []
49
+ client_transact_items = transact_items.map do |tfind_opts|
50
+ model_class = tfind_opts.delete(:model_class)
51
+ model_classes << model_class
52
+ tfind_opts
53
+ end
54
+ request_opts = opts
55
+ request_opts[:transact_items] = client_transact_items
56
+ client_resp = client.transact_get_items(
57
+ request_opts
58
+ )
59
+ responses = client_resp.responses
60
+ index = -1
61
+ ret = OpenStruct.new
62
+ ret.consumed_capacity = client_resp.consumed_capacity
63
+ ret.missing_items = []
64
+ ret.responses = client_resp.responses.map do |item|
65
+ index += 1
66
+ if item.nil? || item.item.nil?
67
+ missing_data = {
68
+ model_class: model_classes[index],
69
+ key: transact_items[index][:get][:key]
70
+ }
71
+ ret.missing_items << missing_data
72
+ nil
73
+ else
74
+ # need to translate the item keys
75
+ raw_item = item.item
76
+ model_class = model_classes[index]
77
+ new_item_opts = {}
78
+ raw_item.each do |db_name, value|
79
+ name = model_class.attributes.db_to_attribute_name(db_name)
80
+ new_item_opts[name] = value
81
+ end
82
+ item = model_class.new(new_item_opts)
83
+ item.clean!
84
+ item
85
+ end
86
+ end
87
+ ret
88
+ end
89
+
90
+ # @example Usage Example
91
+ # class TableOne
92
+ # include Aws::Record
93
+ # string_attr :uuid, hash_key: true
94
+ # string_attr :body
95
+ # end
96
+ #
97
+ # class TableTwo
98
+ # include Aws::Record
99
+ # string_attr :hk, hash_key: true
100
+ # string_attr :rk, range_key: true
101
+ # string_attr :body
102
+ # end
103
+ #
104
+ # check_exp = TableOne.transact_check_expression(
105
+ # key: { uuid: "foo" },
106
+ # condition_expression: "size(#T) <= :v",
107
+ # expression_attribute_names: {
108
+ # "#T" => "body"
109
+ # },
110
+ # expression_attribute_values: {
111
+ # ":v" => 1024
112
+ # }
113
+ # )
114
+ # new_item = TableTwo.new(hk: "hk1", rk: "rk1", body: "Hello!")
115
+ # update_item_1 = TableOne.find(uuid: "bar")
116
+ # update_item_1.body = "Updated the body!"
117
+ # put_item = TableOne.new(uuid: "foobar", body: "Content!")
118
+ # update_item_2 = TableTwo.find(hk: "hk2", rk: "rk2")
119
+ # update_item_2.body = "Update!"
120
+ # delete_item = TableOne.find(uuid: "to_be_deleted")
121
+ #
122
+ # Aws::Record::Transactions.transact_write(
123
+ # transact_items: [
124
+ # { check: check_exp },
125
+ # { save: new_item },
126
+ # { save: update_item_1 },
127
+ # {
128
+ # put: put_item,
129
+ # condition_expression: "attribute_not_exists(#H)",
130
+ # expression_attribute_names: { "#H" => "uuid" },
131
+ # return_values_on_condition_check_failure: "ALL_OLD"
132
+ # },
133
+ # { update: update_item_2 },
134
+ # { delete: delete_item }
135
+ # ]
136
+ # )
137
+ #
138
+ # Provides a way to pass in aws-record items into transactional writes,
139
+ # as well as adding the ability to run 'save' commands in a transaction
140
+ # while allowing aws-record to determine if a :put or :update operation
141
+ # is most appropriate. +#transact_write+ supports 5 different transact
142
+ # item modes:
143
+ # - save: Behaves much like the +#save+ operation on the item itself.
144
+ # If the keys are dirty, and thus it appears to be a new item, will
145
+ # create a :put operation with a conditional check on the item's
146
+ # existance. Note that you cannot bring your own conditional
147
+ # expression in this case. If you wish to force put or add your
148
+ # own conditional checks, use the :put operation.
149
+ # - put: Does a force put for the given item key and model.
150
+ # - update: Does an upsert for the given item.
151
+ # - delete: Deletes the given item.
152
+ # - check: Takes the result of +#transact_check_expression+,
153
+ # performing the specified check as a part of the transaction.
154
+ #
155
+ # @param [Hash] opts Options to pass through to
156
+ # {https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/DynamoDB/Client.html#transact_write_items-instance_method Aws::DynamoDB::Client#transact_write_items}
157
+ # with the exception of :transact_items array, which is transformed
158
+ # to use your item to populate the :key, :table_name, :item, and/or
159
+ # :update_expression parameters as appropriate. See the usage example
160
+ # for a comprehensive set of combinations.
161
+ # @option opts [Array] :transact_items An array of hashes, accepting
162
+ # +:save+, +:put+, +:delete+, +:update+, and +:check+ as specified.
163
+ # @option opts [Aws::DynamoDB::Client] :client Optionally, you can
164
+ # specify a client to use for this transaction call. If not
165
+ # specified, the configured client for +Aws::Record::Transactions+
166
+ # is used.
167
+ def transact_write(opts)
168
+ opts = opts.dup
169
+ client = opts.delete(:client) || dynamodb_client
170
+ dirty_items = []
171
+ delete_items = []
172
+ # fetch abstraction records
173
+ transact_items = _transform_transact_write_items(
174
+ opts.delete(:transact_items),
175
+ dirty_items,
176
+ delete_items
177
+ )
178
+ opts[:transact_items] = transact_items
179
+ resp = client.transact_write_items(opts)
180
+ # mark all items clean/destroyed as needed if we didn't raise an exception
181
+ dirty_items.each { |i| i.clean! }
182
+ delete_items.each { |i| i.instance_variable_get("@data").destroyed = true }
183
+ resp
184
+ end
185
+
186
+ # Configures the Amazon DynamoDB client used by global transaction
187
+ # operations.
188
+ #
189
+ # Please note that this method is also called internally when you first
190
+ # attempt to perform an operation against the remote end, if you have
191
+ # not already configured a client. As such, please read and understand
192
+ # the documentation in the AWS SDK for Ruby V3 around
193
+ # {https://docs.aws.amazon.com/sdk-for-ruby/v3/api/#Configuration configuration}
194
+ # to ensure you understand how default configuration behavior works.
195
+ # When in doubt, call this method to ensure your client is configured
196
+ # the way you want it to be configured.
197
+ #
198
+ # @param [Hash] opts the options you wish to use to create the client.
199
+ # Note that if you include the option +:client+, all other options
200
+ # will be ignored. See the documentation for other options in the
201
+ # {https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/DynamoDB/Client.html#initialize-instance_method AWS SDK for Ruby V3}.
202
+ # @option opts [Aws::DynamoDB::Client] :client allows you to pass in
203
+ # your own pre-configured client.
204
+ def configure_client(opts = {})
205
+ provided_client = opts.delete(:client)
206
+ opts[:user_agent_suffix] = _user_agent(
207
+ opts.delete(:user_agent_suffix)
208
+ )
209
+ client = provided_client || Aws::DynamoDB::Client.new(opts)
210
+ @@dynamodb_client = client
211
+ end
212
+
213
+ # Gets the
214
+ # {https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/DynamoDB/Client.html}
215
+ # instance that Transactions use. When called for the first time, if
216
+ # {#configure_client} has not yet been called, will configure a new
217
+ # client for you with default parameters.
218
+ #
219
+ # @return [Aws::DynamoDB::Client] the Amazon DynamoDB client instance.
220
+ def dynamodb_client
221
+ @@dynamodb_client ||= configure_client
222
+ end
223
+
224
+ private
225
+ def _transform_transact_write_items(transact_items, dirty_items, delete_items)
226
+ transact_items.map do |item|
227
+ # this code will assume users only provided one operation, and
228
+ # will fail down the line if that assumption is wrong
229
+ if save_record = item.delete(:save)
230
+ dirty_items << save_record
231
+ _transform_save_record(save_record, item)
232
+ elsif put_record = item.delete(:put)
233
+ dirty_items << put_record
234
+ _transform_put_record(put_record, item)
235
+ elsif delete_record = item.delete(:delete)
236
+ delete_items << delete_record
237
+ _transform_delete_record(delete_record, item)
238
+ elsif update_record = item.delete(:update)
239
+ dirty_items << update_record
240
+ _transform_update_record(update_record, item)
241
+ elsif check_record = item.delete(:check)
242
+ _transform_check_record(check_record, item)
243
+ else
244
+ raise ArgumentError.new(
245
+ "Invalid transact write item, must include an operation of "\
246
+ "type :save, :update, :delete, :update, or :check - #{item}"
247
+ )
248
+ end
249
+ end
250
+ end
251
+
252
+ def _transform_save_record(save_record, opts)
253
+ # determine if record is considered a new item or not
254
+ # then create a put with conditions, or an update
255
+ if save_record.send(:expect_new_item?)
256
+ safety_expression = save_record.send(:prevent_overwrite_expression)
257
+ if opts.include?(:condition_expression)
258
+ raise Errors::TransactionalSaveConditionCollision.new(
259
+ "Transactional write includes a :save operation that would "\
260
+ "result in a 'safe put' for the given item, yet a "\
261
+ "condition expression was also provided. This is not "\
262
+ "currently supported. You should rewrite this case to use "\
263
+ "a :put transaction, adding the existence check to your "\
264
+ "own condition expression if desired.\n"\
265
+ "\tItem: #{JSON.pretty_unparse(save_record.to_h)}\n"\
266
+ "\tExtra Options: #{JSON.pretty_unparse(opts)}"
267
+ )
268
+ else
269
+ opts = opts.merge(safety_expression)
270
+ _transform_put_record(save_record, opts)
271
+ end
272
+ else
273
+ _transform_update_record(save_record, opts)
274
+ end
275
+ end
276
+
277
+ def _transform_put_record(put_record, opts)
278
+ # convert to a straight put
279
+ opts[:table_name] = put_record.class.table_name
280
+ opts[:item] = put_record.send(:_build_item_for_save)
281
+ { put: opts }
282
+ end
283
+
284
+ def _transform_delete_record(delete_record, opts)
285
+ # extract the key from each record to perform a deletion
286
+ opts[:table_name] = delete_record.class.table_name
287
+ opts[:key] = delete_record.send(:key_values)
288
+ { delete: opts }
289
+ end
290
+
291
+ def _transform_update_record(update_record, opts)
292
+ # extract dirty attribute changes to perform an update
293
+ opts[:table_name] = update_record.class.table_name
294
+ dirty_changes = update_record.send(:_dirty_changes_for_update)
295
+ update_tuple = update_record.class.send(
296
+ :_build_update_expression,
297
+ dirty_changes
298
+ )
299
+ uex, exp_attr_names, exp_attr_values = update_tuple
300
+ opts[:key] = update_record.send(:key_values)
301
+ opts[:update_expression] = uex
302
+ # need to combine expression attribute names and values
303
+ if names = opts[:expression_attribute_names]
304
+ opts[:expression_attribute_names] = exp_attr_names.merge(names)
305
+ else
306
+ opts[:expression_attribute_names] = exp_attr_names
307
+ end
308
+ if values = opts[:expression_attribute_values]
309
+ opts[:expression_attribute_values] = exp_attr_values.merge(values)
310
+ else
311
+ opts[:expression_attribute_values] = exp_attr_values
312
+ end
313
+ { update: opts }
314
+ end
315
+
316
+ def _transform_check_record(check_record, opts)
317
+ # check records are a pass-through
318
+ { condition_check: opts.merge(check_record) }
319
+ end
320
+
321
+ def _user_agent(custom)
322
+ if custom
323
+ custom
324
+ else
325
+ " aws-record/#{VERSION}"
326
+ end
327
+ end
328
+
329
+ end
330
+ end
331
+ end
332
+ end
@@ -13,6 +13,6 @@
13
13
 
14
14
  module Aws
15
15
  module Record
16
- VERSION = '2.1.2'
16
+ VERSION = '2.4.0'
17
17
  end
18
18
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aws-record
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.2
4
+ version: 2.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Amazon Web Services
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-11-15 00:00:00.000000000 Z
11
+ date: 2020-10-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk-dynamodb
@@ -16,17 +16,18 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.0'
19
+ version: '1.18'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.0'
26
+ version: '1.18'
27
27
  description: Provides an object mapping abstraction for Amazon DynamoDB.
28
28
  email:
29
- - alexwood@amazon.com
29
+ - mamuller@amazon.com
30
+ - alexwoo@amazon.com
30
31
  executables: []
31
32
  extensions: []
32
33
  extra_rdoc_files: []
@@ -35,6 +36,7 @@ files:
35
36
  - lib/aws-record/record.rb
36
37
  - lib/aws-record/record/attribute.rb
37
38
  - lib/aws-record/record/attributes.rb
39
+ - lib/aws-record/record/buildable_search.rb
38
40
  - lib/aws-record/record/dirty_tracking.rb
39
41
  - lib/aws-record/record/errors.rb
40
42
  - lib/aws-record/record/item_collection.rb
@@ -58,12 +60,13 @@ files:
58
60
  - lib/aws-record/record/secondary_indexes.rb
59
61
  - lib/aws-record/record/table_config.rb
60
62
  - lib/aws-record/record/table_migration.rb
63
+ - lib/aws-record/record/transactions.rb
61
64
  - lib/aws-record/record/version.rb
62
65
  homepage: http://github.com/aws/aws-sdk-ruby-record
63
66
  licenses:
64
67
  - Apache 2.0
65
68
  metadata: {}
66
- post_install_message:
69
+ post_install_message:
67
70
  rdoc_options: []
68
71
  require_paths:
69
72
  - lib
@@ -78,9 +81,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
78
81
  - !ruby/object:Gem::Version
79
82
  version: '0'
80
83
  requirements: []
81
- rubyforge_project:
82
- rubygems_version: 2.7.6
83
- signing_key:
84
+ rubygems_version: 3.0.3
85
+ signing_key:
84
86
  specification_version: 4
85
87
  summary: AWS Record library for Amazon DynamoDB
86
88
  test_files: []