aws-record 2.2.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 76f3f70bc226f5e34e377328a8829db296bffa304238da8d7542a947ba0b7a9e
4
- data.tar.gz: f22f14e087edf96717fbaffd03bc7b3917001fb5238597994143b50bc5e16894
3
+ metadata.gz: 309ae7c37a70b41c9bb7695bd0c79a5faa83195c764986f28d43becc61f82f09
4
+ data.tar.gz: d13944acd83e60191102e72078cd399190dfc0632a670a791134c33382578260
5
5
  SHA512:
6
- metadata.gz: 745aff26cc027c53552b456066b56900f1489c1aa4433b8e539d1ac6f093d0cbaa62dec5054b433d2f8f2f42220471cca55dcdb6fa0d6ae1d23f542318ccd296
7
- data.tar.gz: 2e20c004f7ca252dfdff66eb4005137c74da60a964ce4862bb2390fceaf6ce2fe0f1c63bcdc00a1671f13c69dc6165b0488afc1ba4779b524ba1740c1d7f2591
6
+ metadata.gz: c4ad5ee650e8d785eee3fac8b5f33f5e2db82ea3fd903bbde374e5d85920ec92c85c724fe55ed913befcb5ad5b1c27e9d38ebc3c41a7ae4c3eefe9cba45ea690
7
+ data.tar.gz: 8f014faf8d060af4db13acde3cdbc67e8a34a534c612c8a7b739911d1ea568743fb899ff88b11474e46417af7ad25761dd3a7cabda850c96c79297ef4b79981c
data/lib/aws-record.rb CHANGED
@@ -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
@@ -56,15 +56,24 @@ module Aws
56
56
  # @param [Hash] opts options to pass on to the client call to
57
57
  # +#create_table+. See the documentation above in the AWS SDK for Ruby
58
58
  # V2.
59
- # @option opts [Hash] :provisioned_throughput This is a required argument,
60
- # in which you must specify the +:read_capacity_units+ and
59
+ # @option opts [Hash] :billing_mode Accepts values 'PAY_PER_REQUEST' or
60
+ # 'PROVISIONED'. If :provisioned_throughput option is specified, this
61
+ # option is not required, as 'PROVISIONED' is assumed. If
62
+ # :provisioned_throughput is not specified, this option is required
63
+ # and must be set to 'PAY_PER_REQUEST'.
64
+ # @option opts [Hash] :provisioned_throughput Unless :billing_mode is
65
+ # set to 'PAY_PER_REQUEST', this is a required argument, in which
66
+ # you must specify the +:read_capacity_units+ and
61
67
  # +:write_capacity_units+ of your new table.
62
68
  # @option opts [Hash] :global_secondary_index_throughput This argument is
63
- # required if you define any global secondary indexes. It should map your
69
+ # required if you define any global secondary indexes, unless
70
+ # :billing_mode is set to 'PAY_PER_REQUEST'. It should map your
64
71
  # global secondary index names to their provisioned throughput, similar
65
72
  # to how you define the provisioned throughput for the table in general.
66
73
  def create!(opts)
67
74
  gsit = opts.delete(:global_secondary_index_throughput)
75
+ _validate_billing(opts)
76
+
68
77
  create_opts = opts.merge({
69
78
  table_name: @model.table_name,
70
79
  attribute_definitions: _attribute_definitions,
@@ -75,14 +84,19 @@ module Aws
75
84
  _append_to_attribute_definitions(lsis, create_opts)
76
85
  end
77
86
  if gsis = @model.global_secondary_indexes_for_migration
78
- unless gsit
87
+ unless gsit || opts[:billing_mode] == 'PAY_PER_REQUEST'
79
88
  raise ArgumentError.new(
80
- "If you define global secondary indexes, you must also define"\
81
- " :global_secondary_index_throughput on table creation."
89
+ 'If you define global secondary indexes, you must also define'\
90
+ ' :global_secondary_index_throughput on table creation,'\
91
+ " unless :billing_mode is set to 'PAY_PER_REQUEST'."
82
92
  )
83
93
  end
84
- gsis_with_throughput = _add_throughout_to_gsis(gsis, gsit)
85
- create_opts[:global_secondary_indexes] = gsis_with_throughput
94
+ gsis_opts = if opts[:billing_mode] == 'PAY_PER_REQUEST'
95
+ gsis
96
+ else
97
+ _add_throughput_to_gsis(gsis, gsit)
98
+ end
99
+ create_opts[:global_secondary_indexes] = gsis_opts
86
100
  _append_to_attribute_definitions(gsis, create_opts)
87
101
  end
88
102
  @client.create_table(create_opts)
@@ -142,6 +156,33 @@ module Aws
142
156
  end
143
157
  end
144
158
 
159
+ def _validate_billing(opts)
160
+ valid_modes = %w[PAY_PER_REQUEST PROVISIONED]
161
+ if opts.key?(:billing_mode)
162
+ unless valid_modes.include?(opts[:billing_mode])
163
+ raise ArgumentError.new(
164
+ ":billing_mode option must be one of #{valid_modes.join(', ')}"\
165
+ " current value is: #{opts[:billing_mode]}"
166
+ )
167
+ end
168
+ end
169
+ if opts.key?(:provisioned_throughput)
170
+ if opts[:billing_mode] == 'PAY_PER_REQUEST'
171
+ raise ArgumentError.new(
172
+ 'when :provisioned_throughput option is specified, :billing_mode'\
173
+ " must either be unspecified or have a value of 'PROVISIONED'"
174
+ )
175
+ end
176
+ else
177
+ if opts[:billing_mode] != 'PAY_PER_REQUEST'
178
+ raise ArgumentError.new(
179
+ 'when :provisioned_throughput option is not specified,'\
180
+ " :billing_mode must be set to 'PAY_PER_REQUEST'"
181
+ )
182
+ end
183
+ end
184
+ end
185
+
145
186
  def _attribute_definitions
146
187
  _keys.map do |type, attr|
147
188
  {
@@ -173,7 +214,7 @@ module Aws
173
214
  create_opts[:attribute_definitions] = attr_def
174
215
  end
175
216
 
176
- def _add_throughout_to_gsis(global_secondary_indexes, gsi_throughput)
217
+ def _add_throughput_to_gsis(global_secondary_indexes, gsi_throughput)
177
218
  missing_throughput = []
178
219
  ret = global_secondary_indexes.map do |params|
179
220
  name = params[:index_name]
@@ -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.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.2.0
4
+ version: 2.6.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-12-05 00:00:00.000000000 Z
11
+ date: 2021-05-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk-dynamodb
@@ -26,7 +26,8 @@ dependencies:
26
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.2.3
85
+ signing_key:
84
86
  specification_version: 4
85
87
  summary: AWS Record library for Amazon DynamoDB
86
88
  test_files: []