aws-record 2.1.2 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
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: []