dynamoid 3.11.0 → 3.13.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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +39 -3
  3. data/README.md +94 -14
  4. data/SECURITY.md +6 -6
  5. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/batch_get_item.rb +3 -1
  6. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +1 -1
  7. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +4 -1
  8. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +4 -1
  9. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/table.rb +13 -0
  10. data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +24 -9
  11. data/lib/dynamoid/config.rb +1 -0
  12. data/lib/dynamoid/criteria/chain.rb +11 -3
  13. data/lib/dynamoid/dirty.rb +22 -11
  14. data/lib/dynamoid/dumping.rb +3 -3
  15. data/lib/dynamoid/errors.rb +16 -1
  16. data/lib/dynamoid/fields/declare.rb +1 -1
  17. data/lib/dynamoid/fields.rb +44 -4
  18. data/lib/dynamoid/finders.rb +44 -19
  19. data/lib/dynamoid/persistence/inc.rb +30 -13
  20. data/lib/dynamoid/persistence/save.rb +24 -12
  21. data/lib/dynamoid/persistence/update_fields.rb +18 -5
  22. data/lib/dynamoid/persistence/update_validations.rb +3 -3
  23. data/lib/dynamoid/persistence/upsert.rb +17 -4
  24. data/lib/dynamoid/persistence.rb +273 -19
  25. data/lib/dynamoid/transaction_read/find.rb +137 -0
  26. data/lib/dynamoid/transaction_read.rb +146 -0
  27. data/lib/dynamoid/transaction_write/base.rb +12 -0
  28. data/lib/dynamoid/transaction_write/delete_with_instance.rb +7 -2
  29. data/lib/dynamoid/transaction_write/delete_with_primary_key.rb +7 -2
  30. data/lib/dynamoid/transaction_write/destroy.rb +10 -5
  31. data/lib/dynamoid/transaction_write/item_updater.rb +60 -0
  32. data/lib/dynamoid/transaction_write/save.rb +22 -9
  33. data/lib/dynamoid/transaction_write/update_fields.rb +176 -31
  34. data/lib/dynamoid/transaction_write/upsert.rb +23 -6
  35. data/lib/dynamoid/transaction_write.rb +212 -3
  36. data/lib/dynamoid/validations.rb +15 -4
  37. data/lib/dynamoid/version.rb +1 -1
  38. data/lib/dynamoid.rb +1 -0
  39. metadata +9 -9
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dynamoid/transaction_read/find'
4
+
5
+ module Dynamoid
6
+ # The class +TransactionRead+ provides means to perform multiple reading
7
+ # operations in transaction, that is atomically, so that either all of them
8
+ # succeed, or all of them fail.
9
+ #
10
+ # The reading methods are supposed to be as close as possible to their
11
+ # non-transactional counterparts:
12
+ #
13
+ # user_id = params[:user_id]
14
+ # payment = params[:payment_id]
15
+ #
16
+ # models = Dynamoid::TransactionRead.execute do |t|
17
+ # t.find User, user_id
18
+ # t.find Payment, payment_id
19
+ # end
20
+ #
21
+ # The only difference is that the methods are called on a transaction
22
+ # instance and a model or a model class should be specified. So +User.find+
23
+ # becomes +t.find(user_id)+ and +Payment.find(payment_id)+ becomes +t.find
24
+ # Payment, payment_id+.
25
+ #
26
+ # A transaction can be used without a block. This way a transaction instance
27
+ # should be instantiated and committed manually with +#commit+ method:
28
+ #
29
+ # t = Dynamoid::TransactionRead.new
30
+ #
31
+ # t.find user_id
32
+ # t.find payment_id
33
+ #
34
+ # models = t.commit
35
+ #
36
+ #
37
+ # ### DynamoDB's transactions
38
+ #
39
+ # The main difference between DynamoDB transactions and a common interface is
40
+ # that DynamoDB's transactions are executed in batch. So in Dynamoid no
41
+ # data actually loaded when some transactional method (e.g+ `#find+) is
42
+ # called. All the changes are loaded at the end.
43
+ #
44
+ # A +TransactGetItems+ DynamoDB operation is used (see
45
+ # [documentation](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactGetItems.html)
46
+ # for details).
47
+ class TransactionRead
48
+ def self.execute
49
+ transaction = new
50
+
51
+ begin
52
+ yield transaction
53
+ transaction.commit
54
+ end
55
+ end
56
+
57
+ def initialize
58
+ @actions = []
59
+ end
60
+
61
+ # Load all the models.
62
+ #
63
+ # transaction = Dynamoid::TransactionRead.new
64
+ # # ...
65
+ # transaction.commit
66
+ def commit
67
+ return [] if @actions.empty?
68
+
69
+ # some actions may produce multiple requests
70
+ action_request_groups = @actions.map(&:action_request).map do |action_request|
71
+ action_request.is_a?(Array) ? action_request : [action_request]
72
+ end
73
+ action_requests = action_request_groups.flatten(1)
74
+
75
+ return [] if action_requests.empty?
76
+
77
+ response = Dynamoid.adapter.transact_read_items(action_requests)
78
+
79
+ responses = response.responses.dup
80
+ @actions.zip(action_request_groups).map do |action, action_requests|
81
+ action_responses = responses.shift(action_requests.size)
82
+ action.process_responses(action_responses)
83
+ end.flatten
84
+ end
85
+
86
+ # Find one or many objects, specified by one id or an array of ids.
87
+ #
88
+ # By default it raises +RecordNotFound+ exception if at least one model
89
+ # isn't found. This behavior can be changed with +raise_error+ option. If
90
+ # specified +raise_error: false+ option then +find+ will not raise the
91
+ # exception.
92
+ #
93
+ # When a document schema includes range key it should always be specified
94
+ # in +find+ method call. In case it's missing +MissingRangeKey+ exception
95
+ # will be raised.
96
+ #
97
+ # Please note that there are the following differences between
98
+ # transactional and non-transactional +find+:
99
+ # - transactional +find+ preserves order of models in result when given multiple ids
100
+ # - transactional +find+ doesn't return results immediately, a single
101
+ # collection with results of all the +find+ calls is returned instead
102
+ # - +:consistent_read+ option isn't supported
103
+ # - transactional +find+ is subject to limitations of the
104
+ # [TransactGetItems](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_TransactGetItems.html)
105
+ # DynamoDB operation, e.g. the whole read transaction can load only up to 100 models.
106
+ #
107
+ # @param [Object|Array] ids a single primary key or an array of primary keys
108
+ # @param [Hash] options optional parameters of the operation
109
+ # @option options [Object] :range_key sort key of a model; required when a single partition key is given and a sort key is declared for a model
110
+ # @option options [true|false] :raise_error whether to raise a +RecordNotFound+ exception; specify explicitly +raise_error: false+ to suppress the exception; default is +true+
111
+ # @return [nil]
112
+ #
113
+ # @example Find by partition key
114
+ # Dynamoid::TransactionRead.execute do |t|
115
+ # t.find Document, 101
116
+ # end
117
+ #
118
+ # @example Find by partition key and sort key
119
+ # Dynamoid::TransactionRead.execute do |t|
120
+ # t.find Document, 101, range_key: 'archived'
121
+ # end
122
+ #
123
+ # @example Find several documents by partition key
124
+ # Dynamoid::TransactionRead.execute do |t|
125
+ # t.find Document, 101, 102, 103
126
+ # t.find Document, [101, 102, 103]
127
+ # end
128
+ #
129
+ # @example Find several documents by partition key and sort key
130
+ # Dynamoid::TransactionRead.execute do |t|
131
+ # t.find Document, [[101, 'archived'], [102, 'new'], [103, 'deleted']]
132
+ # end
133
+ def find(model_class, *ids, **options)
134
+ action = Dynamoid::TransactionRead::Find.new(model_class, *ids, **options)
135
+ register_action action
136
+ end
137
+
138
+ private
139
+
140
+ def register_action(action)
141
+ @actions << action
142
+ action.on_registration
143
+ action.observable_by_user_result
144
+ end
145
+ end
146
+ end
@@ -42,6 +42,18 @@ module Dynamoid
42
42
  def action_request
43
43
  raise 'Not implemented'
44
44
  end
45
+
46
+ # copied from aws_sdk_v3
47
+ def sanitize_item(attributes)
48
+ config_value = Dynamoid.config.store_attribute_with_nil_value
49
+ store_attribute_with_nil_value = config_value.nil? ? false : !!config_value
50
+
51
+ attributes.reject do |_, v|
52
+ !store_attribute_with_nil_value && v.nil?
53
+ end.transform_values do |v|
54
+ v.is_a?(Hash) ? v.stringify_keys : v
55
+ end
56
+ end
45
57
  end
46
58
  end
47
59
  end
@@ -35,10 +35,10 @@ module Dynamoid
35
35
  end
36
36
 
37
37
  def action_request
38
- key = { @model_class.hash_key => @model.hash_key }
38
+ key = { @model_class.hash_key => dump_attribute(@model_class.hash_key, @model.hash_key) }
39
39
 
40
40
  if @model_class.range_key?
41
- key[@model_class.range_key] = @model.range_value
41
+ key[@model_class.range_key] = dump_attribute(@model_class.range_key, @model.range_value)
42
42
  end
43
43
 
44
44
  {
@@ -55,6 +55,11 @@ module Dynamoid
55
55
  raise Dynamoid::Errors::MissingHashKey if @model.hash_key.nil?
56
56
  raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @model.range_value.nil?
57
57
  end
58
+
59
+ def dump_attribute(name, value)
60
+ options = @model_class.attributes[name]
61
+ Dumping.dump_field(value, options)
62
+ end
58
63
  end
59
64
  end
60
65
  end
@@ -34,10 +34,10 @@ module Dynamoid
34
34
  end
35
35
 
36
36
  def action_request
37
- key = { @model_class.hash_key => @hash_key }
37
+ key = { @model_class.hash_key => dump_attribute(@model_class.hash_key, @hash_key) }
38
38
 
39
39
  if @model_class.range_key?
40
- key[@model_class.range_key] = @range_key
40
+ key[@model_class.range_key] = dump_attribute(@model_class.range_key, @range_key)
41
41
  end
42
42
 
43
43
  {
@@ -54,6 +54,11 @@ module Dynamoid
54
54
  raise Dynamoid::Errors::MissingHashKey if @hash_key.nil?
55
55
  raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @range_key.nil?
56
56
  end
57
+
58
+ def dump_attribute(name, value)
59
+ options = @model_class.attributes[name]
60
+ Dumping.dump_field(value, options)
61
+ end
57
62
  end
58
63
  end
59
64
  end
@@ -15,10 +15,10 @@ module Dynamoid
15
15
  end
16
16
 
17
17
  def on_registration
18
- validate_model!
19
-
20
18
  @aborted = true
21
19
  @model.run_callbacks(:destroy) do
20
+ validate_primary_key!
21
+
22
22
  @aborted = false
23
23
  true
24
24
  end
@@ -54,10 +54,10 @@ module Dynamoid
54
54
  end
55
55
 
56
56
  def action_request
57
- key = { @model_class.hash_key => @model.hash_key }
57
+ key = { @model_class.hash_key => dump_attribute(@model_class.hash_key, @model.hash_key) }
58
58
 
59
59
  if @model_class.range_key?
60
- key[@model_class.range_key] = @model.range_value
60
+ key[@model_class.range_key] = dump_attribute(@model_class.range_key, @model.range_value)
61
61
  end
62
62
 
63
63
  {
@@ -70,10 +70,15 @@ module Dynamoid
70
70
 
71
71
  private
72
72
 
73
- def validate_model!
73
+ def validate_primary_key!
74
74
  raise Dynamoid::Errors::MissingHashKey if @model.hash_key.nil?
75
75
  raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @model.range_value.nil?
76
76
  end
77
+
78
+ def dump_attribute(name, value)
79
+ options = @model_class.attributes[name]
80
+ Dumping.dump_field(value, options)
81
+ end
77
82
  end
78
83
  end
79
84
  end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dynamoid
4
+ class TransactionWrite
5
+ class ItemUpdater
6
+ attr_reader :attributes_to_set, :attributes_to_add, :attributes_to_delete, :attributes_to_remove
7
+
8
+ def initialize(model_class)
9
+ @model_class = model_class
10
+
11
+ @attributes_to_set = {}
12
+ @attributes_to_add = {}
13
+ @attributes_to_delete = {}
14
+ @attributes_to_remove = []
15
+ end
16
+
17
+ def empty?
18
+ [@attributes_to_set, @attributes_to_add, @attributes_to_delete, @attributes_to_remove].all?(&:empty?)
19
+ end
20
+
21
+ def set(attributes)
22
+ validate_attribute_names!(attributes.keys)
23
+ if Dynamoid.config.store_attribute_with_nil_value
24
+ @attributes_to_set.merge!(attributes)
25
+ else
26
+ @attributes_to_set.merge!(attributes.reject { |_, v| v.nil? })
27
+ @attributes_to_remove += attributes.select { |_, v| v.nil? }.keys
28
+ end
29
+ end
30
+
31
+ # adds to array of fields for use in REMOVE update expression
32
+ def remove(*names)
33
+ validate_attribute_names!(names)
34
+ @attributes_to_remove += names
35
+ end
36
+
37
+ # increments a number or adds to a set, starts at 0 or [] if it doesn't yet exist
38
+ def add(attributes)
39
+ validate_attribute_names!(attributes.keys)
40
+ @attributes_to_add.merge!(attributes)
41
+ end
42
+
43
+ # deletes a value or values from a set
44
+ def delete(attributes)
45
+ validate_attribute_names!(attributes.keys)
46
+ @attributes_to_delete.merge!(attributes)
47
+ end
48
+
49
+ private
50
+
51
+ def validate_attribute_names!(names)
52
+ names.each do |name|
53
+ unless @model_class.attributes[name]
54
+ raise Dynamoid::Errors::UnknownAttribute.new(@model_class, name)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -18,8 +18,6 @@ module Dynamoid
18
18
  end
19
19
 
20
20
  def on_registration
21
- validate_model!
22
-
23
21
  if @options[:validate] != false && !(@valid = @model.valid?)
24
22
  if @options[:raise_error]
25
23
  raise Dynamoid::Errors::DocumentNotValid, @model
@@ -35,6 +33,8 @@ module Dynamoid
35
33
  @model.run_callbacks(:save) do
36
34
  @model.run_callbacks(callback_name) do
37
35
  @model.run_callbacks(:validate) do
36
+ validate_primary_key!
37
+
38
38
  @aborted = false
39
39
  true
40
40
  end
@@ -88,7 +88,7 @@ module Dynamoid
88
88
 
89
89
  private
90
90
 
91
- def validate_model!
91
+ def validate_primary_key!
92
92
  raise Dynamoid::Errors::MissingHashKey if !@was_new_record && @model.hash_key.nil?
93
93
  raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @model.range_value.nil?
94
94
  end
@@ -97,6 +97,7 @@ module Dynamoid
97
97
  touch_model_timestamps(skip_created_at: false)
98
98
 
99
99
  attributes_dumped = Dynamoid::Dumping.dump_attributes(@model.attributes, @model_class.attributes)
100
+ attributes_dumped = sanitize_item(attributes_dumped)
100
101
 
101
102
  # require primary key not to exist yet
102
103
  condition = "attribute_not_exists(#{@model_class.hash_key})"
@@ -121,12 +122,13 @@ module Dynamoid
121
122
  changes_dumped = Dynamoid::Dumping.dump_attributes(changes, @model_class.attributes)
122
123
 
123
124
  # primary key to look up an item to update
124
- key = { @model_class.hash_key => @model.hash_key }
125
- key[@model_class.range_key] = @model.range_value if @model_class.range_key?
125
+ key = { @model_class.hash_key => dump_attribute(@model_class.hash_key, @model.hash_key) }
126
+ key[@model_class.range_key] = dump_attribute(@model_class.range_key, @model.range_value) if @model_class.range_key?
126
127
 
127
128
  # Build UpdateExpression and keep names and values placeholders mapping
128
129
  # in ExpressionAttributeNames and ExpressionAttributeValues.
129
- update_expression_statements = []
130
+ set_expression_statements = []
131
+ remove_expression_statements = []
130
132
  expression_attribute_names = {}
131
133
  expression_attribute_values = {}
132
134
 
@@ -134,12 +136,18 @@ module Dynamoid
134
136
  name_placeholder = "#_n#{i}"
135
137
  value_placeholder = ":_s#{i}"
136
138
 
137
- update_expression_statements << "#{name_placeholder} = #{value_placeholder}"
139
+ if value || Dynamoid.config.store_attribute_with_nil_value
140
+ set_expression_statements << "#{name_placeholder} = #{value_placeholder}"
141
+ expression_attribute_values[value_placeholder] = value
142
+ else
143
+ remove_expression_statements << name_placeholder
144
+ end
138
145
  expression_attribute_names[name_placeholder] = name
139
- expression_attribute_values[value_placeholder] = value
140
146
  end
141
147
 
142
- update_expression = "SET #{update_expression_statements.join(', ')}"
148
+ update_expression = ''
149
+ update_expression += "SET #{set_expression_statements.join(', ')}" if set_expression_statements.any?
150
+ update_expression += " REMOVE #{remove_expression_statements.join(', ')}" if remove_expression_statements.any?
143
151
 
144
152
  {
145
153
  update: {
@@ -159,6 +167,11 @@ module Dynamoid
159
167
  @model.updated_at = timestamp unless @options[:touch] == false && !@was_new_record
160
168
  @model.created_at ||= timestamp unless skip_created_at
161
169
  end
170
+
171
+ def dump_attribute(name, value)
172
+ options = @model_class.attributes[name]
173
+ Dumping.dump_field(value, options)
174
+ end
162
175
  end
163
176
  end
164
177
  end
@@ -6,18 +6,24 @@ require 'dynamoid/persistence/update_validations'
6
6
  module Dynamoid
7
7
  class TransactionWrite
8
8
  class UpdateFields < Base
9
- def initialize(model_class, hash_key, range_key, attributes)
9
+ def initialize(model_class, hash_key, range_key, attributes, &block)
10
10
  super()
11
11
 
12
12
  @model_class = model_class
13
13
  @hash_key = hash_key
14
14
  @range_key = range_key
15
- @attributes = attributes
15
+ @attributes = attributes || {}
16
+ @block = block
16
17
  end
17
18
 
18
19
  def on_registration
19
20
  validate_primary_key!
20
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
21
27
  end
22
28
 
23
29
  def on_commit; end
@@ -29,7 +35,7 @@ module Dynamoid
29
35
  end
30
36
 
31
37
  def skipped?
32
- @attributes.empty?
38
+ @attributes.empty? && (!@item_updater || @item_updater.empty?)
33
39
  end
34
40
 
35
41
  def observable_by_user_result
@@ -37,48 +43,63 @@ module Dynamoid
37
43
  end
38
44
 
39
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
+
40
52
  # changed attributes to persist
41
53
  changes = @attributes.dup
42
54
  changes = add_timestamps(changes, skip_created_at: true)
43
55
  changes_dumped = Dynamoid::Dumping.dump_attributes(changes, @model_class.attributes)
44
56
 
45
- # primary key to look up an item to update
46
- key = { @model_class.hash_key => @hash_key }
47
- key[@model_class.range_key] = @range_key if @model_class.range_key?
48
-
49
- # Build UpdateExpression and keep names and values placeholders mapping
50
- # in ExpressionAttributeNames and ExpressionAttributeValues.
51
- update_expression_statements = []
52
- expression_attribute_names = {}
53
- expression_attribute_values = {}
54
-
55
- changes_dumped.each_with_index do |(name, value), i|
56
- name_placeholder = "#_n#{i}"
57
- value_placeholder = ":_s#{i}"
58
-
59
- update_expression_statements << "#{name_placeholder} = #{value_placeholder}"
60
- expression_attribute_names[name_placeholder] = name
61
- expression_attribute_values[value_placeholder] = value
57
+ if Dynamoid.config.store_attribute_with_nil_value
58
+ builder.set_attributes(changes_dumped)
59
+ else
60
+ nil_attributes = changes_dumped.select { |_, v| v.nil? }
61
+ non_nil_attributes = changes_dumped.reject { |_, v| v.nil? } # rubocop:disable Style/PartitionInsteadOfDoubleSelect
62
+
63
+ builder.remove_attributes(nil_attributes.keys)
64
+ builder.set_attributes(non_nil_attributes)
62
65
  end
63
66
 
64
- update_expression = "SET #{update_expression_statements.join(', ')}"
67
+ # given a block
68
+ if @item_updater
69
+ builder.set_attributes(@item_updater.attributes_to_set)
70
+ builder.remove_attributes(@item_updater.attributes_to_remove)
71
+
72
+ @item_updater.attributes_to_add.each do |name, value|
73
+ # The ADD section in UpdateExpressions requires values to be a
74
+ # set to update a set attribute.
75
+ # Allow specifying values as any Enumerable collection (e.g. Array).
76
+ # Allow a single value not wrapped into a Set
77
+ if @model_class.attributes[name][:type] == :set
78
+ value = value.is_a?(Enumerable) ? Set.new(value) : Set[value]
79
+ end
80
+
81
+ builder.add_value(name, value)
82
+ end
83
+
84
+ @item_updater.attributes_to_delete.each do |name, value|
85
+ # The DELETE section in UpdateExpressions requires values to be a
86
+ # set to update a set attribute.
87
+ # Allow specifying values as any Enumerable collection (e.g. Array).
88
+ # Allow a single value not wrapped into a Set
89
+ value = value.is_a?(Enumerable) ? Set.new(value) : Set[value]
90
+
91
+ builder.delete_value(name, value)
92
+ end
93
+ end
65
94
 
66
95
  # require primary key to exist
67
96
  condition_expression = "attribute_exists(#{@model_class.hash_key})"
68
97
  if @model_class.range_key?
69
98
  condition_expression += " AND attribute_exists(#{@model_class.range_key})"
70
99
  end
100
+ builder.condition_expression = condition_expression
71
101
 
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
- condition_expression: condition_expression
80
- }
81
- }
102
+ builder.request
82
103
  end
83
104
 
84
105
  private
@@ -97,6 +118,130 @@ module Dynamoid
97
118
  result[:updated_at] ||= timestamp
98
119
  result
99
120
  end
121
+
122
+ def dump_attribute(name, value)
123
+ options = @model_class.attributes[name]
124
+ Dumping.dump_field(value, options)
125
+ end
126
+
127
+ class UpdateRequestBuilder
128
+ attr_writer :hash_key, :range_key, :condition_expression
129
+
130
+ def initialize(model_class)
131
+ @model_class = model_class
132
+
133
+ @attributes_to_set = {}
134
+ @attributes_to_add = {}
135
+ @attributes_to_delete = {}
136
+ @attributes_to_remove = []
137
+ @condition_expression = nil
138
+ end
139
+
140
+ def set_attributes(attributes) # rubocop:disable Naming/AccessorMethodName
141
+ @attributes_to_set.merge!(attributes)
142
+ end
143
+
144
+ def add_value(name, value)
145
+ @attributes_to_add[name] = value
146
+ end
147
+
148
+ def delete_value(name, value)
149
+ @attributes_to_delete[name] = value
150
+ end
151
+
152
+ def remove_attributes(names)
153
+ @attributes_to_remove.concat(names)
154
+ end
155
+
156
+ def request
157
+ key = { @model_class.hash_key => @hash_key }
158
+ key[@model_class.range_key] = @range_key if @model_class.range_key?
159
+
160
+ # Build UpdateExpression and keep names and values placeholders mapping
161
+ # in ExpressionAttributeNames and ExpressionAttributeValues.
162
+ update_expression_statements = []
163
+ expression_attribute_names = {}
164
+ expression_attribute_values = {}
165
+ name_placeholder = '#_n0'
166
+ value_placeholder = ':_v0'
167
+
168
+ unless @attributes_to_set.empty?
169
+ statements = []
170
+
171
+ @attributes_to_set.each do |name, value|
172
+ statements << "#{name_placeholder} = #{value_placeholder}"
173
+
174
+ expression_attribute_names[name_placeholder] = name
175
+ expression_attribute_values[value_placeholder] = value
176
+
177
+ name_placeholder = name_placeholder.succ
178
+ value_placeholder = value_placeholder.succ
179
+ end
180
+
181
+ update_expression_statements << "SET #{statements.join(', ')}"
182
+ end
183
+
184
+ unless @attributes_to_add.empty?
185
+ statements = []
186
+
187
+ @attributes_to_add.each do |name, value|
188
+ statements << "#{name_placeholder} #{value_placeholder}"
189
+
190
+ expression_attribute_names[name_placeholder] = name
191
+ expression_attribute_values[value_placeholder] = value
192
+
193
+ name_placeholder = name_placeholder.succ
194
+ value_placeholder = value_placeholder.succ
195
+ end
196
+
197
+ update_expression_statements << "ADD #{statements.join(', ')}"
198
+ end
199
+
200
+ unless @attributes_to_delete.empty?
201
+ statements = []
202
+
203
+ @attributes_to_delete.each do |name, value|
204
+ statements << "#{name_placeholder} #{value_placeholder}"
205
+
206
+ expression_attribute_names[name_placeholder] = name
207
+ expression_attribute_values[value_placeholder] = value
208
+
209
+ name_placeholder = name_placeholder.succ
210
+ value_placeholder = value_placeholder.succ
211
+ end
212
+
213
+ update_expression_statements << "DELETE #{statements.join(', ')}"
214
+ end
215
+
216
+ unless @attributes_to_remove.empty?
217
+ name_placeholders = []
218
+
219
+ @attributes_to_remove.each do |name|
220
+ name_placeholders << name_placeholder
221
+
222
+ expression_attribute_names[name_placeholder] = name
223
+
224
+ name_placeholder = name_placeholder.succ
225
+ value_placeholder = value_placeholder.succ
226
+ end
227
+
228
+ update_expression_statements << "REMOVE #{name_placeholders.join(', ')}"
229
+ end
230
+
231
+ update_expression = update_expression_statements.join(' ')
232
+
233
+ {
234
+ update: {
235
+ key: key,
236
+ table_name: @model_class.table_name,
237
+ update_expression: update_expression,
238
+ expression_attribute_names: expression_attribute_names,
239
+ expression_attribute_values: expression_attribute_values,
240
+ condition_expression: @condition_expression
241
+ }
242
+ }
243
+ end
244
+ end
100
245
  end
101
246
  end
102
247
  end