dynamoid 2.2.0 → 3.0.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +53 -0
  3. data/.rubocop_todo.yml +55 -0
  4. data/.travis.yml +5 -27
  5. data/Appraisals +17 -15
  6. data/CHANGELOG.md +26 -3
  7. data/Gemfile +4 -2
  8. data/README.md +95 -77
  9. data/Rakefile +17 -17
  10. data/Vagrantfile +5 -3
  11. data/dynamoid.gemspec +39 -45
  12. data/gemfiles/rails_4_2.gemfile +7 -5
  13. data/gemfiles/rails_5_0.gemfile +6 -4
  14. data/gemfiles/rails_5_1.gemfile +6 -4
  15. data/gemfiles/rails_5_2.gemfile +6 -4
  16. data/lib/dynamoid.rb +11 -4
  17. data/lib/dynamoid/adapter.rb +21 -27
  18. data/lib/dynamoid/adapter_plugin/{aws_sdk_v2.rb → aws_sdk_v3.rb} +118 -113
  19. data/lib/dynamoid/application_time_zone.rb +27 -0
  20. data/lib/dynamoid/associations.rb +3 -6
  21. data/lib/dynamoid/associations/association.rb +3 -6
  22. data/lib/dynamoid/associations/belongs_to.rb +4 -5
  23. data/lib/dynamoid/associations/has_and_belongs_to_many.rb +2 -3
  24. data/lib/dynamoid/associations/has_many.rb +2 -3
  25. data/lib/dynamoid/associations/has_one.rb +2 -3
  26. data/lib/dynamoid/associations/many_association.rb +8 -9
  27. data/lib/dynamoid/associations/single_association.rb +3 -3
  28. data/lib/dynamoid/components.rb +2 -2
  29. data/lib/dynamoid/config.rb +9 -5
  30. data/lib/dynamoid/config/backoff_strategies/constant_backoff.rb +4 -2
  31. data/lib/dynamoid/config/backoff_strategies/exponential_backoff.rb +3 -1
  32. data/lib/dynamoid/config/options.rb +4 -4
  33. data/lib/dynamoid/criteria.rb +3 -5
  34. data/lib/dynamoid/criteria/chain.rb +42 -49
  35. data/lib/dynamoid/dirty.rb +5 -4
  36. data/lib/dynamoid/document.rb +142 -36
  37. data/lib/dynamoid/dumping.rb +167 -0
  38. data/lib/dynamoid/dynamodb_time_zone.rb +16 -0
  39. data/lib/dynamoid/errors.rb +7 -6
  40. data/lib/dynamoid/fields.rb +24 -23
  41. data/lib/dynamoid/finders.rb +101 -59
  42. data/lib/dynamoid/identity_map.rb +5 -11
  43. data/lib/dynamoid/indexes.rb +45 -46
  44. data/lib/dynamoid/middleware/identity_map.rb +2 -0
  45. data/lib/dynamoid/persistence.rb +67 -307
  46. data/lib/dynamoid/primary_key_type_mapping.rb +34 -0
  47. data/lib/dynamoid/railtie.rb +3 -1
  48. data/lib/dynamoid/tasks/database.rake +11 -11
  49. data/lib/dynamoid/tasks/database.rb +4 -3
  50. data/lib/dynamoid/type_casting.rb +193 -0
  51. data/lib/dynamoid/undumping.rb +188 -0
  52. data/lib/dynamoid/validations.rb +4 -7
  53. data/lib/dynamoid/version.rb +3 -1
  54. metadata +59 -53
  55. data/gemfiles/rails_4_0.gemfile +0 -9
  56. data/gemfiles/rails_4_1.gemfile +0 -9
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dynamoid
2
4
  module Indexes
3
5
  extend ActiveSupport::Concern
@@ -27,20 +29,18 @@ module Dynamoid
27
29
  # index; does not work on existing indexes.
28
30
  # @option options [Integer] :write_capacity set the write capacity for
29
31
  # the index; does not work on existing indexes.
30
- def global_secondary_index(options={})
32
+ def global_secondary_index(options = {})
31
33
  unless options.present?
32
- raise Dynamoid::Errors::InvalidIndex.new('empty index definition')
34
+ raise Dynamoid::Errors::InvalidIndex, 'empty index definition'
33
35
  end
34
36
 
35
37
  unless options[:hash_key].present?
36
- raise Dynamoid::Errors::InvalidIndex.new(
37
- 'A global secondary index requires a :hash_key to be specified'
38
- )
38
+ raise Dynamoid::Errors::InvalidIndex, 'A global secondary index requires a :hash_key to be specified'
39
39
  end
40
40
 
41
41
  index_opts = {
42
- read_capacity: Dynamoid::Config.read_capacity,
43
- write_capacity: Dynamoid::Config.write_capacity
42
+ read_capacity: Dynamoid::Config.read_capacity,
43
+ write_capacity: Dynamoid::Config.write_capacity
44
44
  }.merge(options)
45
45
 
46
46
  index_opts[:dynamoid_class] = self
@@ -48,7 +48,7 @@ module Dynamoid
48
48
 
49
49
  index = Dynamoid::Indexes::Index.new(index_opts)
50
50
  gsi_key = index_key(options[:hash_key], options[:range_key])
51
- self.global_secondary_indexes[gsi_key] = index
51
+ global_secondary_indexes[gsi_key] = index
52
52
  self
53
53
  end
54
54
 
@@ -63,38 +63,39 @@ module Dynamoid
63
63
  # attributes to project for this index. Can be :keys_only, :all
64
64
  # or an array of included fields. If not specified, defaults to
65
65
  # :keys_only.
66
- def local_secondary_index(options={})
66
+ def local_secondary_index(options = {})
67
67
  unless options.present?
68
- raise Dynamoid::Errors::InvalidIndex.new('empty index definition')
68
+ raise Dynamoid::Errors::InvalidIndex, 'empty index definition'
69
69
  end
70
70
 
71
- primary_hash_key = self.hash_key
72
- primary_range_key = self.range_key
71
+ primary_hash_key = hash_key
72
+ primary_range_key = range_key
73
73
  index_range_key = options[:range_key]
74
74
 
75
75
  unless index_range_key.present?
76
- raise Dynamoid::Errors::InvalidIndex.new('A local secondary index '\
77
- 'requires a :range_key to be specified')
76
+ raise Dynamoid::Errors::InvalidIndex, 'A local secondary index '\
77
+ 'requires a :range_key to be specified'
78
78
  end
79
79
 
80
80
  if primary_range_key.present? && index_range_key == primary_range_key
81
- raise Dynamoid::Errors::InvalidIndex.new('A local secondary index'\
82
- ' must use a different :range_key than the primary key')
81
+ raise Dynamoid::Errors::InvalidIndex, 'A local secondary index'\
82
+ ' must use a different :range_key than the primary key'
83
83
  end
84
84
 
85
85
  index_opts = options.merge(
86
86
  dynamoid_class: self,
87
87
  type: :local_secondary,
88
- hash_key: primary_hash_key)
88
+ hash_key: primary_hash_key
89
+ )
89
90
 
90
91
  index = Dynamoid::Indexes::Index.new(index_opts)
91
92
  key = index_key(primary_hash_key, index_range_key)
92
- self.local_secondary_indexes[key] = index
93
+ local_secondary_indexes[key] = index
93
94
  self
94
95
  end
95
96
 
96
- def find_index(hash, range=nil)
97
- index = self.indexes[index_key(hash, range)]
97
+ def find_index(hash, range = nil)
98
+ index = indexes[index_key(hash, range)]
98
99
  index
99
100
  end
100
101
 
@@ -105,8 +106,8 @@ module Dynamoid
105
106
  # @param [Symbol] range range key name.
106
107
  # @return [Boolean] true iff provided keys correspond to a local
107
108
  # secondary index.
108
- def is_local_secondary_index?(hash, range=nil)
109
- self.local_secondary_indexes[index_key(hash, range)].present?
109
+ def is_local_secondary_index?(hash, range = nil)
110
+ local_secondary_indexes[index_key(hash, range)].present?
110
111
  end
111
112
 
112
113
  # Returns true iff the provided hash[,range] key combo is a global
@@ -116,8 +117,8 @@ module Dynamoid
116
117
  # @param [Symbol] range range key name.
117
118
  # @return [Boolean] true iff provided keys correspond to a global
118
119
  # secondary index.
119
- def is_global_secondary_index?(hash, range=nil)
120
- self.global_secondary_indexes[index_key(hash, range)].present?
120
+ def is_global_secondary_index?(hash, range = nil)
121
+ global_secondary_indexes[index_key(hash, range)].present?
121
122
  end
122
123
 
123
124
  # Generates a convenient lookup key name for a hash/range index.
@@ -126,11 +127,9 @@ module Dynamoid
126
127
  # @param [Symbol] hash hash key name.
127
128
  # @param [Symbol] range range key name.
128
129
  # @return [String] returns "hash" if hash only, "hash_range" otherwise.
129
- def index_key(hash, range=nil)
130
+ def index_key(hash, range = nil)
130
131
  name = hash.to_s
131
- if range.present?
132
- name += "_#{range.to_s}"
133
- end
132
+ name += "_#{range}" if range.present?
134
133
  name
135
134
  end
136
135
 
@@ -139,8 +138,8 @@ module Dynamoid
139
138
  # @param [Symbol] hash hash key name.
140
139
  # @param [Symbol] range range key name.
141
140
  # @return [String] index name of the form "table_name_index_index_key".
142
- def index_name(hash, range=nil)
143
- "#{self.table_name}_index_#{self.index_key(hash, range)}"
141
+ def index_name(hash, range = nil)
142
+ "#{table_name}_index_#{index_key(hash, range)}"
144
143
  end
145
144
 
146
145
  # Convenience method to return all indexes on the table.
@@ -148,11 +147,11 @@ module Dynamoid
148
147
  # @return [Hash<String, Object>] the combined hash of global and local
149
148
  # secondary indexes.
150
149
  def indexes
151
- self.local_secondary_indexes.merge(self.global_secondary_indexes)
150
+ local_secondary_indexes.merge(global_secondary_indexes)
152
151
  end
153
152
 
154
153
  def indexed_hash_keys
155
- self.global_secondary_indexes.map do |name, index|
154
+ global_secondary_indexes.map do |_name, index|
156
155
  index.hash_key.to_s
157
156
  end
158
157
  end
@@ -162,12 +161,12 @@ module Dynamoid
162
161
  class Index
163
162
  include ActiveModel::Validations
164
163
 
165
- PROJECTION_TYPES = [:keys_only, :all].to_set
164
+ PROJECTION_TYPES = %i[keys_only all].to_set
166
165
  DEFAULT_PROJECTION_TYPE = :keys_only
167
166
 
168
167
  attr_accessor :name, :dynamoid_class, :type, :hash_key, :range_key,
169
- :hash_key_schema, :range_key_schema, :projected_attributes,
170
- :read_capacity, :write_capacity
168
+ :hash_key_schema, :range_key_schema, :projected_attributes,
169
+ :read_capacity, :write_capacity
171
170
 
172
171
  validate do
173
172
  validate_index_type
@@ -176,9 +175,9 @@ module Dynamoid
176
175
  validate_projected_attributes
177
176
  end
178
177
 
179
- def initialize(attrs={})
178
+ def initialize(attrs = {})
180
179
  unless attrs[:dynamoid_class].present?
181
- raise Dynamoid::Errors::InvalidIndex.new(':dynamoid_class is required')
180
+ raise Dynamoid::Errors::InvalidIndex, ':dynamoid_class is required'
182
181
  end
183
182
 
184
183
  @dynamoid_class = attrs[:dynamoid_class]
@@ -187,11 +186,11 @@ module Dynamoid
187
186
  @range_key = attrs[:range_key]
188
187
  @name = attrs[:name] || @dynamoid_class.index_name(@hash_key, @range_key)
189
188
  @projected_attributes =
190
- attrs[:projected_attributes] || DEFAULT_PROJECTION_TYPE
189
+ attrs[:projected_attributes] || DEFAULT_PROJECTION_TYPE
191
190
  @read_capacity = attrs[:read_capacity]
192
191
  @write_capacity = attrs[:write_capacity]
193
192
 
194
- raise Dynamoid::Errors::InvalidIndex.new(self) unless self.valid?
193
+ raise Dynamoid::Errors::InvalidIndex, self unless valid?
195
194
  end
196
195
 
197
196
  # Convenience method to determine the projection type for an index.
@@ -209,16 +208,16 @@ module Dynamoid
209
208
  private
210
209
 
211
210
  def validate_projected_attributes
212
- unless (@projected_attributes.is_a?(Array) ||
213
- PROJECTION_TYPES.include?(@projected_attributes))
211
+ unless @projected_attributes.is_a?(Array) ||
212
+ PROJECTION_TYPES.include?(@projected_attributes)
214
213
  errors.add(:projected_attributes, 'Invalid projected attributes specified.')
215
214
  end
216
215
  end
217
216
 
218
217
  def validate_index_type
219
- unless (@type.present? &&
220
- [:local_secondary, :global_secondary].include?(@type))
221
- errors.add(:type, 'Invalid index :type specified')
218
+ unless @type.present? &&
219
+ %i[local_secondary global_secondary].include?(@type)
220
+ errors.add(:type, 'Invalid index :type specified')
222
221
  end
223
222
  end
224
223
 
@@ -229,7 +228,7 @@ module Dynamoid
229
228
  range_key_type = range_field_attributes[:type]
230
229
  if Dynamoid::Fields::PERMITTED_KEY_TYPES.include?(range_key_type)
231
230
  @range_key_schema = {
232
- @range_key => @dynamoid_class.dynamo_type(range_key_type)
231
+ @range_key => PrimaryKeyTypeMapping.dynamodb_type(range_key_type, range_field_attributes)
233
232
  }
234
233
  else
235
234
  errors.add(:range_key, 'Index :range_key is not a valid key type')
@@ -246,7 +245,7 @@ module Dynamoid
246
245
  hash_field_type = hash_field_attributes[:type]
247
246
  if Dynamoid::Fields::PERMITTED_KEY_TYPES.include?(hash_field_type)
248
247
  @hash_key_schema = {
249
- @hash_key => @dynamoid_class.dynamo_type(hash_field_type)
248
+ @hash_key => PrimaryKeyTypeMapping.dynamodb_type(hash_field_type, hash_field_attributes)
250
249
  }
251
250
  else
252
251
  errors.add(:hash_key, 'Index :hash_key is not a valid key type')
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dynamoid
2
4
  module Middleware
3
5
  class IdentityMap
@@ -1,25 +1,25 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bigdecimal'
2
4
  require 'securerandom'
3
5
  require 'yaml'
4
6
 
5
7
  # encoding: utf-8
6
8
  module Dynamoid
7
-
8
9
  # Persistence is responsible for dumping objects to and marshalling objects from the datastore. It tries to reserialize
9
10
  # values to be of the same type as when they were passed in, based on the fields in the class.
10
11
  module Persistence
11
12
  extend ActiveSupport::Concern
12
13
 
13
14
  attr_accessor :new_record
14
- alias :new_record? :new_record
15
+ alias new_record? new_record
15
16
 
16
17
  UNIX_EPOCH_DATE = Date.new(1970, 1, 1).freeze
17
18
 
18
19
  module ClassMethods
19
-
20
20
  def table_name
21
21
  table_base_name = options[:name] || base_class.name.split('::').last
22
- .downcase.pluralize
22
+ .downcase.pluralize
23
23
 
24
24
  @table_name ||= [Dynamoid::Config.namespace.to_s, table_base_name].reject(&:empty?).join('_')
25
25
  end
@@ -35,20 +35,19 @@ module Dynamoid
35
35
  # @option options [Symbol] :hash_key_type the dynamo type of the hash key (:string or :number)
36
36
  # @since 0.4.0
37
37
  def create_table(options = {})
38
- if self.range_key
39
- range_key_hash = { range_key => dynamo_type(attributes[range_key][:type]) }
40
- else
41
- range_key_hash = nil
42
- end
38
+ range_key_hash = if range_key
39
+ { range_key => PrimaryKeyTypeMapping.dynamodb_type(attributes[range_key][:type], attributes[range_key]) }
40
+ end
41
+
43
42
  options = {
44
- id: self.hash_key,
45
- table_name: self.table_name,
46
- write_capacity: self.write_capacity,
47
- read_capacity: self.read_capacity,
43
+ id: hash_key,
44
+ table_name: table_name,
45
+ write_capacity: write_capacity,
46
+ read_capacity: read_capacity,
48
47
  range_key: range_key_hash,
49
- hash_key_type: dynamo_type(attributes[self.hash_key][:type]),
50
- local_secondary_indexes: self.local_secondary_indexes.values,
51
- global_secondary_indexes: self.global_secondary_indexes.values
48
+ hash_key_type: PrimaryKeyTypeMapping.dynamodb_type(attributes[hash_key][:type], attributes[hash_key]),
49
+ local_secondary_indexes: local_secondary_indexes.values,
50
+ global_secondary_indexes: global_secondary_indexes.values
52
51
  }.merge(options)
53
52
 
54
53
  Dynamoid.adapter.create_table(options[:table_name], options[:id], options)
@@ -56,160 +55,13 @@ module Dynamoid
56
55
 
57
56
  # Deletes the table for the model
58
57
  def delete_table
59
- Dynamoid.adapter.delete_table(self.table_name)
58
+ Dynamoid.adapter.delete_table(table_name)
60
59
  end
61
60
 
62
61
  def from_database(attrs = {})
63
62
  clazz = attrs[:type] ? obj = attrs[:type].constantize : self
64
- clazz.new(attrs).tap { |r| r.new_record = false }
65
- end
66
-
67
- # Undump an object into a hash, converting each type from a string representation of itself into the type specified by the field.
68
- #
69
- # @since 0.2.0
70
- def undump(incoming = nil)
71
- incoming = (incoming || {}).symbolize_keys
72
- Hash.new.tap do |hash|
73
- self.attributes.each do |attribute, options|
74
- if incoming.has_key?(attribute)
75
- hash[attribute] = undump_field(incoming[attribute], options)
76
- elsif options.has_key?(:default)
77
- hash[attribute] = evaluate_default_value(options[:default])
78
- else
79
- hash[attribute] = nil
80
- end
81
- end
82
- incoming.each {|attribute, value| hash[attribute] = value unless hash.has_key? attribute }
83
- end
84
- end
85
-
86
- # Undump a string value for a given type.
87
- #
88
- # @since 0.2.0
89
- def undump_field(value, options)
90
- if (field_class = options[:type]).is_a?(Class)
91
- raise 'Dynamoid class-type fields do not support default values' if options[:default]
92
-
93
- if field_class.respond_to?(:dynamoid_load)
94
- field_class.dynamoid_load(value)
95
- end
96
- elsif options[:type] == :serialized
97
- if value.is_a?(String)
98
- options[:serializer] ? options[:serializer].load(value) : YAML.load(value)
99
- else
100
- value
101
- end
102
- else
103
- unless value.nil?
104
- case options[:type]
105
- when :string
106
- value.to_s
107
- when :integer
108
- Integer(value)
109
- when :number
110
- BigDecimal.new(value.to_s)
111
- when :array
112
- value.to_a
113
- when :raw
114
- if value.is_a?(Hash)
115
- undump_hash(value)
116
- else
117
- value
118
- end
119
- when :set
120
- undump_set(options, value)
121
- when :datetime
122
- parse_datetime(value, options)
123
- when :date
124
- if value.is_a?(Date) || value.is_a?(DateTime) || value.is_a?(Time)
125
- value.to_date
126
- else
127
- parse_date(value, options)
128
- end
129
- when :boolean
130
- if value == 't' || value == true
131
- true
132
- elsif value == 'f' || value == false
133
- false
134
- else
135
- raise ArgumentError, 'Boolean column neither true nor false'
136
- end
137
- else
138
- raise ArgumentError, "Unknown type #{options[:type]}"
139
- end
140
- end
141
- end
142
- end
143
-
144
- def undump_set(options, value)
145
- case options[:of]
146
- when :integer
147
- value.map { |v| Integer(v) }.to_set
148
- when :number
149
- value.map { |v| BigDecimal.new(v.to_s) }.to_set
150
- else
151
- value.is_a?(Set) ? value : Set.new(value)
152
- end
153
- end
154
-
155
- def dump_field(value, options)
156
- if (field_class = options[:type]).is_a?(Class)
157
- if value.respond_to?(:dynamoid_dump)
158
- value.dynamoid_dump
159
- elsif field_class.respond_to?(:dynamoid_dump)
160
- field_class.dynamoid_dump(value)
161
- else
162
- raise ArgumentError, "Neither #{field_class} nor #{value} support serialization for Dynamoid."
163
- end
164
- else
165
- case options[:type]
166
- when :string
167
- !value.nil? ? value.to_s : nil
168
- when :integer
169
- !value.nil? ? Integer(value) : nil
170
- when :number
171
- !value.nil? ? value : nil
172
- when :set
173
- !value.nil? ? Set.new(value) : nil
174
- when :array
175
- !value.nil? ? value : nil
176
- when :datetime
177
- !value.nil? ? format_datetime(value, options) : nil
178
- when :date
179
- !value.nil? ? format_date(value, options) : nil
180
- when :serialized
181
- options[:serializer] ? options[:serializer].dump(value) : value.to_yaml
182
- when :raw
183
- !value.nil? ? value : nil
184
- when :boolean
185
- if !value.nil?
186
- if options[:store_as_native_boolean]
187
- !!value # native boolean type
188
- else
189
- value.to_s[0] # => "f" or "t"
190
- end
191
- else
192
- nil
193
- end
194
- else
195
- raise ArgumentError, "Unknown type #{options[:type]}"
196
- end
197
- end
198
- end
199
-
200
- def dynamo_type(type)
201
- if type.is_a?(Class)
202
- type.respond_to?(:dynamoid_field_type) ? type.dynamoid_field_type : :string
203
- else
204
- case type
205
- when :integer, :number, :datetime, :date
206
- :number
207
- when :string, :serialized
208
- :string
209
- else
210
- raise 'unknown type'
211
- end
212
- end
63
+ attrs_undumped = Undumping.undump_attributes(attrs, clazz.attributes)
64
+ clazz.new(attrs_undumped).tap { |r| r.new_record = false }
213
65
  end
214
66
 
215
67
  # Creates several models at once.
@@ -226,16 +78,27 @@ module Dynamoid
226
78
  # User.import([{ name: 'a' }, { name: 'b' }])
227
79
  def import(objects)
228
80
  documents = objects.map do |attrs|
229
- self.build(attrs).tap do |item|
81
+ attrs = attrs.symbolize_keys
82
+
83
+ if Dynamoid::Config.timestamps
84
+ time_now = DateTime.now.in_time_zone(Time.zone)
85
+ attrs[:created_at] ||= time_now
86
+ attrs[:updated_at] ||= time_now
87
+ end
88
+
89
+ build(attrs).tap do |item|
230
90
  item.hash_key = SecureRandom.uuid if item.hash_key.blank?
231
91
  end
232
92
  end
233
93
 
234
- unless Dynamoid.config.backoff
235
- Dynamoid.adapter.batch_write_item(self.table_name, documents.map(&:dump))
236
- else
94
+ if Dynamoid.config.backoff
237
95
  backoff = nil
238
- Dynamoid.adapter.batch_write_item(self.table_name, documents.map(&:dump)) do |has_unprocessed_items|
96
+
97
+ array = documents.map do |d|
98
+ Dumping.dump_attributes(d.attributes, attributes)
99
+ end
100
+
101
+ Dynamoid.adapter.batch_write_item(table_name, array) do |has_unprocessed_items|
239
102
  if has_unprocessed_items
240
103
  backoff ||= Dynamoid.config.build_backoff
241
104
  backoff.call
@@ -243,106 +106,16 @@ module Dynamoid
243
106
  backoff = nil
244
107
  end
245
108
  end
246
- end
247
-
248
- documents.each { |d| d.new_record = false }
249
- documents
250
- end
251
-
252
- private
253
-
254
- def undump_hash(hash)
255
- {}.tap do |h|
256
- hash.each { |key, value| h[key.to_sym] = undump_hash_value(value) }
257
- end
258
- end
259
-
260
- def undump_hash_value(val)
261
- case val
262
- when BigDecimal
263
- if Dynamoid::Config.convert_big_decimal
264
- val.to_f
265
- else
266
- val
267
- end
268
- when Hash
269
- undump_hash(val)
270
- when Array
271
- val.map { |v| undump_hash_value(v) }
272
- else
273
- val
274
- end
275
- end
276
-
277
- def format_datetime(value, options)
278
- use_string_format = options[:store_as_string].nil? \
279
- ? Dynamoid.config.store_datetime_as_string \
280
- : options[:store_as_string]
281
-
282
- if use_string_format
283
- value.to_time.iso8601
284
109
  else
285
- unless value.respond_to?(:to_i) && value.respond_to?(:nsec)
286
- value = value.to_time
110
+ array = documents.map do |d|
111
+ Dumping.dump_attributes(d.attributes, attributes)
287
112
  end
288
- BigDecimal("%d.%09d" % [value.to_i, value.nsec])
289
- end
290
- end
291
-
292
- def format_date(value, options)
293
- use_string_format = options[:store_as_string].nil? \
294
- ? Dynamoid.config.store_date_as_string \
295
- : options[:store_as_string]
296
-
297
- unless use_string_format
298
- (value.to_date - UNIX_EPOCH_DATE).to_i
299
- else
300
- value.to_date.iso8601
301
- end
302
- end
303
-
304
- def parse_datetime(value, options)
305
- return value if value.is_a?(Date) || value.is_a?(DateTime) || value.is_a?(Time)
306
-
307
- use_string_format = options[:store_as_string].nil? \
308
- ? Dynamoid.config.store_datetime_as_string \
309
- : options[:store_as_string]
310
- value = DateTime.iso8601(value).to_time.to_i if use_string_format
311
-
312
- case Dynamoid::Config.application_timezone
313
- when :utc
314
- ActiveSupport::TimeZone['UTC'].at(value).to_datetime
315
- when :local
316
- Time.at(value).to_datetime
317
- when String
318
- ActiveSupport::TimeZone[Dynamoid::Config.application_timezone].at(value).to_datetime
319
- end
320
- end
321
-
322
- def parse_date(value, options)
323
- use_string_format = options[:store_as_string].nil? \
324
- ? Dynamoid.config.store_date_as_string \
325
- : options[:store_as_string]
326
113
 
327
- unless use_string_format
328
- UNIX_EPOCH_DATE + value.to_i
329
- else
330
- Date.iso8601(value)
114
+ Dynamoid.adapter.batch_write_item(table_name, array)
331
115
  end
332
- end
333
116
 
334
- # Evaluates the default value given, this is used by undump
335
- # when determining the value of the default given for a field options.
336
- #
337
- # @param [Object] :value the attribute's default value
338
- def evaluate_default_value(val)
339
- if val.respond_to?(:call)
340
- val.call
341
- elsif val.duplicable?
342
- val.dup
343
- else
344
- val
345
- end
117
+ documents.each { |d| d.new_record = false }
118
+ documents
346
119
  end
347
120
  end
348
121
 
@@ -365,12 +138,12 @@ module Dynamoid
365
138
  # Run the callbacks and then persist this object in the datastore.
366
139
  #
367
140
  # @since 0.2.0
368
- def save(options = {})
141
+ def save(_options = {})
369
142
  self.class.create_table
370
143
 
371
144
  if new_record?
372
- conditions = { unless_exists: [self.class.hash_key]}
373
- conditions[:unless_exists] << range_key if(range_key)
145
+ conditions = { unless_exists: [self.class.hash_key] }
146
+ conditions[:unless_exists] << range_key if range_key
374
147
 
375
148
  run_callbacks(:create) { persist(conditions) }
376
149
  else
@@ -383,19 +156,23 @@ module Dynamoid
383
156
  # never cause an update! to fail, but an update! may cause a concurrent save to fail.
384
157
  #
385
158
  #
386
- def update!(conditions = {}, &block)
159
+ def update!(conditions = {})
387
160
  run_callbacks(:update) do
388
- options = range_key ? {range_key: dump_field(self.read_attribute(range_key), self.class.attributes[range_key])} : {}
161
+ options = range_key ? { range_key: Dumping.dump_field(read_attribute(range_key), self.class.attributes[range_key]) } : {}
389
162
 
390
163
  begin
391
- new_attrs = Dynamoid.adapter.update_item(self.class.table_name, self.hash_key, options.merge(conditions: conditions)) do |t|
392
- if(self.class.attributes[:lock_version])
393
- t.add(lock_version: 1)
164
+ new_attrs = Dynamoid.adapter.update_item(self.class.table_name, hash_key, options.merge(conditions: conditions)) do |t|
165
+ t.add(lock_version: 1) if self.class.attributes[:lock_version]
166
+
167
+ if Dynamoid::Config.timestamps
168
+ time_now = DateTime.now.in_time_zone(Time.zone)
169
+ time_now_dumped = Dumping.dump_field(time_now, self.class.attributes[:updated_at])
170
+ t.set(updated_at: time_now_dumped)
394
171
  end
395
172
 
396
173
  yield t
397
174
  end
398
- load(new_attrs)
175
+ load(Undumping.undump_attributes(new_attrs, self.class.attributes))
399
176
  rescue Dynamoid::Errors::ConditionalCheckFailedException
400
177
  raise Dynamoid::Errors::StaleObjectError.new(self, 'update')
401
178
  end
@@ -414,81 +191,64 @@ module Dynamoid
414
191
  # @since 0.2.0
415
192
  def destroy
416
193
  ret = run_callbacks(:destroy) do
417
- self.delete
194
+ delete
418
195
  end
419
- (ret == false) ? false : self
196
+ ret == false ? false : self
420
197
  end
421
198
 
422
199
  def destroy!
423
- destroy || raise(Dynamoid::Errors::RecordNotDestroyed.new(self))
200
+ destroy || (raise Dynamoid::Errors::RecordNotDestroyed, self)
424
201
  end
425
202
 
426
203
  # Delete this object from the datastore.
427
204
  #
428
205
  # @since 0.2.0
429
206
  def delete
430
- options = range_key ? {range_key: dump_field(self.read_attribute(range_key), self.class.attributes[range_key])} : {}
207
+ options = range_key ? { range_key: Dumping.dump_field(read_attribute(range_key), self.class.attributes[range_key]) } : {}
431
208
 
432
209
  # Add an optimistic locking check if the lock_version column exists
433
- if(self.class.attributes[:lock_version])
434
- conditions = {if: {}}
210
+ if self.class.attributes[:lock_version]
211
+ conditions = { if: {} }
435
212
  conditions[:if][:lock_version] =
436
213
  if changes[:lock_version].nil?
437
- self.lock_version
214
+ lock_version
438
215
  else
439
216
  changes[:lock_version][0]
440
217
  end
441
218
  options[:conditions] = conditions
442
219
  end
443
- Dynamoid.adapter.delete(self.class.table_name, self.hash_key, options)
220
+ Dynamoid.adapter.delete(self.class.table_name, hash_key, options)
444
221
  rescue Dynamoid::Errors::ConditionalCheckFailedException
445
222
  raise Dynamoid::Errors::StaleObjectError.new(self, 'delete')
446
223
  end
447
224
 
448
- # Dump this object's attributes into hash form, fit to be persisted into the datastore.
449
- #
450
- # @since 0.2.0
451
- def dump
452
- Hash.new.tap do |hash|
453
- self.class.attributes.each do |attribute, options|
454
- hash[attribute] = dump_field(self.read_attribute(attribute), options)
455
- end
456
- end
457
- end
458
-
459
225
  private
460
226
 
461
- # Determine how to dump this field. Given a value, it'll determine how to turn it into a value that can be
462
- # persisted into the datastore.
463
- #
464
- # @since 0.2.0
465
- def dump_field(value, options)
466
- self.class.dump_field(value, options)
467
- end
468
-
469
227
  # Persist the object into the datastore. Assign it an id first if it doesn't have one.
470
228
  #
471
229
  # @since 0.2.0
472
230
  def persist(conditions = nil)
473
231
  run_callbacks(:save) do
474
- self.hash_key = SecureRandom.uuid if self.hash_key.blank?
232
+ self.hash_key = SecureRandom.uuid if hash_key.blank?
475
233
 
476
234
  # Add an exists check to prevent overwriting existing records with new ones
477
- if(new_record?)
235
+ if new_record?
478
236
  conditions ||= {}
479
237
  (conditions[:unless_exists] ||= []) << self.class.hash_key
480
238
  end
481
239
 
482
240
  # Add an optimistic locking check if the lock_version column exists
483
- if(self.class.attributes[:lock_version])
241
+ if self.class.attributes[:lock_version]
484
242
  conditions ||= {}
485
243
  self.lock_version = (lock_version || 0) + 1
486
244
  # Uses the original lock_version value from ActiveModel::Dirty in case user changed lock_version manually
487
- (conditions[:if] ||= {})[:lock_version] = changes[:lock_version][0] if(changes[:lock_version][0])
245
+ (conditions[:if] ||= {})[:lock_version] = changes[:lock_version][0] if changes[:lock_version][0]
488
246
  end
489
247
 
248
+ attributes_dumped = Dumping.dump_attributes(attributes, self.class.attributes)
249
+
490
250
  begin
491
- Dynamoid.adapter.write(self.class.table_name, self.dump, conditions)
251
+ Dynamoid.adapter.write(self.class.table_name, attributes_dumped, conditions)
492
252
  @new_record = false
493
253
  true
494
254
  rescue Dynamoid::Errors::ConditionalCheckFailedException => e