aws-record 2.2.0 → 2.3.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: 76f3f70bc226f5e34e377328a8829db296bffa304238da8d7542a947ba0b7a9e
4
- data.tar.gz: f22f14e087edf96717fbaffd03bc7b3917001fb5238597994143b50bc5e16894
3
+ metadata.gz: ded98837103b96b4599121d600cc139054de43637168121bae6a35302ba24ac8
4
+ data.tar.gz: 14c7d413a0b108fffd9c118aa05234b7e6d33852121e8b6828e88db6cadd45d7
5
5
  SHA512:
6
- metadata.gz: 745aff26cc027c53552b456066b56900f1489c1aa4433b8e539d1ac6f093d0cbaa62dec5054b433d2f8f2f42220471cca55dcdb6fa0d6ae1d23f542318ccd296
7
- data.tar.gz: 2e20c004f7ca252dfdff66eb4005137c74da60a964ce4862bb2390fceaf6ce2fe0f1c63bcdc00a1671f13c69dc6165b0488afc1ba4779b524ba1740c1d7f2591
6
+ metadata.gz: 722b31cd37b441f33cc9993596b7fb92dc360bc280a00d9da9f03419a792669419bd9fa58006e59f09a027bde88444d795951902e3181aa697644a89baf09b17
7
+ data.tar.gz: bd7399265054c4483039e432fb4804713428151b3cef0f8e661132e9cf30d0c6876ed5611b09fcd6063c50b0f1baa31976cbfede27aba042457a9df3ac6cdffa
@@ -27,6 +27,7 @@ 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'
30
31
  require_relative 'aws-record/record/marshalers/string_marshaler'
31
32
  require_relative 'aws-record/record/marshalers/boolean_marshaler'
32
33
  require_relative 'aws-record/record/marshalers/integer_marshaler'
@@ -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
@@ -329,6 +329,112 @@ module Aws
329
329
 
330
330
  module ItemOperationsClassMethods
331
331
 
332
+ # @example Usage Example
333
+ # check_exp = Model.transact_check_expression(
334
+ # key: { uuid: "foo" },
335
+ # condition_expression: "size(#T) <= :v",
336
+ # expression_attribute_names: {
337
+ # "#T" => "body"
338
+ # },
339
+ # expression_attribute_values: {
340
+ # ":v" => 1024
341
+ # }
342
+ # )
343
+ #
344
+ # Allows you to build a "check" expression for use in transactional
345
+ # write operations.
346
+ #
347
+ # @param [Hash] opts Options matching the :condition_check contents in
348
+ # the
349
+ # {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}
350
+ # API, with the exception that keys will be marshalled for you, and
351
+ # the table name will be provided for you by the operation.
352
+ # @return [Hash] Options suitable to be used as a check expression when
353
+ # calling the +#transact_write+ operation.
354
+ def transact_check_expression(opts)
355
+ # need to transform the key, and add the table name
356
+ opts = opts.dup
357
+ key = opts.delete(:key)
358
+ check_key = {}
359
+ @keys.keys.each_value do |attr_sym|
360
+ unless key[attr_sym]
361
+ raise Errors::KeyMissing.new(
362
+ "Missing required key #{attr_sym} in #{key}"
363
+ )
364
+ end
365
+ attr_name = attributes.storage_name_for(attr_sym)
366
+ check_key[attr_name] = attributes.attribute_for(attr_sym).
367
+ serialize(key[attr_sym])
368
+ end
369
+ opts[:key] = check_key
370
+ opts[:table_name] = table_name
371
+ opts
372
+ end
373
+
374
+ def tfind_opts(opts)
375
+ opts = opts.dup
376
+ key = opts.delete(:key)
377
+ request_key = {}
378
+ @keys.keys.each_value do |attr_sym|
379
+ unless key[attr_sym]
380
+ raise Errors::KeyMissing.new(
381
+ "Missing required key #{attr_sym} in #{key}"
382
+ )
383
+ end
384
+ attr_name = attributes.storage_name_for(attr_sym)
385
+ request_key[attr_name] = attributes.attribute_for(attr_sym).
386
+ serialize(key[attr_sym])
387
+ end
388
+ # this is a :get item used by #transact_get_items, with the exception
389
+ # of :model_class which needs to be removed before passing along
390
+ opts[:key] = request_key
391
+ opts[:table_name] = table_name
392
+ {
393
+ model_class: self,
394
+ get: opts
395
+ }
396
+ end
397
+
398
+ # @example Usage Example
399
+ # class Table
400
+ # include Aws::Record
401
+ # string_attr :hk, hash_key: true
402
+ # string_attr :rk, range_key: true
403
+ # end
404
+ #
405
+ # results = Table.transact_find(
406
+ # transact_items: [
407
+ # {key: { hk: "hk1", rk: "rk1"}},
408
+ # {key: { hk: "hk2", rk: "rk2"}}
409
+ # ]
410
+ # ) # => results.responses contains nil or instances of Table
411
+ #
412
+ # Provides a way to run a transactional find across multiple DynamoDB
413
+ # items, including transactions which get items across multiple actual
414
+ # or virtual tables.
415
+ #
416
+ # @param [Hash] opts Options to pass through to
417
+ # {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},
418
+ # with the exception of the :transact_items array, which uses the
419
+ # +#tfind_opts+ operation on your model class to provide extra
420
+ # metadata used to marshal your items after retrieval.
421
+ # @option opts [Array] :transact_items A set of options describing
422
+ # instances of the model class to return.
423
+ # @return [OpenStruct] Structured like the client API response from
424
+ # +#transact_get_items+, except that the +responses+ member contains
425
+ # +Aws::Record+ items marshaled into the model class used to call
426
+ # this method. See the usage example.
427
+ def transact_find(opts)
428
+ opts = opts.dup
429
+ transact_items = opts.delete(:transact_items)
430
+ global_transact_items = transact_items.map do |topts|
431
+ tfind_opts(topts)
432
+ end
433
+ opts[:transact_items] = global_transact_items
434
+ opts[:client] = dynamodb_client
435
+ Transactions.transact_find(opts)
436
+ end
437
+
332
438
  # @example Usage Example
333
439
  # class MyModel
334
440
  # include Aws::Record
@@ -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.2.0'
16
+ VERSION = '2.3.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.2.0
4
+ version: 2.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Amazon Web Services
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-12-05 00:00:00.000000000 Z
11
+ date: 2019-02-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk-dynamodb
@@ -58,6 +58,7 @@ files:
58
58
  - lib/aws-record/record/secondary_indexes.rb
59
59
  - lib/aws-record/record/table_config.rb
60
60
  - lib/aws-record/record/table_migration.rb
61
+ - lib/aws-record/record/transactions.rb
61
62
  - lib/aws-record/record/version.rb
62
63
  homepage: http://github.com/aws/aws-sdk-ruby-record
63
64
  licenses: