dynamoid 3.10.0 → 3.12.1
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/CHANGELOG.md +37 -1
- data/README.md +268 -8
- data/dynamoid.gemspec +4 -4
- data/lib/dynamoid/adapter.rb +1 -1
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/filter_expression_convertor.rb +53 -18
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +5 -4
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/projection_expression_convertor.rb +9 -7
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +1 -1
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +1 -1
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/transact.rb +31 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +17 -5
- data/lib/dynamoid/components.rb +1 -0
- data/lib/dynamoid/config.rb +3 -0
- data/lib/dynamoid/criteria/chain.rb +74 -21
- data/lib/dynamoid/criteria/where_conditions.rb +13 -6
- data/lib/dynamoid/dirty.rb +97 -11
- data/lib/dynamoid/dumping.rb +39 -17
- data/lib/dynamoid/errors.rb +30 -3
- data/lib/dynamoid/fields.rb +13 -3
- data/lib/dynamoid/finders.rb +44 -23
- data/lib/dynamoid/loadable.rb +1 -0
- data/lib/dynamoid/persistence/inc.rb +35 -19
- data/lib/dynamoid/persistence/item_updater_with_casting_and_dumping.rb +36 -0
- data/lib/dynamoid/persistence/item_updater_with_dumping.rb +33 -0
- data/lib/dynamoid/persistence/save.rb +29 -14
- data/lib/dynamoid/persistence/update_fields.rb +23 -8
- data/lib/dynamoid/persistence/update_validations.rb +3 -3
- data/lib/dynamoid/persistence/upsert.rb +22 -8
- data/lib/dynamoid/persistence.rb +184 -28
- data/lib/dynamoid/transaction_read/find.rb +137 -0
- data/lib/dynamoid/transaction_read.rb +146 -0
- data/lib/dynamoid/transaction_write/base.rb +47 -0
- data/lib/dynamoid/transaction_write/create.rb +49 -0
- data/lib/dynamoid/transaction_write/delete_with_instance.rb +65 -0
- data/lib/dynamoid/transaction_write/delete_with_primary_key.rb +64 -0
- data/lib/dynamoid/transaction_write/destroy.rb +84 -0
- data/lib/dynamoid/transaction_write/item_updater.rb +55 -0
- data/lib/dynamoid/transaction_write/save.rb +169 -0
- data/lib/dynamoid/transaction_write/update_attributes.rb +46 -0
- data/lib/dynamoid/transaction_write/update_fields.rb +239 -0
- data/lib/dynamoid/transaction_write/upsert.rb +106 -0
- data/lib/dynamoid/transaction_write.rb +673 -0
- data/lib/dynamoid/type_casting.rb +3 -1
- data/lib/dynamoid/undumping.rb +13 -2
- data/lib/dynamoid/validations.rb +8 -5
- data/lib/dynamoid/version.rb +1 -1
- data/lib/dynamoid.rb +8 -0
- metadata +21 -5
@@ -0,0 +1,239 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
require 'dynamoid/persistence/update_validations'
|
5
|
+
|
6
|
+
module Dynamoid
|
7
|
+
class TransactionWrite
|
8
|
+
class UpdateFields < Base
|
9
|
+
def initialize(model_class, hash_key, range_key, attributes, &block)
|
10
|
+
super()
|
11
|
+
|
12
|
+
@model_class = model_class
|
13
|
+
@hash_key = hash_key
|
14
|
+
@range_key = range_key
|
15
|
+
@attributes = attributes || {}
|
16
|
+
@block = block
|
17
|
+
end
|
18
|
+
|
19
|
+
def on_registration
|
20
|
+
validate_primary_key!
|
21
|
+
Dynamoid::Persistence::UpdateValidations.validate_attributes_exist(@model_class, @attributes)
|
22
|
+
|
23
|
+
if @block
|
24
|
+
@item_updater = ItemUpdater.new(@model_class)
|
25
|
+
@block.call(@item_updater)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def on_commit; end
|
30
|
+
|
31
|
+
def on_rollback; end
|
32
|
+
|
33
|
+
def aborted?
|
34
|
+
false
|
35
|
+
end
|
36
|
+
|
37
|
+
def skipped?
|
38
|
+
@attributes.empty? && (!@item_updater || @item_updater.empty?)
|
39
|
+
end
|
40
|
+
|
41
|
+
def observable_by_user_result
|
42
|
+
nil
|
43
|
+
end
|
44
|
+
|
45
|
+
def action_request
|
46
|
+
builder = UpdateRequestBuilder.new(@model_class)
|
47
|
+
|
48
|
+
# primary key to look up an item to update
|
49
|
+
builder.hash_key = dump_attribute(@model_class.hash_key, @hash_key)
|
50
|
+
builder.range_key = dump_attribute(@model_class.range_key, @range_key) if @model_class.range_key?
|
51
|
+
|
52
|
+
# changed attributes to persist
|
53
|
+
changes = @attributes.dup
|
54
|
+
changes = add_timestamps(changes, skip_created_at: true)
|
55
|
+
changes_dumped = Dynamoid::Dumping.dump_attributes(changes, @model_class.attributes)
|
56
|
+
|
57
|
+
builder.set_attributes(changes_dumped)
|
58
|
+
|
59
|
+
# given a block
|
60
|
+
if @item_updater
|
61
|
+
builder.set_attributes(@item_updater.attributes_to_set)
|
62
|
+
builder.remove_attributes(@item_updater.attributes_to_remove)
|
63
|
+
|
64
|
+
@item_updater.attributes_to_add.each do |name, value|
|
65
|
+
# The ADD section in UpdateExpressions requires values to be a
|
66
|
+
# set to update a set attribute.
|
67
|
+
# Allow specifying values as any Enumerable collection (e.g. Array).
|
68
|
+
# Allow a single value not wrapped into a Set
|
69
|
+
if @model_class.attributes[name][:type] == :set
|
70
|
+
value = value.is_a?(Enumerable) ? Set.new(value) : Set[value]
|
71
|
+
end
|
72
|
+
|
73
|
+
builder.add_value(name, value)
|
74
|
+
end
|
75
|
+
|
76
|
+
@item_updater.attributes_to_delete.each do |name, value|
|
77
|
+
# The DELETE section in UpdateExpressions requires values to be a
|
78
|
+
# set to update a set attribute.
|
79
|
+
# Allow specifying values as any Enumerable collection (e.g. Array).
|
80
|
+
# Allow a single value not wrapped into a Set
|
81
|
+
value = value.is_a?(Enumerable) ? Set.new(value) : Set[value]
|
82
|
+
|
83
|
+
builder.delete_value(name, value)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# require primary key to exist
|
88
|
+
condition_expression = "attribute_exists(#{@model_class.hash_key})"
|
89
|
+
if @model_class.range_key?
|
90
|
+
condition_expression += " AND attribute_exists(#{@model_class.range_key})"
|
91
|
+
end
|
92
|
+
builder.condition_expression = condition_expression
|
93
|
+
|
94
|
+
builder.request
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def validate_primary_key!
|
100
|
+
raise Dynamoid::Errors::MissingHashKey if @hash_key.nil?
|
101
|
+
raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @range_key.nil?
|
102
|
+
end
|
103
|
+
|
104
|
+
def add_timestamps(attributes, skip_created_at: false)
|
105
|
+
return attributes unless @model_class.timestamps_enabled?
|
106
|
+
|
107
|
+
result = attributes.clone
|
108
|
+
timestamp = DateTime.now.in_time_zone(Time.zone)
|
109
|
+
result[:created_at] ||= timestamp unless skip_created_at
|
110
|
+
result[:updated_at] ||= timestamp
|
111
|
+
result
|
112
|
+
end
|
113
|
+
|
114
|
+
def dump_attribute(name, value)
|
115
|
+
options = @model_class.attributes[name]
|
116
|
+
Dumping.dump_field(value, options)
|
117
|
+
end
|
118
|
+
|
119
|
+
class UpdateRequestBuilder
|
120
|
+
attr_writer :hash_key, :range_key, :condition_expression
|
121
|
+
|
122
|
+
def initialize(model_class)
|
123
|
+
@model_class = model_class
|
124
|
+
|
125
|
+
@attributes_to_set = {}
|
126
|
+
@attributes_to_add = {}
|
127
|
+
@attributes_to_delete = {}
|
128
|
+
@attributes_to_remove = []
|
129
|
+
@condition_expression = nil
|
130
|
+
end
|
131
|
+
|
132
|
+
def set_attributes(attributes) # rubocop:disable Naming/AccessorMethodName
|
133
|
+
@attributes_to_set.merge!(attributes)
|
134
|
+
end
|
135
|
+
|
136
|
+
def add_value(name, value)
|
137
|
+
@attributes_to_add[name] = value
|
138
|
+
end
|
139
|
+
|
140
|
+
def delete_value(name, value)
|
141
|
+
@attributes_to_delete[name] = value
|
142
|
+
end
|
143
|
+
|
144
|
+
def remove_attributes(names)
|
145
|
+
@attributes_to_remove.concat(names)
|
146
|
+
end
|
147
|
+
|
148
|
+
def request
|
149
|
+
key = { @model_class.hash_key => @hash_key }
|
150
|
+
key[@model_class.range_key] = @range_key if @model_class.range_key?
|
151
|
+
|
152
|
+
# Build UpdateExpression and keep names and values placeholders mapping
|
153
|
+
# in ExpressionAttributeNames and ExpressionAttributeValues.
|
154
|
+
update_expression_statements = []
|
155
|
+
expression_attribute_names = {}
|
156
|
+
expression_attribute_values = {}
|
157
|
+
name_placeholder = '#_n0'
|
158
|
+
value_placeholder = ':_v0'
|
159
|
+
|
160
|
+
unless @attributes_to_set.empty?
|
161
|
+
statements = []
|
162
|
+
|
163
|
+
@attributes_to_set.each do |name, value|
|
164
|
+
statements << "#{name_placeholder} = #{value_placeholder}"
|
165
|
+
|
166
|
+
expression_attribute_names[name_placeholder] = name
|
167
|
+
expression_attribute_values[value_placeholder] = value
|
168
|
+
|
169
|
+
name_placeholder = name_placeholder.succ
|
170
|
+
value_placeholder = value_placeholder.succ
|
171
|
+
end
|
172
|
+
|
173
|
+
update_expression_statements << "SET #{statements.join(', ')}"
|
174
|
+
end
|
175
|
+
|
176
|
+
unless @attributes_to_add.empty?
|
177
|
+
statements = []
|
178
|
+
|
179
|
+
@attributes_to_add.each do |name, value|
|
180
|
+
statements << "#{name_placeholder} #{value_placeholder}"
|
181
|
+
|
182
|
+
expression_attribute_names[name_placeholder] = name
|
183
|
+
expression_attribute_values[value_placeholder] = value
|
184
|
+
|
185
|
+
name_placeholder = name_placeholder.succ
|
186
|
+
value_placeholder = value_placeholder.succ
|
187
|
+
end
|
188
|
+
|
189
|
+
update_expression_statements << "ADD #{statements.join(', ')}"
|
190
|
+
end
|
191
|
+
|
192
|
+
unless @attributes_to_delete.empty?
|
193
|
+
statements = []
|
194
|
+
|
195
|
+
@attributes_to_delete.each do |name, value|
|
196
|
+
statements << "#{name_placeholder} #{value_placeholder}"
|
197
|
+
|
198
|
+
expression_attribute_names[name_placeholder] = name
|
199
|
+
expression_attribute_values[value_placeholder] = value
|
200
|
+
|
201
|
+
name_placeholder = name_placeholder.succ
|
202
|
+
value_placeholder = value_placeholder.succ
|
203
|
+
end
|
204
|
+
|
205
|
+
update_expression_statements << "DELETE #{statements.join(', ')}"
|
206
|
+
end
|
207
|
+
|
208
|
+
unless @attributes_to_remove.empty?
|
209
|
+
name_placeholders = []
|
210
|
+
|
211
|
+
@attributes_to_remove.each do |name|
|
212
|
+
name_placeholders << name_placeholder
|
213
|
+
|
214
|
+
expression_attribute_names[name_placeholder] = name
|
215
|
+
|
216
|
+
name_placeholder = name_placeholder.succ
|
217
|
+
value_placeholder = value_placeholder.succ
|
218
|
+
end
|
219
|
+
|
220
|
+
update_expression_statements << "REMOVE #{name_placeholders.join(', ')}"
|
221
|
+
end
|
222
|
+
|
223
|
+
update_expression = update_expression_statements.join(' ')
|
224
|
+
|
225
|
+
{
|
226
|
+
update: {
|
227
|
+
key: key,
|
228
|
+
table_name: @model_class.table_name,
|
229
|
+
update_expression: update_expression,
|
230
|
+
expression_attribute_names: expression_attribute_names,
|
231
|
+
expression_attribute_values: expression_attribute_values,
|
232
|
+
condition_expression: @condition_expression
|
233
|
+
}
|
234
|
+
}
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
require 'dynamoid/persistence/update_validations'
|
5
|
+
|
6
|
+
module Dynamoid
|
7
|
+
class TransactionWrite
|
8
|
+
class Upsert < Base
|
9
|
+
def initialize(model_class, hash_key, range_key, attributes)
|
10
|
+
super()
|
11
|
+
|
12
|
+
@model_class = model_class
|
13
|
+
@hash_key = hash_key
|
14
|
+
@range_key = range_key
|
15
|
+
@attributes = attributes
|
16
|
+
end
|
17
|
+
|
18
|
+
def on_registration
|
19
|
+
validate_primary_key!
|
20
|
+
Dynamoid::Persistence::UpdateValidations.validate_attributes_exist(@model_class, @attributes)
|
21
|
+
end
|
22
|
+
|
23
|
+
def on_commit; end
|
24
|
+
|
25
|
+
def on_rollback; end
|
26
|
+
|
27
|
+
def aborted?
|
28
|
+
false
|
29
|
+
end
|
30
|
+
|
31
|
+
def skipped?
|
32
|
+
attributes_to_assign = @attributes.except(@model_class.hash_key, @model_class.range_key)
|
33
|
+
attributes_to_assign.empty? && !@model_class.timestamps_enabled?
|
34
|
+
end
|
35
|
+
|
36
|
+
def observable_by_user_result
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def action_request
|
41
|
+
# changed attributes to persist
|
42
|
+
changes = @attributes.dup
|
43
|
+
changes = add_timestamps(changes, skip_created_at: true)
|
44
|
+
changes_dumped = Dynamoid::Dumping.dump_attributes(changes, @model_class.attributes)
|
45
|
+
|
46
|
+
# primary key to look up an item to update
|
47
|
+
partition_key_dumped = dump(@model_class.hash_key, @hash_key)
|
48
|
+
key = { @model_class.hash_key => partition_key_dumped }
|
49
|
+
|
50
|
+
if @model_class.range_key?
|
51
|
+
sort_key_dumped = dump(@model_class.range_key, @range_key)
|
52
|
+
key[@model_class.range_key] = sort_key_dumped
|
53
|
+
end
|
54
|
+
|
55
|
+
# Build UpdateExpression and keep names and values placeholders mapping
|
56
|
+
# in ExpressionAttributeNames and ExpressionAttributeValues.
|
57
|
+
update_expression_statements = []
|
58
|
+
expression_attribute_names = {}
|
59
|
+
expression_attribute_values = {}
|
60
|
+
|
61
|
+
changes_dumped.each_with_index do |(name, value), i|
|
62
|
+
name_placeholder = "#_n#{i}"
|
63
|
+
value_placeholder = ":_s#{i}"
|
64
|
+
|
65
|
+
update_expression_statements << "#{name_placeholder} = #{value_placeholder}"
|
66
|
+
expression_attribute_names[name_placeholder] = name
|
67
|
+
expression_attribute_values[value_placeholder] = value
|
68
|
+
end
|
69
|
+
|
70
|
+
update_expression = "SET #{update_expression_statements.join(', ')}"
|
71
|
+
|
72
|
+
{
|
73
|
+
update: {
|
74
|
+
key: key,
|
75
|
+
table_name: @model_class.table_name,
|
76
|
+
update_expression: update_expression,
|
77
|
+
expression_attribute_names: expression_attribute_names,
|
78
|
+
expression_attribute_values: expression_attribute_values
|
79
|
+
}
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def validate_primary_key!
|
86
|
+
raise Dynamoid::Errors::MissingHashKey if @hash_key.nil?
|
87
|
+
raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @range_key.nil?
|
88
|
+
end
|
89
|
+
|
90
|
+
def add_timestamps(attributes, skip_created_at: false)
|
91
|
+
return attributes unless @model_class.timestamps_enabled?
|
92
|
+
|
93
|
+
result = attributes.clone
|
94
|
+
timestamp = DateTime.now.in_time_zone(Time.zone)
|
95
|
+
result[:created_at] ||= timestamp unless skip_created_at
|
96
|
+
result[:updated_at] ||= timestamp
|
97
|
+
result
|
98
|
+
end
|
99
|
+
|
100
|
+
def dump(name, value)
|
101
|
+
options = @model_class.attributes[name]
|
102
|
+
Dumping.dump_field(value, options)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|