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 +4 -4
- data/lib/aws-record.rb +2 -0
- data/lib/aws-record/record/attribute.rb +10 -4
- data/lib/aws-record/record/buildable_search.rb +276 -0
- data/lib/aws-record/record/errors.rb +8 -0
- data/lib/aws-record/record/item_collection.rb +5 -2
- data/lib/aws-record/record/item_data.rb +2 -2
- data/lib/aws-record/record/item_operations.rb +107 -2
- data/lib/aws-record/record/marshalers/numeric_set_marshaler.rb +1 -1
- data/lib/aws-record/record/query.rb +48 -0
- data/lib/aws-record/record/table_migration.rb +50 -9
- data/lib/aws-record/record/transactions.rb +332 -0
- data/lib/aws-record/record/version.rb +1 -1
- metadata +10 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 309ae7c37a70b41c9bb7695bd0c79a5faa83195c764986f28d43becc61f82f09
|
|
4
|
+
data.tar.gz: d13944acd83e60191102e72078cd399190dfc0632a670a791134c33382578260
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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]
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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] =
|
|
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
|
-
#
|
|
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
|
|
@@ -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] :
|
|
60
|
-
#
|
|
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
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
85
|
-
|
|
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
|
|
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
|
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.
|
|
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:
|
|
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
|
-
-
|
|
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
|
-
|
|
82
|
-
|
|
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: []
|