dynamoid 3.13.1 → 3.14.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/CHANGELOG.md +34 -5
- data/README.md +60 -1584
- data/lib/dynamoid/associations/association.rb +10 -2
- data/lib/dynamoid/associations/belongs_to.rb +3 -1
- data/lib/dynamoid/associations/has_and_belongs_to_many.rb +0 -1
- data/lib/dynamoid/associations/has_many.rb +0 -1
- data/lib/dynamoid/associations/has_one.rb +1 -2
- data/lib/dynamoid/associations/many_association.rb +1 -0
- data/lib/dynamoid/associations.rb +64 -48
- data/lib/dynamoid/components.rb +1 -0
- data/lib/dynamoid/criteria/chain.rb +43 -30
- data/lib/dynamoid/dirty.rb +3 -2
- data/lib/dynamoid/document.rb +29 -21
- data/lib/dynamoid/fields.rb +55 -41
- data/lib/dynamoid/finders.rb +1 -1
- data/lib/dynamoid/indexes.rb +29 -21
- data/lib/dynamoid/persistence/inc.rb +15 -7
- data/lib/dynamoid/persistence/save.rb +21 -14
- data/lib/dynamoid/persistence.rb +88 -54
- data/lib/dynamoid/transactions/mutation/base.rb +76 -0
- data/lib/dynamoid/transactions/mutation/builders/delete_request_builder.rb +44 -0
- data/lib/dynamoid/transactions/mutation/builders/update_request_builder.rb +139 -0
- data/lib/dynamoid/transactions/mutation/create.rb +52 -0
- data/lib/dynamoid/transactions/mutation/delete_with_instance.rb +80 -0
- data/lib/dynamoid/transactions/mutation/delete_with_primary_key.rb +62 -0
- data/lib/dynamoid/transactions/mutation/destroy.rb +96 -0
- data/lib/dynamoid/transactions/mutation/import.rb +84 -0
- data/lib/dynamoid/transactions/mutation/inc.rb +101 -0
- data/lib/dynamoid/transactions/mutation/increment.rb +63 -0
- data/lib/dynamoid/transactions/mutation/save.rb +194 -0
- data/lib/dynamoid/transactions/mutation/touch.rb +99 -0
- data/lib/dynamoid/transactions/mutation/update_attributes.rb +49 -0
- data/lib/dynamoid/transactions/mutation/update_fields.rb +185 -0
- data/lib/dynamoid/transactions/mutation/upsert.rb +94 -0
- data/lib/dynamoid/transactions/mutation.rb +935 -0
- data/lib/dynamoid/transactions/retrieval/find.rb +140 -0
- data/lib/dynamoid/transactions/retrieval.rb +150 -0
- data/lib/dynamoid/transactions.rb +63 -0
- data/lib/dynamoid/version.rb +1 -1
- data/lib/dynamoid.rb +1 -2
- metadata +23 -17
- data/lib/dynamoid/transaction_read/find.rb +0 -137
- data/lib/dynamoid/transaction_read.rb +0 -146
- data/lib/dynamoid/transaction_write/base.rb +0 -59
- data/lib/dynamoid/transaction_write/create.rb +0 -49
- data/lib/dynamoid/transaction_write/delete_with_instance.rb +0 -65
- data/lib/dynamoid/transaction_write/delete_with_primary_key.rb +0 -64
- data/lib/dynamoid/transaction_write/destroy.rb +0 -84
- data/lib/dynamoid/transaction_write/item_updater.rb +0 -60
- data/lib/dynamoid/transaction_write/save.rb +0 -177
- data/lib/dynamoid/transaction_write/update_attributes.rb +0 -46
- data/lib/dynamoid/transaction_write/update_fields.rb +0 -247
- data/lib/dynamoid/transaction_write/upsert.rb +0 -113
- data/lib/dynamoid/transaction_write.rb +0 -673
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative 'inc'
|
|
5
|
+
|
|
6
|
+
module Dynamoid
|
|
7
|
+
module Transactions
|
|
8
|
+
class Mutation
|
|
9
|
+
# @private
|
|
10
|
+
class Increment < Base
|
|
11
|
+
def initialize(model, attribute, by, touch: nil)
|
|
12
|
+
super()
|
|
13
|
+
|
|
14
|
+
@model = model
|
|
15
|
+
@attribute = attribute
|
|
16
|
+
@by = by
|
|
17
|
+
@touch = touch
|
|
18
|
+
@action_requests = [] # will be filled in later with Inc#action_requests
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def on_registration
|
|
22
|
+
@model.run_callbacks(:touch) do
|
|
23
|
+
@model.increment(@attribute, @by)
|
|
24
|
+
change = @model.read_attribute(@attribute) - (@model.send(:attribute_was, @attribute) || 0)
|
|
25
|
+
|
|
26
|
+
inc_action = Inc.new(@model.class, @model.hash_key, @model.range_value, { @attribute => change, touch: @touch })
|
|
27
|
+
inc_action.on_registration
|
|
28
|
+
@action_requests = inc_action.action_requests
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def on_commit
|
|
33
|
+
# ignore Inc#on_commit
|
|
34
|
+
@model.clear_attribute_changes(@attribute)
|
|
35
|
+
@model.run_callbacks(:commit)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def on_rollback
|
|
39
|
+
# ignore Inc#on_rollback
|
|
40
|
+
@model.run_callbacks(:rollback)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def aborted?
|
|
44
|
+
# ignore Inc#abort?
|
|
45
|
+
false
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def skipped?
|
|
49
|
+
# ignore Inc#skipped?
|
|
50
|
+
@by == 0 # rubocop:disable Style/NumericPredicate
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def observable_by_user_result
|
|
54
|
+
@model
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def action_requests # rubocop:disable Style/TrivialAccessors
|
|
58
|
+
@action_requests
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative 'builders/update_request_builder'
|
|
5
|
+
|
|
6
|
+
module Dynamoid
|
|
7
|
+
module Transactions
|
|
8
|
+
class Mutation
|
|
9
|
+
# @private
|
|
10
|
+
class Save < Base
|
|
11
|
+
def initialize(model, **options)
|
|
12
|
+
super()
|
|
13
|
+
|
|
14
|
+
@model = model
|
|
15
|
+
@model_class = model.class
|
|
16
|
+
@options = options
|
|
17
|
+
|
|
18
|
+
@aborted = false
|
|
19
|
+
@was_new_record = model.new_record?
|
|
20
|
+
@valid = nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def on_registration
|
|
24
|
+
if @options[:validate] != false && !(@valid = @model.valid?)
|
|
25
|
+
if @options[:raise_error]
|
|
26
|
+
raise Dynamoid::Errors::DocumentNotValid, @model
|
|
27
|
+
else
|
|
28
|
+
@aborted = true
|
|
29
|
+
return
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
@aborted = true
|
|
34
|
+
callback_name = @was_new_record ? :create : :update
|
|
35
|
+
|
|
36
|
+
@model.run_callbacks(:save) do
|
|
37
|
+
@model.run_callbacks(callback_name) do
|
|
38
|
+
@model.run_callbacks(:validate) do
|
|
39
|
+
validate_primary_key!
|
|
40
|
+
|
|
41
|
+
@aborted = false
|
|
42
|
+
true
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
if @aborted && @options[:raise_error]
|
|
48
|
+
raise Dynamoid::Errors::RecordNotSaved, @model
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
if @was_new_record && @model.hash_key.nil?
|
|
52
|
+
@model.hash_key = SecureRandom.uuid
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
if @model.class.attributes[:lock_version]
|
|
56
|
+
if @model.lock_version.nil? && @model.new_record?
|
|
57
|
+
@model.lock_version = 1
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
if @model.lock_version && !@model.changes[:lock_version]
|
|
61
|
+
@model.lock_version += 1
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def on_commit
|
|
67
|
+
return if @aborted
|
|
68
|
+
|
|
69
|
+
@model.changes_applied
|
|
70
|
+
|
|
71
|
+
if @was_new_record
|
|
72
|
+
@model.new_record = false
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
@model.run_callbacks(:commit)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def on_rollback
|
|
79
|
+
@model.run_callbacks(:rollback)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def aborted?
|
|
83
|
+
@aborted
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def skipped?
|
|
87
|
+
@model.persisted? && !@model.changed?
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def observable_by_user_result
|
|
91
|
+
!@aborted
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def action_requests
|
|
95
|
+
request = if @was_new_record
|
|
96
|
+
action_request_to_create
|
|
97
|
+
else
|
|
98
|
+
action_request_to_update
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
[request]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def validate_primary_key!
|
|
107
|
+
raise Dynamoid::Errors::MissingHashKey if !@was_new_record && @model.hash_key.nil?
|
|
108
|
+
raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @model.range_value.nil?
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def action_request_to_create
|
|
112
|
+
touch_model_timestamps(skip_created_at: false)
|
|
113
|
+
|
|
114
|
+
attributes_dumped = Dynamoid::Dumping.dump_attributes(@model.attributes, @model_class.attributes)
|
|
115
|
+
attributes_dumped = sanitize_item(attributes_dumped)
|
|
116
|
+
|
|
117
|
+
# require primary key not to exist yet
|
|
118
|
+
expression_attribute_names = { '#_h' => @model_class.hash_key }
|
|
119
|
+
condition = 'attribute_not_exists(#_h)'
|
|
120
|
+
|
|
121
|
+
if @model_class.range_key?
|
|
122
|
+
expression_attribute_names['#_r'] = @model_class.range_key
|
|
123
|
+
condition += ' AND attribute_not_exists(#_r)'
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
{
|
|
127
|
+
put: {
|
|
128
|
+
item: attributes_dumped,
|
|
129
|
+
table_name: @model_class.table_name,
|
|
130
|
+
condition_expression: condition,
|
|
131
|
+
expression_attribute_names: expression_attribute_names
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def action_request_to_update
|
|
137
|
+
touch_model_timestamps(skip_created_at: true)
|
|
138
|
+
|
|
139
|
+
# changed attributes to persist
|
|
140
|
+
changes = @model.attributes.slice(*@model.changed.map(&:to_sym))
|
|
141
|
+
changes_dumped = Dynamoid::Dumping.dump_attributes(changes, @model_class.attributes)
|
|
142
|
+
|
|
143
|
+
builder = Builders::UpdateRequestBuilder.new(@model_class)
|
|
144
|
+
builder.hash_key = dump_attribute(@model_class.hash_key, @model.hash_key)
|
|
145
|
+
builder.range_key = dump_attribute(@model_class.range_key, @model.range_value) if @model_class.range_key?
|
|
146
|
+
|
|
147
|
+
attributes_to_set = {}
|
|
148
|
+
attributes_to_remove = []
|
|
149
|
+
|
|
150
|
+
changes_dumped.each do |name, value|
|
|
151
|
+
if value || Dynamoid.config.store_attribute_with_nil_value
|
|
152
|
+
attributes_to_set[name] = value
|
|
153
|
+
else
|
|
154
|
+
attributes_to_remove << name
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
builder.set_attributes(attributes_to_set)
|
|
159
|
+
builder.remove_attributes(attributes_to_remove)
|
|
160
|
+
|
|
161
|
+
if @model_class.attributes[:lock_version]
|
|
162
|
+
lock_version = if @model.changes[:lock_version].nil?
|
|
163
|
+
@model.lock_version
|
|
164
|
+
else
|
|
165
|
+
@model.changes[:lock_version][0]
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# skip concurrency control when lock_version is nil
|
|
169
|
+
if lock_version
|
|
170
|
+
builder.add_expression_attribute_name('#_lock_version', 'lock_version')
|
|
171
|
+
builder.add_expression_attribute_value(':lock_version_value', lock_version)
|
|
172
|
+
builder.condition_expression = '#_lock_version = :lock_version_value'
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
builder.request
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def touch_model_timestamps(skip_created_at:)
|
|
180
|
+
return unless @model_class.timestamps_enabled?
|
|
181
|
+
|
|
182
|
+
timestamp = DateTime.now.in_time_zone(Time.zone)
|
|
183
|
+
@model.updated_at = timestamp unless @options[:touch] == false && !@was_new_record
|
|
184
|
+
@model.created_at ||= timestamp unless skip_created_at
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def dump_attribute(name, value)
|
|
188
|
+
options = @model_class.attributes[name]
|
|
189
|
+
Dumping.dump_field(value, options)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative 'builders/update_request_builder'
|
|
5
|
+
|
|
6
|
+
module Dynamoid
|
|
7
|
+
module Transactions
|
|
8
|
+
class Mutation
|
|
9
|
+
# @private
|
|
10
|
+
class Touch < Base
|
|
11
|
+
def initialize(model, *names, time: nil)
|
|
12
|
+
super()
|
|
13
|
+
|
|
14
|
+
@model = model
|
|
15
|
+
@names = names
|
|
16
|
+
@time = time
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def on_registration
|
|
20
|
+
if @model.new_record?
|
|
21
|
+
raise Dynamoid::Errors::Error, 'cannot touch on a new or destroyed record object'
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
validate_primary_key!
|
|
25
|
+
|
|
26
|
+
@model.run_callbacks(:touch) do
|
|
27
|
+
@time_to_assign = @time || DateTime.now.in_time_zone(Time.zone)
|
|
28
|
+
|
|
29
|
+
@model.updated_at = @time_to_assign if @model.class.timestamps_enabled?
|
|
30
|
+
@names.each do |name|
|
|
31
|
+
@model.write_attribute(name, @time_to_assign)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def on_commit
|
|
37
|
+
attribute_names = @names.map(&:to_s)
|
|
38
|
+
attribute_names << 'updated_at' if @model.class.timestamps_enabled?
|
|
39
|
+
@model.clear_attribute_changes(attribute_names)
|
|
40
|
+
@model.run_callbacks(:commit)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def on_rollback
|
|
44
|
+
@model.run_callbacks(:rollback)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def aborted?
|
|
48
|
+
false
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def skipped?
|
|
52
|
+
!@model.class.timestamps_enabled? && @names.empty?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def observable_by_user_result
|
|
56
|
+
@model
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def action_requests
|
|
60
|
+
builder = Builders::UpdateRequestBuilder.new(@model.class)
|
|
61
|
+
builder.hash_key = dump(@model.class.hash_key, @model.hash_key)
|
|
62
|
+
builder.range_key = dump(@model.class.range_key, @model.range_value) if @model.class.range_key?
|
|
63
|
+
|
|
64
|
+
# require primary key to exist
|
|
65
|
+
builder.add_expression_attribute_name('#_h', @model.class.hash_key)
|
|
66
|
+
condition_expression = 'attribute_exists(#_h)'
|
|
67
|
+
|
|
68
|
+
if @model.class.range_key?
|
|
69
|
+
builder.add_expression_attribute_name('#_r', @model.class.range_key)
|
|
70
|
+
condition_expression += ' AND attribute_exists(#_r)'
|
|
71
|
+
end
|
|
72
|
+
builder.condition_expression = condition_expression
|
|
73
|
+
|
|
74
|
+
attributes_to_set = {}
|
|
75
|
+
attributes_to_set[:updated_at] = dump(:updated_at, @model[:updated_at]) if @model.class.timestamps_enabled?
|
|
76
|
+
|
|
77
|
+
@names.each do |name|
|
|
78
|
+
attributes_to_set[name] = dump(name, @model[name])
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
builder.set_attributes(attributes_to_set)
|
|
82
|
+
[builder.request]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def validate_primary_key!
|
|
88
|
+
raise Dynamoid::Errors::MissingHashKey if @model.hash_key.nil?
|
|
89
|
+
raise Dynamoid::Errors::MissingRangeKey if @model.class.range_key? && @model.range_value.nil?
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def dump(name, value)
|
|
93
|
+
options = @model.class.attributes[name]
|
|
94
|
+
Dumping.dump_field(value, options)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require 'dynamoid/persistence/update_validations'
|
|
5
|
+
|
|
6
|
+
module Dynamoid
|
|
7
|
+
module Transactions
|
|
8
|
+
class Mutation
|
|
9
|
+
# @private
|
|
10
|
+
class UpdateAttributes < Base
|
|
11
|
+
def initialize(model, attributes, **options)
|
|
12
|
+
super()
|
|
13
|
+
|
|
14
|
+
@model = model
|
|
15
|
+
@model.assign_attributes(attributes)
|
|
16
|
+
@save_action = Save.new(model, **options)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def on_registration
|
|
20
|
+
@save_action.on_registration
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def on_commit
|
|
24
|
+
@save_action.on_commit
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def on_rollback
|
|
28
|
+
@save_action.on_rollback
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def aborted?
|
|
32
|
+
@save_action.aborted?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def skipped?
|
|
36
|
+
@save_action.skipped?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def observable_by_user_result
|
|
40
|
+
@save_action.observable_by_user_result
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def action_requests
|
|
44
|
+
@save_action.action_requests
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative 'builders/update_request_builder'
|
|
5
|
+
|
|
6
|
+
module Dynamoid
|
|
7
|
+
module Transactions
|
|
8
|
+
class Mutation
|
|
9
|
+
# @private
|
|
10
|
+
class UpdateFields < Base
|
|
11
|
+
class ItemUpdater
|
|
12
|
+
attr_reader :attributes_to_set, :attributes_to_add, :attributes_to_delete, :attributes_to_remove
|
|
13
|
+
|
|
14
|
+
def initialize(model_class)
|
|
15
|
+
@model_class = model_class
|
|
16
|
+
|
|
17
|
+
@attributes_to_set = {}
|
|
18
|
+
@attributes_to_add = {}
|
|
19
|
+
@attributes_to_delete = {}
|
|
20
|
+
@attributes_to_remove = []
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def empty?
|
|
24
|
+
[@attributes_to_set, @attributes_to_add, @attributes_to_delete, @attributes_to_remove].all?(&:empty?)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def set(attributes)
|
|
28
|
+
validate_attribute_names!(attributes.keys)
|
|
29
|
+
if Dynamoid.config.store_attribute_with_nil_value
|
|
30
|
+
@attributes_to_set.merge!(attributes)
|
|
31
|
+
else
|
|
32
|
+
@attributes_to_set.merge!(attributes.reject { |_, v| v.nil? })
|
|
33
|
+
@attributes_to_remove += attributes.select { |_, v| v.nil? }.keys
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# adds to array of fields for use in REMOVE update expression
|
|
38
|
+
def remove(*names)
|
|
39
|
+
validate_attribute_names!(names)
|
|
40
|
+
@attributes_to_remove += names
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# increments a number or adds to a set, starts at 0 or [] if it doesn't yet exist
|
|
44
|
+
def add(attributes)
|
|
45
|
+
validate_attribute_names!(attributes.keys)
|
|
46
|
+
@attributes_to_add.merge!(attributes)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# deletes a value or values from a set
|
|
50
|
+
def delete(attributes)
|
|
51
|
+
validate_attribute_names!(attributes.keys)
|
|
52
|
+
@attributes_to_delete.merge!(attributes)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def validate_attribute_names!(names)
|
|
58
|
+
UpdateFields.validate_attribute_names!(@model_class, names)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def initialize(model_class, hash_key, range_key, attributes, &block)
|
|
63
|
+
super()
|
|
64
|
+
|
|
65
|
+
@model_class = model_class
|
|
66
|
+
@hash_key = hash_key
|
|
67
|
+
@range_key = range_key
|
|
68
|
+
@attributes = attributes || {}
|
|
69
|
+
@block = block
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def on_registration
|
|
73
|
+
validate_primary_key!
|
|
74
|
+
validate_attribute_names!(@model_class, @attributes.keys)
|
|
75
|
+
|
|
76
|
+
if @block
|
|
77
|
+
@item_updater = ItemUpdater.new(@model_class)
|
|
78
|
+
@block.call(@item_updater)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def on_commit; end
|
|
83
|
+
|
|
84
|
+
def on_rollback; end
|
|
85
|
+
|
|
86
|
+
def aborted?
|
|
87
|
+
false
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def skipped?
|
|
91
|
+
@attributes.empty? && (!@item_updater || @item_updater.empty?)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def observable_by_user_result
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def action_requests
|
|
99
|
+
builder = Builders::UpdateRequestBuilder.new(@model_class)
|
|
100
|
+
|
|
101
|
+
# primary key to look up an item to update
|
|
102
|
+
builder.hash_key = cast_and_dump(@model_class.hash_key, @hash_key)
|
|
103
|
+
builder.range_key = cast_and_dump(@model_class.range_key, @range_key) if @model_class.range_key?
|
|
104
|
+
|
|
105
|
+
# require primary key to exist
|
|
106
|
+
builder.add_expression_attribute_name('#_h', @model_class.hash_key)
|
|
107
|
+
condition_expression = 'attribute_exists(#_h)'
|
|
108
|
+
|
|
109
|
+
if @model_class.range_key?
|
|
110
|
+
builder.add_expression_attribute_name('#_r', @model_class.range_key)
|
|
111
|
+
condition_expression += ' AND attribute_exists(#_r)'
|
|
112
|
+
end
|
|
113
|
+
builder.condition_expression = condition_expression
|
|
114
|
+
|
|
115
|
+
# changed attributes to persist
|
|
116
|
+
changes = add_timestamps(@attributes, skip_created_at: true)
|
|
117
|
+
changes_dumped = Dynamoid::Dumping.dump_attributes(changes, @model_class.attributes)
|
|
118
|
+
|
|
119
|
+
if Dynamoid.config.store_attribute_with_nil_value
|
|
120
|
+
builder.set_attributes(changes_dumped)
|
|
121
|
+
else
|
|
122
|
+
nil_attributes = changes_dumped.select { |_, v| v.nil? }
|
|
123
|
+
non_nil_attributes = changes_dumped.reject { |_, v| v.nil? } # rubocop:disable Style/PartitionInsteadOfDoubleSelect
|
|
124
|
+
|
|
125
|
+
builder.remove_attributes(nil_attributes.keys)
|
|
126
|
+
builder.set_attributes(non_nil_attributes)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# given a block
|
|
130
|
+
if @item_updater
|
|
131
|
+
builder.set_attributes(@item_updater.attributes_to_set)
|
|
132
|
+
builder.remove_attributes(@item_updater.attributes_to_remove)
|
|
133
|
+
|
|
134
|
+
@item_updater.attributes_to_add.each do |name, value|
|
|
135
|
+
# The ADD section in UpdateExpressions requires values to be a
|
|
136
|
+
# set to update a set attribute.
|
|
137
|
+
# Allow specifying values as any Enumerable collection (e.g. Array).
|
|
138
|
+
# Allow a single value not wrapped into a Set
|
|
139
|
+
if @model_class.attributes[name][:type] == :set
|
|
140
|
+
value = value.is_a?(Enumerable) ? Set.new(value) : Set[value]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
builder.add_value(name, value)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
@item_updater.attributes_to_delete.each do |name, value|
|
|
147
|
+
# The DELETE section in UpdateExpressions requires values to be a
|
|
148
|
+
# set to update a set attribute.
|
|
149
|
+
# Allow specifying values as any Enumerable collection (e.g. Array).
|
|
150
|
+
# Allow a single value not wrapped into a Set
|
|
151
|
+
value = value.is_a?(Enumerable) ? Set.new(value) : Set[value]
|
|
152
|
+
|
|
153
|
+
builder.delete_value(name, value)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
[builder.request]
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
private
|
|
161
|
+
|
|
162
|
+
def validate_primary_key!
|
|
163
|
+
raise Dynamoid::Errors::MissingHashKey if @hash_key.nil?
|
|
164
|
+
raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @range_key.nil?
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def add_timestamps(attributes, skip_created_at: false)
|
|
168
|
+
return attributes unless @model_class.timestamps_enabled?
|
|
169
|
+
|
|
170
|
+
result = attributes.clone
|
|
171
|
+
timestamp = DateTime.now.in_time_zone(Time.zone)
|
|
172
|
+
result[:created_at] ||= timestamp unless skip_created_at
|
|
173
|
+
result[:updated_at] ||= timestamp
|
|
174
|
+
result
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def cast_and_dump(name, value)
|
|
178
|
+
options = @model_class.attributes[name]
|
|
179
|
+
value_casted = TypeCasting.cast_field(value, options)
|
|
180
|
+
Dumping.dump_field(value_casted, options)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative 'builders/update_request_builder'
|
|
5
|
+
|
|
6
|
+
module Dynamoid
|
|
7
|
+
module Transactions
|
|
8
|
+
class Mutation
|
|
9
|
+
# @private
|
|
10
|
+
class Upsert < Base
|
|
11
|
+
def initialize(model_class, hash_key, range_key, attributes)
|
|
12
|
+
super()
|
|
13
|
+
|
|
14
|
+
@model_class = model_class
|
|
15
|
+
@hash_key = hash_key
|
|
16
|
+
@range_key = range_key
|
|
17
|
+
@attributes = attributes
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def on_registration
|
|
21
|
+
validate_primary_key!
|
|
22
|
+
validate_attribute_names!(@model_class, @attributes.keys)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def on_commit; end
|
|
26
|
+
|
|
27
|
+
def on_rollback; end
|
|
28
|
+
|
|
29
|
+
def aborted?
|
|
30
|
+
false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def skipped?
|
|
34
|
+
attributes_to_assign = @attributes.except(@model_class.hash_key, @model_class.range_key)
|
|
35
|
+
attributes_to_assign.empty? && !@model_class.timestamps_enabled?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def observable_by_user_result
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def action_requests
|
|
43
|
+
# changed attributes to persist
|
|
44
|
+
changes = @attributes.dup
|
|
45
|
+
changes = add_timestamps(changes, skip_created_at: true)
|
|
46
|
+
changes_dumped = Dynamoid::Dumping.dump_attributes(changes, @model_class.attributes)
|
|
47
|
+
|
|
48
|
+
builder = Builders::UpdateRequestBuilder.new(@model_class)
|
|
49
|
+
builder.hash_key = cast_and_dump(@model_class.hash_key, @hash_key)
|
|
50
|
+
builder.range_key = cast_and_dump(@model_class.range_key, @range_key) if @model_class.range_key?
|
|
51
|
+
|
|
52
|
+
attributes_to_set = {}
|
|
53
|
+
attributes_to_remove = []
|
|
54
|
+
|
|
55
|
+
changes_dumped.each do |name, value|
|
|
56
|
+
if value || Dynamoid.config.store_attribute_with_nil_value
|
|
57
|
+
attributes_to_set[name] = value
|
|
58
|
+
else
|
|
59
|
+
attributes_to_remove << name
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
builder.set_attributes(attributes_to_set)
|
|
64
|
+
builder.remove_attributes(attributes_to_remove)
|
|
65
|
+
|
|
66
|
+
[builder.request]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def validate_primary_key!
|
|
72
|
+
raise Dynamoid::Errors::MissingHashKey if @hash_key.nil?
|
|
73
|
+
raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @range_key.nil?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def add_timestamps(attributes, skip_created_at: false)
|
|
77
|
+
return attributes unless @model_class.timestamps_enabled?
|
|
78
|
+
|
|
79
|
+
result = attributes.clone
|
|
80
|
+
timestamp = DateTime.now.in_time_zone(Time.zone)
|
|
81
|
+
result[:created_at] ||= timestamp unless skip_created_at
|
|
82
|
+
result[:updated_at] ||= timestamp
|
|
83
|
+
result
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def cast_and_dump(name, value)
|
|
87
|
+
options = @model_class.attributes[name]
|
|
88
|
+
value_casted = TypeCasting.cast_field(value, options)
|
|
89
|
+
Dumping.dump_field(value_casted, options)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|