dynamoid 3.9.0 → 3.11.0

Sign up to get free protection for your applications and to get access to all the features.
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