dynamoid 3.9.0 → 3.11.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -6
  3. data/README.md +202 -25
  4. data/dynamoid.gemspec +5 -6
  5. data/lib/dynamoid/adapter.rb +19 -13
  6. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb +2 -2
  7. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/filter_expression_convertor.rb +113 -0
  8. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +21 -2
  9. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/projection_expression_convertor.rb +40 -0
  10. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +46 -61
  11. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +34 -28
  12. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/transact.rb +31 -0
  13. data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +95 -66
  14. data/lib/dynamoid/associations/belongs_to.rb +6 -6
  15. data/lib/dynamoid/associations.rb +1 -1
  16. data/lib/dynamoid/components.rb +1 -0
  17. data/lib/dynamoid/config/options.rb +12 -12
  18. data/lib/dynamoid/config.rb +3 -0
  19. data/lib/dynamoid/criteria/chain.rb +149 -142
  20. data/lib/dynamoid/criteria/key_fields_detector.rb +6 -7
  21. data/lib/dynamoid/criteria/nonexistent_fields_detector.rb +2 -2
  22. data/lib/dynamoid/criteria/where_conditions.rb +36 -0
  23. data/lib/dynamoid/dirty.rb +87 -12
  24. data/lib/dynamoid/document.rb +1 -1
  25. data/lib/dynamoid/dumping.rb +38 -16
  26. data/lib/dynamoid/errors.rb +14 -2
  27. data/lib/dynamoid/fields/declare.rb +6 -6
  28. data/lib/dynamoid/fields.rb +6 -8
  29. data/lib/dynamoid/finders.rb +23 -32
  30. data/lib/dynamoid/indexes.rb +6 -7
  31. data/lib/dynamoid/loadable.rb +3 -2
  32. data/lib/dynamoid/persistence/inc.rb +6 -7
  33. data/lib/dynamoid/persistence/item_updater_with_casting_and_dumping.rb +36 -0
  34. data/lib/dynamoid/persistence/item_updater_with_dumping.rb +33 -0
  35. data/lib/dynamoid/persistence/save.rb +17 -18
  36. data/lib/dynamoid/persistence/update_fields.rb +7 -5
  37. data/lib/dynamoid/persistence/update_validations.rb +1 -1
  38. data/lib/dynamoid/persistence/upsert.rb +5 -4
  39. data/lib/dynamoid/persistence.rb +77 -21
  40. data/lib/dynamoid/transaction_write/base.rb +47 -0
  41. data/lib/dynamoid/transaction_write/create.rb +49 -0
  42. data/lib/dynamoid/transaction_write/delete_with_instance.rb +60 -0
  43. data/lib/dynamoid/transaction_write/delete_with_primary_key.rb +59 -0
  44. data/lib/dynamoid/transaction_write/destroy.rb +79 -0
  45. data/lib/dynamoid/transaction_write/save.rb +164 -0
  46. data/lib/dynamoid/transaction_write/update_attributes.rb +46 -0
  47. data/lib/dynamoid/transaction_write/update_fields.rb +102 -0
  48. data/lib/dynamoid/transaction_write/upsert.rb +96 -0
  49. data/lib/dynamoid/transaction_write.rb +464 -0
  50. data/lib/dynamoid/type_casting.rb +18 -15
  51. data/lib/dynamoid/undumping.rb +14 -3
  52. data/lib/dynamoid/validations.rb +1 -1
  53. data/lib/dynamoid/version.rb +1 -1
  54. data/lib/dynamoid.rb +7 -0
  55. metadata +30 -16
  56. data/lib/dynamoid/criteria/ignored_conditions_detector.rb +0 -41
  57. data/lib/dynamoid/criteria/overwritten_conditions_detector.rb +0 -40
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Dynamoid
6
+ class TransactionWrite
7
+ class Destroy < Base
8
+ def initialize(model, **options)
9
+ super()
10
+
11
+ @model = model
12
+ @options = options
13
+ @model_class = model.class
14
+ @aborted = false
15
+ end
16
+
17
+ def on_registration
18
+ validate_model!
19
+
20
+ @aborted = true
21
+ @model.run_callbacks(:destroy) do
22
+ @aborted = false
23
+ true
24
+ end
25
+
26
+ if @aborted && @options[:raise_error]
27
+ raise Dynamoid::Errors::RecordNotDestroyed, @model
28
+ end
29
+ end
30
+
31
+ def on_commit
32
+ return if @aborted
33
+
34
+ @model.destroyed = true
35
+ @model.run_callbacks(:commit)
36
+ end
37
+
38
+ def on_rollback
39
+ @model.run_callbacks(:rollback)
40
+ end
41
+
42
+ def aborted?
43
+ @aborted
44
+ end
45
+
46
+ def skipped?
47
+ false
48
+ end
49
+
50
+ def observable_by_user_result
51
+ return false if @aborted
52
+
53
+ @model
54
+ end
55
+
56
+ def action_request
57
+ key = { @model_class.hash_key => @model.hash_key }
58
+
59
+ if @model_class.range_key?
60
+ key[@model_class.range_key] = @model.range_value
61
+ end
62
+
63
+ {
64
+ delete: {
65
+ key: key,
66
+ table_name: @model_class.table_name
67
+ }
68
+ }
69
+ end
70
+
71
+ private
72
+
73
+ def validate_model!
74
+ raise Dynamoid::Errors::MissingHashKey if @model.hash_key.nil?
75
+ raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @model.range_value.nil?
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Dynamoid
6
+ class TransactionWrite
7
+ class Save < Base
8
+ def initialize(model, **options)
9
+ super()
10
+
11
+ @model = model
12
+ @model_class = model.class
13
+ @options = options
14
+
15
+ @aborted = false
16
+ @was_new_record = model.new_record?
17
+ @valid = nil
18
+ end
19
+
20
+ def on_registration
21
+ validate_model!
22
+
23
+ if @options[:validate] != false && !(@valid = @model.valid?)
24
+ if @options[:raise_error]
25
+ raise Dynamoid::Errors::DocumentNotValid, @model
26
+ else
27
+ @aborted = true
28
+ return
29
+ end
30
+ end
31
+
32
+ @aborted = true
33
+ callback_name = @was_new_record ? :create : :update
34
+
35
+ @model.run_callbacks(:save) do
36
+ @model.run_callbacks(callback_name) do
37
+ @model.run_callbacks(:validate) do
38
+ @aborted = false
39
+ true
40
+ end
41
+ end
42
+ end
43
+
44
+ if @aborted && @options[:raise_error]
45
+ raise Dynamoid::Errors::RecordNotSaved, @model
46
+ end
47
+
48
+ if @was_new_record && @model.hash_key.nil?
49
+ @model.hash_key = SecureRandom.uuid
50
+ end
51
+ end
52
+
53
+ def on_commit
54
+ return if @aborted
55
+
56
+ @model.changes_applied
57
+
58
+ if @was_new_record
59
+ @model.new_record = false
60
+ end
61
+
62
+ @model.run_callbacks(:commit)
63
+ end
64
+
65
+ def on_rollback
66
+ @model.run_callbacks(:rollback)
67
+ end
68
+
69
+ def aborted?
70
+ @aborted
71
+ end
72
+
73
+ def skipped?
74
+ @model.persisted? && !@model.changed?
75
+ end
76
+
77
+ def observable_by_user_result
78
+ !@aborted
79
+ end
80
+
81
+ def action_request
82
+ if @was_new_record
83
+ action_request_to_create
84
+ else
85
+ action_request_to_update
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ def validate_model!
92
+ raise Dynamoid::Errors::MissingHashKey if !@was_new_record && @model.hash_key.nil?
93
+ raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @model.range_value.nil?
94
+ end
95
+
96
+ def action_request_to_create
97
+ touch_model_timestamps(skip_created_at: false)
98
+
99
+ attributes_dumped = Dynamoid::Dumping.dump_attributes(@model.attributes, @model_class.attributes)
100
+
101
+ # require primary key not to exist yet
102
+ condition = "attribute_not_exists(#{@model_class.hash_key})"
103
+ if @model_class.range_key?
104
+ condition += " AND attribute_not_exists(#{@model_class.range_key})"
105
+ end
106
+
107
+ {
108
+ put: {
109
+ item: attributes_dumped,
110
+ table_name: @model_class.table_name,
111
+ condition_expression: condition
112
+ }
113
+ }
114
+ end
115
+
116
+ def action_request_to_update
117
+ touch_model_timestamps(skip_created_at: true)
118
+
119
+ # changed attributes to persist
120
+ changes = @model.attributes.slice(*@model.changed.map(&:to_sym))
121
+ changes_dumped = Dynamoid::Dumping.dump_attributes(changes, @model_class.attributes)
122
+
123
+ # 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?
126
+
127
+ # Build UpdateExpression and keep names and values placeholders mapping
128
+ # in ExpressionAttributeNames and ExpressionAttributeValues.
129
+ update_expression_statements = []
130
+ expression_attribute_names = {}
131
+ expression_attribute_values = {}
132
+
133
+ changes_dumped.each_with_index do |(name, value), i|
134
+ name_placeholder = "#_n#{i}"
135
+ value_placeholder = ":_s#{i}"
136
+
137
+ update_expression_statements << "#{name_placeholder} = #{value_placeholder}"
138
+ expression_attribute_names[name_placeholder] = name
139
+ expression_attribute_values[value_placeholder] = value
140
+ end
141
+
142
+ update_expression = "SET #{update_expression_statements.join(', ')}"
143
+
144
+ {
145
+ update: {
146
+ key: key,
147
+ table_name: @model_class.table_name,
148
+ update_expression: update_expression,
149
+ expression_attribute_names: expression_attribute_names,
150
+ expression_attribute_values: expression_attribute_values
151
+ }
152
+ }
153
+ end
154
+
155
+ def touch_model_timestamps(skip_created_at:)
156
+ return unless @model_class.timestamps_enabled?
157
+
158
+ timestamp = DateTime.now.in_time_zone(Time.zone)
159
+ @model.updated_at = timestamp unless @options[:touch] == false && !@was_new_record
160
+ @model.created_at ||= timestamp unless skip_created_at
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,46 @@
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 UpdateAttributes < Base
9
+ def initialize(model, attributes, **options)
10
+ super()
11
+
12
+ @model = model
13
+ @model.assign_attributes(attributes)
14
+ @save_action = Save.new(model, **options)
15
+ end
16
+
17
+ def on_registration
18
+ @save_action.on_registration
19
+ end
20
+
21
+ def on_commit
22
+ @save_action.on_commit
23
+ end
24
+
25
+ def on_rollback
26
+ @save_action.on_rollback
27
+ end
28
+
29
+ def aborted?
30
+ @save_action.aborted?
31
+ end
32
+
33
+ def skipped?
34
+ @save_action.skipped?
35
+ end
36
+
37
+ def observable_by_user_result
38
+ @save_action.observable_by_user_result
39
+ end
40
+
41
+ def action_request
42
+ @save_action.action_request
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,102 @@
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)
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.empty?
33
+ end
34
+
35
+ def observable_by_user_result
36
+ nil
37
+ end
38
+
39
+ def action_request
40
+ # changed attributes to persist
41
+ changes = @attributes.dup
42
+ changes = add_timestamps(changes, skip_created_at: true)
43
+ changes_dumped = Dynamoid::Dumping.dump_attributes(changes, @model_class.attributes)
44
+
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
62
+ end
63
+
64
+ update_expression = "SET #{update_expression_statements.join(', ')}"
65
+
66
+ # require primary key to exist
67
+ condition_expression = "attribute_exists(#{@model_class.hash_key})"
68
+ if @model_class.range_key?
69
+ condition_expression += " AND attribute_exists(#{@model_class.range_key})"
70
+ end
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
+ condition_expression: condition_expression
80
+ }
81
+ }
82
+ end
83
+
84
+ private
85
+
86
+ def validate_primary_key!
87
+ raise Dynamoid::Errors::MissingHashKey if @hash_key.nil?
88
+ raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @range_key.nil?
89
+ end
90
+
91
+ def add_timestamps(attributes, skip_created_at: false)
92
+ return attributes unless @model_class.timestamps_enabled?
93
+
94
+ result = attributes.clone
95
+ timestamp = DateTime.now.in_time_zone(Time.zone)
96
+ result[:created_at] ||= timestamp unless skip_created_at
97
+ result[:updated_at] ||= timestamp
98
+ result
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,96 @@
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
+ key = { @model_class.hash_key => @hash_key }
48
+ key[@model_class.range_key] = @range_key if @model_class.range_key?
49
+
50
+ # Build UpdateExpression and keep names and values placeholders mapping
51
+ # in ExpressionAttributeNames and ExpressionAttributeValues.
52
+ update_expression_statements = []
53
+ expression_attribute_names = {}
54
+ expression_attribute_values = {}
55
+
56
+ changes_dumped.each_with_index do |(name, value), i|
57
+ name_placeholder = "#_n#{i}"
58
+ value_placeholder = ":_s#{i}"
59
+
60
+ update_expression_statements << "#{name_placeholder} = #{value_placeholder}"
61
+ expression_attribute_names[name_placeholder] = name
62
+ expression_attribute_values[value_placeholder] = value
63
+ end
64
+
65
+ update_expression = "SET #{update_expression_statements.join(', ')}"
66
+
67
+ {
68
+ update: {
69
+ key: key,
70
+ table_name: @model_class.table_name,
71
+ update_expression: update_expression,
72
+ expression_attribute_names: expression_attribute_names,
73
+ expression_attribute_values: expression_attribute_values
74
+ }
75
+ }
76
+ end
77
+
78
+ private
79
+
80
+ def validate_primary_key!
81
+ raise Dynamoid::Errors::MissingHashKey if @hash_key.nil?
82
+ raise Dynamoid::Errors::MissingRangeKey if @model_class.range_key? && @range_key.nil?
83
+ end
84
+
85
+ def add_timestamps(attributes, skip_created_at: false)
86
+ return attributes unless @model_class.timestamps_enabled?
87
+
88
+ result = attributes.clone
89
+ timestamp = DateTime.now.in_time_zone(Time.zone)
90
+ result[:created_at] ||= timestamp unless skip_created_at
91
+ result[:updated_at] ||= timestamp
92
+ result
93
+ end
94
+ end
95
+ end
96
+ end