dynamoid 3.0.0 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -64,6 +64,7 @@ module Dynamoid #:nodoc:
64
64
  end
65
65
  end
66
66
  define_method("#{named}=") { |value| write_attribute(named, value) }
67
+ define_method("#{named}_before_type_cast") { read_attribute_before_type_cast(named) }
67
68
  end
68
69
  end
69
70
 
@@ -88,6 +89,7 @@ module Dynamoid #:nodoc:
88
89
  remove_method field
89
90
  remove_method :"#{field}="
90
91
  remove_method :"#{field}?"
92
+ remove_method:"#{field}_before_type_cast"
91
93
  end
92
94
  end
93
95
 
@@ -119,6 +121,8 @@ module Dynamoid #:nodoc:
119
121
  association.reset
120
122
  end
121
123
 
124
+ @attributes_before_type_cast[name] = value
125
+
122
126
  value_casted = TypeCasting.cast_field(value, self.class.attributes[name])
123
127
  attributes[name] = value_casted
124
128
  end
@@ -134,25 +138,17 @@ module Dynamoid #:nodoc:
134
138
  end
135
139
  alias [] read_attribute
136
140
 
137
- # Updates multiple attibutes at once, saving the object once the updates are complete.
138
- #
139
- # @param [Hash] attributes a hash of attributes to update
140
- #
141
- # @since 0.2.0
142
- def update_attributes(attributes)
143
- attributes.each { |attribute, value| write_attribute(attribute, value) } unless attributes.nil? || attributes.empty?
144
- save
141
+ # Returns a hash of attributes before typecasting
142
+ def attributes_before_type_cast
143
+ @attributes_before_type_cast
145
144
  end
146
145
 
147
- # Update a single attribute, saving the object afterwards.
146
+ # Returns the value of the attribute identified by name before typecasting
148
147
  #
149
- # @param [Symbol] attribute the attribute to update
150
- # @param [Object] value the value to assign it
151
- #
152
- # @since 0.2.0
153
- def update_attribute(attribute, value)
154
- write_attribute(attribute, value)
155
- save
148
+ # @param [Symbol] attribute name
149
+ def read_attribute_before_type_cast(name)
150
+ return nil unless name.respond_to?(:to_sym)
151
+ @attributes_before_type_cast[name.to_sym]
156
152
  end
157
153
 
158
154
  private
@@ -173,8 +169,14 @@ module Dynamoid #:nodoc:
173
169
  end
174
170
  end
175
171
 
176
- def set_type
177
- self.type ||= self.class.name if self.class.attributes[:type]
172
+ def set_inheritance_field
173
+ # actually it does only following logic:
174
+ # self.type ||= self.class.name if self.class.attributes[:type]
175
+
176
+ type = self.class.inheritance_field
177
+ if self.class.attributes[type] && self.send(type).nil?
178
+ self.send("#{type}=", self.class.name)
179
+ end
178
180
  end
179
181
  end
180
182
  end
@@ -37,12 +37,17 @@ module Dynamoid
37
37
  # @example Find several documents by partition key and sort key
38
38
  # Document.find([[101, 'archived'], [102, 'new'], [103, 'deleted']])
39
39
  #
40
+ # @example Perform strong consistent reads
41
+ # Document.find(101, consistent_read: true)
42
+ # Document.find(101, 102, 103, consistent_read: true)
43
+ # Document.find(101, range_key: 'archived', consistent_read: true)
44
+ #
40
45
  # @since 0.2.0
41
46
  def find(*ids, **options)
42
47
  if ids.size == 1 && !ids[0].is_a?(Array)
43
48
  _find_by_id(ids[0], options.merge(raise_error: true))
44
49
  else
45
- _find_all(ids.flatten(1), raise_error: true)
50
+ _find_all(ids.flatten(1), options.merge(raise_error: true))
46
51
  end
47
52
  end
48
53
 
@@ -95,10 +100,12 @@ module Dynamoid
95
100
  end
96
101
  end
97
102
 
103
+ read_options = options.slice(:consistent_read)
104
+
98
105
  items = if Dynamoid.config.backoff
99
106
  items = []
100
107
  backoff = nil
101
- Dynamoid.adapter.read(table_name, ids, options) do |hash, has_unprocessed_items|
108
+ Dynamoid.adapter.read(table_name, ids, read_options) do |hash, has_unprocessed_items|
102
109
  items += hash[table_name]
103
110
 
104
111
  if has_unprocessed_items
@@ -110,14 +117,15 @@ module Dynamoid
110
117
  end
111
118
  items
112
119
  else
113
- items = Dynamoid.adapter.read(table_name, ids, options)
120
+ items = Dynamoid.adapter.read(table_name, ids, read_options)
114
121
  items ? items[table_name] : []
115
122
  end
116
123
 
117
124
  if items.size == ids.size || !options[:raise_error]
118
125
  items ? items.map { |i| from_database(i) } : []
119
126
  else
120
- message = "Couldn't find all #{name.pluralize} with '#{hash_key}': (#{ids.join(', ')}) "
127
+ ids_list = range_key ? ids.map { |pk, sk| "(#{pk},#{sk})" } : ids.map(&:to_s)
128
+ message = "Couldn't find all #{name.pluralize} with primary keys [#{ids_list.join(', ')}] "
121
129
  message += "(found #{items.size} results, but was looking for #{ids.size})"
122
130
  raise Errors::RecordNotFound, message
123
131
  end
@@ -132,10 +140,11 @@ module Dynamoid
132
140
  options[:range_key] = key_dumped
133
141
  end
134
142
 
135
- if item = Dynamoid.adapter.read(table_name, id, options)
143
+ if item = Dynamoid.adapter.read(table_name, id, options.slice(:range_key, :consistent_read))
136
144
  from_database(item)
137
145
  elsif options[:raise_error]
138
- message = "Couldn't find #{name} with '#{hash_key}'=#{id}"
146
+ primary_key = range_key ? "(#{id},#{options[:range_key]})" : id
147
+ message = "Couldn't find #{name} with primary key #{primary_key}"
139
148
  raise Errors::RecordNotFound, message
140
149
  end
141
150
  end
@@ -59,7 +59,7 @@ module Dynamoid
59
59
  end
60
60
 
61
61
  def from_database(attrs = {})
62
- clazz = attrs[:type] ? obj = attrs[:type].constantize : self
62
+ clazz = choose_right_class(attrs)
63
63
  attrs_undumped = Undumping.undump_attributes(attrs, clazz.attributes)
64
64
  clazz.new(attrs_undumped).tap { |r| r.new_record = false }
65
65
  end
@@ -151,6 +151,36 @@ module Dynamoid
151
151
  end
152
152
  end
153
153
 
154
+ # Updates multiple attibutes at once, saving the object once the updates are complete.
155
+ #
156
+ # @param [Hash] attributes a hash of attributes to update
157
+ #
158
+ # @since 0.2.0
159
+ def update_attributes(attributes)
160
+ attributes.each { |attribute, value| write_attribute(attribute, value) } unless attributes.nil? || attributes.empty?
161
+ save
162
+ end
163
+
164
+ # Updates multiple attibutes at once, saving the object once the updates are complete.
165
+ # Raises a Dynamoid::Errors::DocumentNotValid exception if there is vaidation and it fails.
166
+ #
167
+ # @param [Hash] attributes a hash of attributes to update
168
+ def update_attributes!(attributes)
169
+ attributes.each { |attribute, value| write_attribute(attribute, value) } unless attributes.nil? || attributes.empty?
170
+ save!
171
+ end
172
+
173
+ # Update a single attribute, saving the object afterwards.
174
+ #
175
+ # @param [Symbol] attribute the attribute to update
176
+ # @param [Object] value the value to assign it
177
+ #
178
+ # @since 0.2.0
179
+ def update_attribute(attribute, value)
180
+ write_attribute(attribute, value)
181
+ save
182
+ end
183
+
154
184
  #
155
185
  # update!() will increment the lock_version if the table has the column, but will not check it. Thus, a concurrent save will
156
186
  # never cause an update! to fail, but an update! may cause a concurrent save to fail.
@@ -186,6 +216,36 @@ module Dynamoid
186
216
  false
187
217
  end
188
218
 
219
+ # Initializes attribute to zero if nil and adds the value passed as by (default is 1).
220
+ # Only makes sense for number-based attributes. Returns self.
221
+ def increment(attribute, by = 1)
222
+ self[attribute] ||= 0
223
+ self[attribute] += by
224
+ self
225
+ end
226
+
227
+ # Wrapper around increment that saves the record.
228
+ # Returns true if the record could be saved.
229
+ def increment!(attribute, by = 1)
230
+ increment(attribute, by)
231
+ save
232
+ end
233
+
234
+ # Initializes attribute to zero if nil and subtracts the value passed as by (default is 1).
235
+ # Only makes sense for number-based attributes. Returns self.
236
+ def decrement(attribute, by = 1)
237
+ self[attribute] ||= 0
238
+ self[attribute] -= by
239
+ self
240
+ end
241
+
242
+ # Wrapper around decrement that saves the record.
243
+ # Returns true if the record could be saved.
244
+ def decrement!(attribute, by = 1)
245
+ decrement(attribute, by)
246
+ save
247
+ end
248
+
189
249
  # Delete this object, but only after running callbacks for it.
190
250
  #
191
251
  # @since 0.2.0
@@ -7,7 +7,7 @@ namespace :dynamoid do
7
7
  desc 'Creates DynamoDB tables, one for each of your Dynamoid models - does not modify pre-existing tables'
8
8
  task create_tables: :environment do
9
9
  # Load models so Dynamoid will be able to discover tables expected.
10
- Dir[File.join(Dynamoid::Config.models_dir, '*.rb')].sort.each { |file| require file }
10
+ Dir[File.join(Dynamoid::Config.models_dir, '**/*.rb')].sort.each { |file| require file }
11
11
  if Dynamoid.included_models.any?
12
12
  tables = Dynamoid::Tasks::Database.create_tables
13
13
  result = tables[:created].map { |c| "#{c} created" } + tables[:existing].map { |e| "#{e} already exists" }
@@ -14,7 +14,7 @@ module Dynamoid
14
14
  if Dynamoid.adapter.list_tables.include? model.table_name
15
15
  results[:existing] << model.table_name
16
16
  else
17
- model.create_table
17
+ model.create_table(sync: true)
18
18
  results[:created] << model.table_name
19
19
  end
20
20
  end
@@ -106,6 +106,18 @@ module Dynamoid
106
106
 
107
107
  class SetTypeCaster < Base
108
108
  def process(value)
109
+ set = type_cast_to_set(value)
110
+
111
+ if set.present? && @options[:of].present?
112
+ process_typed_set(set)
113
+ else
114
+ set
115
+ end
116
+ end
117
+
118
+ private
119
+
120
+ def type_cast_to_set(value)
109
121
  if value.is_a?(Set)
110
122
  value.dup
111
123
  elsif value.respond_to?(:to_set)
@@ -114,10 +126,50 @@ module Dynamoid
114
126
  nil
115
127
  end
116
128
  end
129
+
130
+ def process_typed_set(set)
131
+ type_caster = TypeCasting.find_type_caster(element_options)
132
+
133
+ if type_caster.nil?
134
+ raise ArgumentError, "Set element type #{element_type} isn't supported"
135
+ end
136
+
137
+ set.map { |el| type_caster.process(el) }.to_set
138
+ end
139
+
140
+ def element_type
141
+ unless @options[:of].is_a?(Hash)
142
+ @options[:of]
143
+ else
144
+ @options[:of].keys.first
145
+ end
146
+ end
147
+
148
+ def element_options
149
+ unless @options[:of].is_a?(Hash)
150
+ { type: element_type }
151
+ else
152
+ @options[:of][element_type].dup.tap do |options|
153
+ options[:type] = element_type
154
+ end
155
+ end
156
+ end
117
157
  end
118
158
 
119
159
  class ArrayTypeCaster < Base
120
160
  def process(value)
161
+ array = type_cast_to_array(value)
162
+
163
+ if array.present? && @options[:of].present?
164
+ process_typed_array(array)
165
+ else
166
+ array
167
+ end
168
+ end
169
+
170
+ private
171
+
172
+ def type_cast_to_array(value)
121
173
  if value.is_a?(Array)
122
174
  value.dup
123
175
  elsif value.respond_to?(:to_a)
@@ -126,6 +178,34 @@ module Dynamoid
126
178
  nil
127
179
  end
128
180
  end
181
+
182
+ def process_typed_array(array)
183
+ type_caster = TypeCasting.find_type_caster(element_options)
184
+
185
+ if type_caster.nil?
186
+ raise ArgumentError, "Set element type #{element_type} isn't supported"
187
+ end
188
+
189
+ array.map { |el| type_caster.process(el) }
190
+ end
191
+
192
+ def element_type
193
+ unless @options[:of].is_a?(Hash)
194
+ @options[:of]
195
+ else
196
+ @options[:of].keys.first
197
+ end
198
+ end
199
+
200
+ def element_options
201
+ unless @options[:of].is_a?(Hash)
202
+ { type: element_type }
203
+ else
204
+ @options[:of][element_type].dup.tap do |options|
205
+ options[:type] = element_type
206
+ end
207
+ end
208
+ end
129
209
  end
130
210
 
131
211
  class DateTimeTypeCaster < Base
@@ -4,20 +4,24 @@ module Dynamoid
4
4
  module Undumping
5
5
  def self.undump_attributes(attributes, attributes_options)
6
6
  {}.tap do |h|
7
- attributes.symbolize_keys.each do |attribute, value|
8
- h[attribute] = undump_field(value, attributes_options[attribute])
7
+ # ignore existing attributes not declared in document class
8
+ attributes.symbolize_keys
9
+ .select { |attribute| attributes_options.key?(attribute) }
10
+ .each do |attribute, value|
11
+ h[attribute] = undump_field(value, attributes_options[attribute])
9
12
  end
10
13
  end
11
14
  end
12
15
 
13
16
  def self.undump_field(value, options)
17
+ return nil if value.nil?
18
+
14
19
  undumper = find_undumper(options)
15
20
 
16
21
  if undumper.nil?
17
22
  raise ArgumentError, "Unknown type #{options[:type]}"
18
23
  end
19
24
 
20
- return nil if value.nil?
21
25
  undumper.process(value)
22
26
  end
23
27
 
@@ -64,19 +68,93 @@ module Dynamoid
64
68
  end
65
69
 
66
70
  class SetUndumper < Base
67
- def process(value)
68
- case @options[:of]
69
- when :integer
70
- value.map { |v| Integer(v) }.to_set
71
- when :number
72
- value.map { |v| BigDecimal(v.to_s) }.to_set
71
+ ALLOWED_TYPES = [:string, :integer, :number, :date, :datetime, :serialized]
72
+
73
+ def process(set)
74
+ if @options.key?(:of)
75
+ process_typed_collection(set)
76
+ else
77
+ set.is_a?(Set) ? set : Set.new(set)
78
+ end
79
+ end
80
+
81
+ private
82
+
83
+ def process_typed_collection(set)
84
+ if allowed_type?
85
+ undumper = Undumping.find_undumper(element_options)
86
+ set.map { |el| undumper.process(el) }.to_set
73
87
  else
74
- value.is_a?(Set) ? value : Set.new(value)
88
+ raise ArgumentError, "Set element type #{element_type} isn't supported"
89
+ end
90
+ end
91
+
92
+ def allowed_type?
93
+ ALLOWED_TYPES.include?(element_type) || element_type.is_a?(Class)
94
+ end
95
+
96
+ def element_type
97
+ unless @options[:of].is_a?(Hash)
98
+ @options[:of]
99
+ else
100
+ @options[:of].keys.first
101
+ end
102
+ end
103
+
104
+ def element_options
105
+ unless @options[:of].is_a?(Hash)
106
+ { type: element_type }
107
+ else
108
+ @options[:of][element_type].dup.tap do |options|
109
+ options[:type] = element_type
110
+ end
75
111
  end
76
112
  end
77
113
  end
78
114
 
79
115
  class ArrayUndumper < Base
116
+ ALLOWED_TYPES = [:string, :integer, :number, :date, :datetime, :serialized]
117
+
118
+ def process(array)
119
+ if @options.key?(:of)
120
+ process_typed_collection(array)
121
+ else
122
+ array.is_a?(Array) ? array : Array(array)
123
+ end
124
+ end
125
+
126
+ private
127
+
128
+ def process_typed_collection(array)
129
+ if allowed_type?
130
+ undumper = Undumping.find_undumper(element_options)
131
+ array.map { |el| undumper.process(el) }
132
+ else
133
+ raise ArgumentError, "Array element type #{element_type} isn't supported"
134
+ end
135
+ end
136
+
137
+ def allowed_type?
138
+ ALLOWED_TYPES.include?(element_type) || element_type.is_a?(Class)
139
+ end
140
+
141
+ def element_type
142
+ unless @options[:of].is_a?(Hash)
143
+ @options[:of]
144
+ else
145
+ @options[:of].keys.first
146
+ end
147
+ end
148
+
149
+ def element_options
150
+ unless @options[:of].is_a?(Hash)
151
+ { type: element_type }
152
+ else
153
+ @options[:of][element_type].dup.tap do |options|
154
+ options[:type] = element_type
155
+ end
156
+ end
157
+ end
80
158
  end
81
159
 
82
160
  class DateTimeUndumper < Base