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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -5
  3. data/README.md +60 -1584
  4. data/lib/dynamoid/associations/association.rb +10 -2
  5. data/lib/dynamoid/associations/belongs_to.rb +3 -1
  6. data/lib/dynamoid/associations/has_and_belongs_to_many.rb +0 -1
  7. data/lib/dynamoid/associations/has_many.rb +0 -1
  8. data/lib/dynamoid/associations/has_one.rb +1 -2
  9. data/lib/dynamoid/associations/many_association.rb +1 -0
  10. data/lib/dynamoid/associations.rb +64 -48
  11. data/lib/dynamoid/components.rb +1 -0
  12. data/lib/dynamoid/criteria/chain.rb +43 -30
  13. data/lib/dynamoid/dirty.rb +3 -2
  14. data/lib/dynamoid/document.rb +29 -21
  15. data/lib/dynamoid/fields.rb +55 -41
  16. data/lib/dynamoid/finders.rb +1 -1
  17. data/lib/dynamoid/indexes.rb +29 -21
  18. data/lib/dynamoid/persistence/inc.rb +15 -7
  19. data/lib/dynamoid/persistence/save.rb +21 -14
  20. data/lib/dynamoid/persistence.rb +88 -54
  21. data/lib/dynamoid/transactions/mutation/base.rb +76 -0
  22. data/lib/dynamoid/transactions/mutation/builders/delete_request_builder.rb +44 -0
  23. data/lib/dynamoid/transactions/mutation/builders/update_request_builder.rb +139 -0
  24. data/lib/dynamoid/transactions/mutation/create.rb +52 -0
  25. data/lib/dynamoid/transactions/mutation/delete_with_instance.rb +80 -0
  26. data/lib/dynamoid/transactions/mutation/delete_with_primary_key.rb +62 -0
  27. data/lib/dynamoid/transactions/mutation/destroy.rb +96 -0
  28. data/lib/dynamoid/transactions/mutation/import.rb +84 -0
  29. data/lib/dynamoid/transactions/mutation/inc.rb +101 -0
  30. data/lib/dynamoid/transactions/mutation/increment.rb +63 -0
  31. data/lib/dynamoid/transactions/mutation/save.rb +194 -0
  32. data/lib/dynamoid/transactions/mutation/touch.rb +99 -0
  33. data/lib/dynamoid/transactions/mutation/update_attributes.rb +49 -0
  34. data/lib/dynamoid/transactions/mutation/update_fields.rb +185 -0
  35. data/lib/dynamoid/transactions/mutation/upsert.rb +94 -0
  36. data/lib/dynamoid/transactions/mutation.rb +935 -0
  37. data/lib/dynamoid/transactions/retrieval/find.rb +140 -0
  38. data/lib/dynamoid/transactions/retrieval.rb +150 -0
  39. data/lib/dynamoid/transactions.rb +63 -0
  40. data/lib/dynamoid/version.rb +1 -1
  41. data/lib/dynamoid.rb +1 -2
  42. metadata +23 -17
  43. data/lib/dynamoid/transaction_read/find.rb +0 -137
  44. data/lib/dynamoid/transaction_read.rb +0 -146
  45. data/lib/dynamoid/transaction_write/base.rb +0 -59
  46. data/lib/dynamoid/transaction_write/create.rb +0 -49
  47. data/lib/dynamoid/transaction_write/delete_with_instance.rb +0 -65
  48. data/lib/dynamoid/transaction_write/delete_with_primary_key.rb +0 -64
  49. data/lib/dynamoid/transaction_write/destroy.rb +0 -84
  50. data/lib/dynamoid/transaction_write/item_updater.rb +0 -60
  51. data/lib/dynamoid/transaction_write/save.rb +0 -177
  52. data/lib/dynamoid/transaction_write/update_attributes.rb +0 -46
  53. data/lib/dynamoid/transaction_write/update_fields.rb +0 -247
  54. data/lib/dynamoid/transaction_write/upsert.rb +0 -113
  55. data/lib/dynamoid/transaction_write.rb +0 -673
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dynamoid
4
+ module Transactions
5
+ class Mutation
6
+ # @private
7
+ module Builders
8
+ class DeleteRequestBuilder
9
+ attr_writer :hash_key, :range_key, :condition_expression
10
+
11
+ def initialize(model_class)
12
+ @model_class = model_class
13
+ @condition_expression = nil
14
+ @extra_attribute_names = {}
15
+ @extra_attribute_values = {}
16
+ end
17
+
18
+ def add_expression_attribute_name(placeholder, name)
19
+ @extra_attribute_names[placeholder] = name
20
+ end
21
+
22
+ def add_expression_attribute_value(placeholder, value)
23
+ @extra_attribute_values[placeholder] = value
24
+ end
25
+
26
+ def request
27
+ key = { @model_class.hash_key => @hash_key }
28
+ key[@model_class.range_key] = @range_key if @model_class.range_key?
29
+
30
+ {
31
+ delete: {
32
+ key: key,
33
+ table_name: @model_class.table_name,
34
+ expression_attribute_names: @extra_attribute_names.presence,
35
+ expression_attribute_values: @extra_attribute_values.presence,
36
+ condition_expression: @condition_expression
37
+ }.compact
38
+ }
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dynamoid
4
+ module Transactions
5
+ class Mutation
6
+ # @private
7
+ module Builders
8
+ class UpdateRequestBuilder
9
+ attr_writer :hash_key, :range_key, :condition_expression
10
+
11
+ def initialize(model_class)
12
+ @model_class = model_class
13
+
14
+ @attributes_to_set = {}
15
+ @attributes_to_add = {}
16
+ @attributes_to_delete = {}
17
+ @attributes_to_remove = []
18
+ @condition_expression = nil
19
+ @extra_attribute_names = {}
20
+ @extra_attribute_values = {}
21
+ end
22
+
23
+ def add_expression_attribute_name(placeholder, name)
24
+ @extra_attribute_names[placeholder] = name
25
+ end
26
+
27
+ def add_expression_attribute_value(placeholder, value)
28
+ @extra_attribute_values[placeholder] = value
29
+ end
30
+
31
+ def set_attributes(attributes) # rubocop:disable Naming/AccessorMethodName
32
+ @attributes_to_set.merge!(attributes)
33
+ end
34
+
35
+ def add_value(name, value)
36
+ @attributes_to_add[name] = value
37
+ end
38
+
39
+ def delete_value(name, value)
40
+ @attributes_to_delete[name] = value
41
+ end
42
+
43
+ def remove_attributes(names)
44
+ @attributes_to_remove.concat(names)
45
+ end
46
+
47
+ def request
48
+ key = { @model_class.hash_key => @hash_key }
49
+ key[@model_class.range_key] = @range_key if @model_class.range_key?
50
+
51
+ # Build UpdateExpression and keep names and values placeholders mapping
52
+ # in ExpressionAttributeNames and ExpressionAttributeValues.
53
+ update_expression_statements = []
54
+ expression_attribute_names = @extra_attribute_names.dup
55
+ expression_attribute_values = @extra_attribute_values.dup
56
+ name_placeholder = '#_n0'
57
+ value_placeholder = ':_v0'
58
+
59
+ unless @attributes_to_set.empty?
60
+ statements = []
61
+
62
+ @attributes_to_set.each do |name, value|
63
+ statements << "#{name_placeholder} = #{value_placeholder}"
64
+
65
+ expression_attribute_names[name_placeholder] = name
66
+ expression_attribute_values[value_placeholder] = value
67
+
68
+ name_placeholder = name_placeholder.succ
69
+ value_placeholder = value_placeholder.succ
70
+ end
71
+
72
+ update_expression_statements << "SET #{statements.join(', ')}"
73
+ end
74
+
75
+ unless @attributes_to_add.empty?
76
+ statements = []
77
+
78
+ @attributes_to_add.each do |name, value|
79
+ statements << "#{name_placeholder} #{value_placeholder}"
80
+
81
+ expression_attribute_names[name_placeholder] = name
82
+ expression_attribute_values[value_placeholder] = value
83
+
84
+ name_placeholder = name_placeholder.succ
85
+ value_placeholder = value_placeholder.succ
86
+ end
87
+
88
+ update_expression_statements << "ADD #{statements.join(', ')}"
89
+ end
90
+
91
+ unless @attributes_to_delete.empty?
92
+ statements = []
93
+
94
+ @attributes_to_delete.each do |name, value|
95
+ statements << "#{name_placeholder} #{value_placeholder}"
96
+
97
+ expression_attribute_names[name_placeholder] = name
98
+ expression_attribute_values[value_placeholder] = value
99
+
100
+ name_placeholder = name_placeholder.succ
101
+ value_placeholder = value_placeholder.succ
102
+ end
103
+
104
+ update_expression_statements << "DELETE #{statements.join(', ')}"
105
+ end
106
+
107
+ unless @attributes_to_remove.empty?
108
+ name_placeholders = []
109
+
110
+ @attributes_to_remove.each do |name|
111
+ name_placeholders << name_placeholder
112
+
113
+ expression_attribute_names[name_placeholder] = name
114
+
115
+ name_placeholder = name_placeholder.succ
116
+ value_placeholder = value_placeholder.succ
117
+ end
118
+
119
+ update_expression_statements << "REMOVE #{name_placeholders.join(', ')}"
120
+ end
121
+
122
+ update_expression = update_expression_statements.join(' ')
123
+
124
+ {
125
+ update: {
126
+ key: key,
127
+ table_name: @model_class.table_name,
128
+ update_expression: update_expression,
129
+ expression_attribute_names: expression_attribute_names,
130
+ expression_attribute_values: expression_attribute_values,
131
+ condition_expression: @condition_expression
132
+ }
133
+ }
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Dynamoid
6
+ module Transactions
7
+ class Mutation
8
+ # @private
9
+ class Create < Base
10
+ def initialize(model_class, attributes = {}, **options, &block)
11
+ super()
12
+
13
+ @model = model_class.new(attributes)
14
+
15
+ if block
16
+ yield(@model)
17
+ end
18
+
19
+ @save_action = Save.new(@model, **options)
20
+ end
21
+
22
+ def on_registration
23
+ @save_action.on_registration
24
+ end
25
+
26
+ def on_commit
27
+ @save_action.on_commit
28
+ end
29
+
30
+ def on_rollback
31
+ @save_action.on_rollback
32
+ end
33
+
34
+ def aborted?
35
+ @save_action.aborted?
36
+ end
37
+
38
+ def skipped?
39
+ false
40
+ end
41
+
42
+ def observable_by_user_result
43
+ @model
44
+ end
45
+
46
+ def action_requests
47
+ @save_action.action_requests
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative 'builders/delete_request_builder'
5
+
6
+ module Dynamoid
7
+ module Transactions
8
+ class Mutation
9
+ # @private
10
+ class DeleteWithInstance < Base
11
+ def initialize(model)
12
+ super()
13
+
14
+ @model = model
15
+ @model_class = model.class
16
+ end
17
+
18
+ def on_registration
19
+ validate_model!
20
+ end
21
+
22
+ def on_commit
23
+ @model.destroyed = true
24
+ @model.run_callbacks(:commit)
25
+ end
26
+
27
+ def on_rollback
28
+ @model.run_callbacks(:rollback)
29
+ end
30
+
31
+ def aborted?
32
+ false
33
+ end
34
+
35
+ def skipped?
36
+ false
37
+ end
38
+
39
+ def observable_by_user_result
40
+ @model
41
+ end
42
+
43
+ def action_requests
44
+ builder = Builders::DeleteRequestBuilder.new(@model_class)
45
+ builder.hash_key = dump_attribute(@model_class.hash_key, @model.hash_key)
46
+ builder.range_key = dump_attribute(@model_class.range_key, @model.range_value) if @model_class.range_key?
47
+
48
+ if @model_class.attributes[:lock_version]
49
+ lock_version = if @model.changes[:lock_version].nil?
50
+ @model.lock_version
51
+ else
52
+ @model.changes[:lock_version][0]
53
+ end
54
+
55
+ # skip concurrency control when lock_version is nil
56
+ if lock_version
57
+ builder.add_expression_attribute_name('#_lock_version', 'lock_version')
58
+ builder.add_expression_attribute_value(':lock_version_value', lock_version)
59
+ builder.condition_expression = '#_lock_version = :lock_version_value'
60
+ end
61
+ end
62
+
63
+ [builder.request]
64
+ end
65
+
66
+ private
67
+
68
+ def validate_model!
69
+ raise Dynamoid::Errors::MissingHashKey if @model.hash_key.nil?
70
+ raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @model.range_value.nil?
71
+ end
72
+
73
+ def dump_attribute(name, value)
74
+ options = @model_class.attributes[name]
75
+ Dumping.dump_field(value, options)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative 'builders/delete_request_builder'
5
+
6
+ module Dynamoid
7
+ module Transactions
8
+ class Mutation
9
+ # @private
10
+ class DeleteWithPrimaryKey < Base
11
+ def initialize(model_class, hash_key, range_key)
12
+ super()
13
+
14
+ @model_class = model_class
15
+ @hash_key = hash_key
16
+ @range_key = range_key
17
+ end
18
+
19
+ def on_registration
20
+ validate_primary_key!
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
+ false
33
+ end
34
+
35
+ def observable_by_user_result
36
+ nil
37
+ end
38
+
39
+ def action_requests
40
+ builder = Builders::DeleteRequestBuilder.new(@model_class)
41
+ builder.hash_key = cast_and_dump(@model_class.hash_key, @hash_key)
42
+ builder.range_key = cast_and_dump(@model_class.range_key, @range_key) if @model_class.range_key?
43
+
44
+ [builder.request]
45
+ end
46
+
47
+ private
48
+
49
+ def validate_primary_key!
50
+ raise Dynamoid::Errors::MissingHashKey if @hash_key.nil?
51
+ raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @range_key.nil?
52
+ end
53
+
54
+ def cast_and_dump(name, value)
55
+ options = @model_class.attributes[name]
56
+ value_casted = TypeCasting.cast_field(value, options)
57
+ Dumping.dump_field(value_casted, options)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require_relative 'builders/delete_request_builder'
5
+
6
+ module Dynamoid
7
+ module Transactions
8
+ class Mutation
9
+ # @private
10
+ class Destroy < Base
11
+ def initialize(model, **options)
12
+ super()
13
+
14
+ @model = model
15
+ @options = options
16
+ @model_class = model.class
17
+ @aborted = false
18
+ end
19
+
20
+ def on_registration
21
+ @aborted = true
22
+ @model.run_callbacks(:destroy) do
23
+ validate_primary_key!
24
+
25
+ @aborted = false
26
+ true
27
+ end
28
+
29
+ if @aborted && @options[:raise_error]
30
+ raise Dynamoid::Errors::RecordNotDestroyed, @model
31
+ end
32
+ end
33
+
34
+ def on_commit
35
+ return if @aborted
36
+
37
+ @model.destroyed = true
38
+ @model.run_callbacks(:commit)
39
+ end
40
+
41
+ def on_rollback
42
+ @model.run_callbacks(:rollback)
43
+ end
44
+
45
+ def aborted?
46
+ @aborted
47
+ end
48
+
49
+ def skipped?
50
+ false
51
+ end
52
+
53
+ def observable_by_user_result
54
+ return false if @aborted
55
+
56
+ @model
57
+ end
58
+
59
+ def action_requests
60
+ builder = Builders::DeleteRequestBuilder.new(@model_class)
61
+ builder.hash_key = dump_attribute(@model_class.hash_key, @model.hash_key)
62
+ builder.range_key = dump_attribute(@model_class.range_key, @model.range_value) if @model_class.range_key?
63
+
64
+ if @model_class.attributes[:lock_version]
65
+ lock_version = if @model.changes[:lock_version].nil?
66
+ @model.lock_version
67
+ else
68
+ @model.changes[:lock_version][0]
69
+ end
70
+
71
+ # skip concurrency control when lock_version is nil
72
+ if lock_version
73
+ builder.add_expression_attribute_name('#_lock_version', 'lock_version')
74
+ builder.add_expression_attribute_value(':lock_version_value', lock_version)
75
+ builder.condition_expression = '#_lock_version = :lock_version_value'
76
+ end
77
+ end
78
+
79
+ [builder.request]
80
+ end
81
+
82
+ private
83
+
84
+ def validate_primary_key!
85
+ raise Dynamoid::Errors::MissingHashKey if @model.hash_key.nil?
86
+ raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @model.range_value.nil?
87
+ end
88
+
89
+ def dump_attribute(name, value)
90
+ options = @model_class.attributes[name]
91
+ Dumping.dump_field(value, options)
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Dynamoid
6
+ module Transactions
7
+ class Mutation
8
+ # @private
9
+ class Import < Base
10
+ def initialize(model_class, array_of_attributes)
11
+ super()
12
+
13
+ @model_class = model_class
14
+ @models = array_of_attributes.map do |attributes|
15
+ attributes = attributes.symbolize_keys
16
+
17
+ if @model_class.timestamps_enabled?
18
+ time_now = DateTime.now.in_time_zone(Time.zone)
19
+ attributes[:created_at] ||= time_now
20
+ attributes[:updated_at] ||= time_now
21
+ end
22
+
23
+ model = @model_class.build(attributes)
24
+ model.hash_key = SecureRandom.uuid if model.hash_key.nil?
25
+ model
26
+ end
27
+ end
28
+
29
+ def on_registration
30
+ @models.each do |model|
31
+ validate_primary_key!(model)
32
+ end
33
+ end
34
+
35
+ def on_commit
36
+ @models.each do |model|
37
+ model.clear_changes_information
38
+ model.new_record = false
39
+ model.run_callbacks(:commit)
40
+ end
41
+ end
42
+
43
+ def on_rollback
44
+ @models.each do |model|
45
+ model.run_callbacks(:rollback)
46
+ end
47
+ end
48
+
49
+ def aborted?
50
+ false
51
+ end
52
+
53
+ def skipped?
54
+ @models.empty?
55
+ end
56
+
57
+ def observable_by_user_result
58
+ @models
59
+ end
60
+
61
+ def action_requests
62
+ @models.map do |model|
63
+ attributes_dumped = Dynamoid::Dumping.dump_attributes(model.attributes, @model_class.attributes)
64
+ attributes_dumped = sanitize_item(attributes_dumped)
65
+
66
+ {
67
+ put: {
68
+ item: attributes_dumped,
69
+ table_name: @model_class.table_name
70
+ }
71
+ }
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ def validate_primary_key!(model)
78
+ raise Dynamoid::Errors::MissingHashKey if model.hash_key.nil?
79
+ raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && model.range_value.nil?
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,101 @@
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 Inc < Base
11
+ def initialize(model_class, hash_key, range_key, counters)
12
+ super()
13
+
14
+ @model_class = model_class
15
+ @hash_key = hash_key
16
+ @range_key = range_key
17
+ @counters = counters || {}
18
+ @touch = @counters.delete(:touch)
19
+ end
20
+
21
+ def on_registration
22
+ validate_primary_key!
23
+ validate_attribute_names!(@model_class, @counters.keys)
24
+ end
25
+
26
+ def on_commit; end
27
+
28
+ def on_rollback; end
29
+
30
+ def aborted?
31
+ false
32
+ end
33
+
34
+ def skipped?
35
+ @counters.empty?
36
+ end
37
+
38
+ def observable_by_user_result
39
+ nil
40
+ end
41
+
42
+ def action_requests
43
+ builder = Builders::UpdateRequestBuilder.new(@model_class)
44
+
45
+ # primary key to look up an item to update
46
+ builder.hash_key = cast_and_dump(@model_class.hash_key, @hash_key)
47
+ builder.range_key = cast_and_dump(@model_class.range_key, @range_key) if @model_class.range_key?
48
+
49
+ # require primary key to exist
50
+ builder.add_expression_attribute_name('#_h', @model_class.hash_key)
51
+ condition_expression = 'attribute_exists(#_h)'
52
+
53
+ if @model_class.range_key?
54
+ builder.add_expression_attribute_name('#_r', @model_class.range_key)
55
+ condition_expression += ' AND attribute_exists(#_r)'
56
+ end
57
+ builder.condition_expression = condition_expression
58
+
59
+ @counters.each do |name, value|
60
+ builder.add_value(name, cast_and_dump(name, value))
61
+ end
62
+
63
+ if @touch
64
+ timestamp = DateTime.now.in_time_zone(Time.zone)
65
+
66
+ timestamp_attributes_to_touch.each do |name|
67
+ builder.set_attributes(name => dump(name, timestamp))
68
+ end
69
+ end
70
+
71
+ [builder.request]
72
+ end
73
+
74
+ private
75
+
76
+ def validate_primary_key!
77
+ raise Dynamoid::Errors::MissingHashKey if @hash_key.nil?
78
+ raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @range_key.nil?
79
+ end
80
+
81
+ def timestamp_attributes_to_touch
82
+ names = []
83
+ names << :updated_at if @model_class.timestamps_enabled?
84
+ names += Array.wrap(@touch) if @touch != true
85
+ names
86
+ end
87
+
88
+ def cast_and_dump(name, value)
89
+ options = @model_class.attributes[name]
90
+ value_casted = TypeCasting.cast_field(value, options)
91
+ Dumping.dump_field(value_casted, options)
92
+ end
93
+
94
+ def dump(name, value)
95
+ options = @model_class.attributes[name]
96
+ Dumping.dump_field(value, options)
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end