dynamoid 2.2.0 → 3.0.0

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