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
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dynamoid
4
+ module DynamodbTimeZone
5
+ def self.in_time_zone(value)
6
+ case Dynamoid::Config.dynamodb_timezone
7
+ when :utc
8
+ value.utc.to_datetime
9
+ when :local
10
+ value.getlocal.to_datetime
11
+ else
12
+ value.in_time_zone(Dynamoid::Config.dynamodb_timezone).to_datetime
13
+ end
14
+ end
15
+ end
16
+ end
@@ -1,9 +1,8 @@
1
- # encoding: utf-8
2
- module Dynamoid
1
+ # frozen_string_literal: true
3
2
 
3
+ module Dynamoid
4
4
  # All the errors specific to Dynamoid. The goal is to mimic ActiveRecord.
5
5
  module Errors
6
-
7
6
  # Generic Dynamoid error
8
7
  class Error < StandardError; end
9
8
 
@@ -15,10 +14,10 @@ module Dynamoid
15
14
  # specified key attribute(s) or projected attributes do not exist.
16
15
  class InvalidIndex < Error
17
16
  def initialize(item)
18
- if (item.is_a? String)
17
+ if item.is_a? String
19
18
  super(item)
20
19
  else
21
- super("Validation failed: #{item.errors.full_messages.join(", ")}")
20
+ super("Validation failed: #{item.errors.full_messages.join(', ')}")
22
21
  end
23
22
  end
24
23
  end
@@ -68,11 +67,13 @@ module Dynamoid
68
67
  attr_reader :document
69
68
 
70
69
  def initialize(document)
71
- super("Validation failed: #{document.errors.full_messages.join(", ")}")
70
+ super("Validation failed: #{document.errors.full_messages.join(', ')}")
72
71
  @document = document
73
72
  end
74
73
  end
75
74
 
76
75
  class InvalidQuery < Error; end
76
+
77
+ class UnsupportedKeyType < Error; end
77
78
  end
78
79
  end
@@ -1,4 +1,5 @@
1
- # encoding: utf-8
1
+ # frozen_string_literal: true
2
+
2
3
  module Dynamoid #:nodoc:
3
4
  # All fields on a Dynamoid::Document must be explicitly defined -- if you have fields in the database that are not
4
5
  # specified with field, then they will be ignored.
@@ -6,13 +7,13 @@ module Dynamoid #:nodoc:
6
7
  extend ActiveSupport::Concern
7
8
 
8
9
  # Types allowed in indexes:
9
- PERMITTED_KEY_TYPES = [
10
- :number,
11
- :integer,
12
- :string,
13
- :datetime,
14
- :serialized,
15
- ]
10
+ PERMITTED_KEY_TYPES = %i[
11
+ number
12
+ integer
13
+ string
14
+ datetime
15
+ serialized
16
+ ].freeze
16
17
 
17
18
  # Initialize the attributes we know the class has, in addition to our magic attributes: id, created_at, and updated_at.
18
19
  included do
@@ -27,7 +28,6 @@ module Dynamoid #:nodoc:
27
28
  end
28
29
 
29
30
  module ClassMethods
30
-
31
31
  # Specify a field for a document.
32
32
  #
33
33
  # Its type determines how it is coerced when read in and out of the datastore.
@@ -50,7 +50,7 @@ module Dynamoid #:nodoc:
50
50
  Dynamoid.logger.warn("Field type :float, which you declared for '#{name}', is deprecated in favor of :number.")
51
51
  type = :number
52
52
  end
53
- self.attributes = attributes.merge(name => {type: type}.merge(options))
53
+ self.attributes = attributes.merge(name => { type: type }.merge(options))
54
54
 
55
55
  generated_methods.module_eval do
56
56
  define_method(named) { read_attribute(named) }
@@ -63,7 +63,7 @@ module Dynamoid #:nodoc:
63
63
  !value.nil?
64
64
  end
65
65
  end
66
- define_method("#{named}=") {|value| write_attribute(named, value) }
66
+ define_method("#{named}=") { |value| write_attribute(named, value) }
67
67
  end
68
68
  end
69
69
 
@@ -72,9 +72,9 @@ module Dynamoid #:nodoc:
72
72
  self.range_key = name
73
73
  end
74
74
 
75
- def table(options)
75
+ def table(_options)
76
76
  # a default 'id' column is created when Dynamoid::Document is included
77
- unless(attributes.has_key? hash_key)
77
+ unless attributes.key? hash_key
78
78
  remove_field :id
79
79
  field(hash_key)
80
80
  end
@@ -82,7 +82,7 @@ module Dynamoid #:nodoc:
82
82
 
83
83
  def remove_field(field)
84
84
  field = field.to_sym
85
- attributes.delete(field) or raise 'No such field'
85
+ attributes.delete(field) || raise('No such field')
86
86
 
87
87
  generated_methods.module_eval do
88
88
  remove_method field
@@ -104,7 +104,7 @@ module Dynamoid #:nodoc:
104
104
 
105
105
  # You can access the attributes of an object directly on its attributes method, which is by default an empty hash.
106
106
  attr_accessor :attributes
107
- alias :raw_attributes :attributes
107
+ alias raw_attributes attributes
108
108
 
109
109
  # Write an attribute on the object. Also marks the previous value as dirty.
110
110
  #
@@ -113,13 +113,16 @@ module Dynamoid #:nodoc:
113
113
  #
114
114
  # @since 0.2.0
115
115
  def write_attribute(name, value)
116
+ name = name.to_sym
117
+
116
118
  if association = @associations[name]
117
119
  association.reset
118
120
  end
119
121
 
120
- attributes[name.to_sym] = value
122
+ value_casted = TypeCasting.cast_field(value, self.class.attributes[name])
123
+ attributes[name] = value_casted
121
124
  end
122
- alias :[]= :write_attribute
125
+ alias []= write_attribute
123
126
 
124
127
  # Read an attribute from an object.
125
128
  #
@@ -129,7 +132,7 @@ module Dynamoid #:nodoc:
129
132
  def read_attribute(name)
130
133
  attributes[name.to_sym]
131
134
  end
132
- alias :[] :read_attribute
135
+ alias [] read_attribute
133
136
 
134
137
  # Updates multiple attibutes at once, saving the object once the updates are complete.
135
138
  #
@@ -137,7 +140,7 @@ module Dynamoid #:nodoc:
137
140
  #
138
141
  # @since 0.2.0
139
142
  def update_attributes(attributes)
140
- attributes.each {|attribute, value| self.write_attribute(attribute, value)} unless attributes.nil? || attributes.empty?
143
+ attributes.each { |attribute, value| write_attribute(attribute, value) } unless attributes.nil? || attributes.empty?
141
144
  save
142
145
  end
143
146
 
@@ -165,15 +168,13 @@ module Dynamoid #:nodoc:
165
168
  #
166
169
  # @since 0.2.0
167
170
  def set_updated_at
168
- if Dynamoid::Config.timestamps && !self.updated_at_changed?
171
+ if Dynamoid::Config.timestamps && !updated_at_changed?
169
172
  self.updated_at = DateTime.now.in_time_zone(Time.zone)
170
173
  end
171
174
  end
172
175
 
173
176
  def set_type
174
- self.type ||= self.class.to_s if self.class.attributes[:type]
177
+ self.type ||= self.class.name if self.class.attributes[:type]
175
178
  end
176
-
177
179
  end
178
-
179
180
  end
@@ -1,6 +1,6 @@
1
- # encoding: utf-8
2
- module Dynamoid
1
+ # frozen_string_literal: true
3
2
 
3
+ module Dynamoid
4
4
  # This module defines the finder methods that hang off the document at the
5
5
  # class level, like find, find_by_id, and the method_missing style finders.
6
6
  module Finders
@@ -14,41 +14,35 @@ module Dynamoid
14
14
  'begins_with' => :range_begins_with,
15
15
  'between' => :range_between,
16
16
  'eq' => :range_eq
17
- }
17
+ }.freeze
18
18
 
19
19
  module ClassMethods
20
-
21
20
  # Find one or many objects, specified by one id or an array of ids.
22
21
  #
23
22
  # @param [Array/String] *id an array of ids or one single id
23
+ # @param [Hash] options
24
24
  #
25
25
  # @return [Dynamoid::Document] one object or an array of objects, depending on whether the input was an array or not
26
26
  #
27
+ # @example Find by partition key
28
+ # Document.find(101)
29
+ #
30
+ # @example Find by partition key and sort key
31
+ # Document.find(101, range_key: 'archived')
32
+ #
33
+ # @example Find several documents by partition key
34
+ # Document.find(101, 102, 103)
35
+ # Document.find([101, 102, 103])
36
+ #
37
+ # @example Find several documents by partition key and sort key
38
+ # Document.find([[101, 'archived'], [102, 'new'], [103, 'deleted']])
39
+ #
27
40
  # @since 0.2.0
28
- def find(*ids)
29
- options = if ids.last.is_a? Hash
30
- ids.slice!(-1)
31
- else
32
- {}
33
- end
34
- expects_array = ids.first.kind_of?(Array)
35
-
36
- ids = Array(ids.flatten.uniq)
37
- if ids.count == 1
38
- result = self.find_by_id(ids.first, options)
39
- if result.nil?
40
- message = "Couldn't find #{self.name} with '#{self.hash_key}'=#{ids[0]}"
41
- raise Errors::RecordNotFound.new(message)
42
- end
43
- expects_array ? Array(result) : result
41
+ def find(*ids, **options)
42
+ if ids.size == 1 && !ids[0].is_a?(Array)
43
+ _find_by_id(ids[0], options.merge(raise_error: true))
44
44
  else
45
- result = find_all(ids)
46
- if result.size != ids.size
47
- message = "Couldn't find all #{self.name.pluralize} with '#{self.hash_key}': (#{ids.join(', ')}) "
48
- message << "(found #{result.size} results, but was looking for #{ids.size})"
49
- raise Errors::RecordNotFound.new(message)
50
- end
51
- result
45
+ _find_all(ids.flatten(1), raise_error: true)
52
46
  end
53
47
  end
54
48
 
@@ -67,26 +61,9 @@ module Dynamoid
67
61
  # find all the tweets using hash key and range key with consistent read
68
62
  # Tweet.find_all([['1', 'red'], ['1', 'green']], :consistent_read => true)
69
63
  def find_all(ids, options = {})
70
- results = unless Dynamoid.config.backoff
71
- items = Dynamoid.adapter.read(self.table_name, ids, options)
72
- items ? items[self.table_name] : []
73
- else
74
- items = []
75
- backoff = nil
76
- Dynamoid.adapter.read(self.table_name, ids, options) do |hash, has_unprocessed_items|
77
- items += hash[self.table_name]
78
-
79
- if has_unprocessed_items
80
- backoff ||= Dynamoid.config.build_backoff
81
- backoff.call
82
- else
83
- backoff = nil
84
- end
85
- end
86
- items
87
- end
64
+ ActiveSupport::Deprecation.warn('[Dynamoid] .find_all is deprecated! Call .find instead of')
88
65
 
89
- results ? results.map {|i| from_database(i) } : []
66
+ _find_all(ids, options)
90
67
  end
91
68
 
92
69
  # Find one object directly by id.
@@ -95,12 +72,71 @@ module Dynamoid
95
72
  #
96
73
  # @return [Dynamoid::Document] the found object, or nil if nothing was found
97
74
  #
75
+ # @example Find by partition key
76
+ # Document.find_by_id(101)
77
+ #
78
+ # @example Find by partition key and sort key
79
+ # Document.find_by_id(101, range_key: 'archived')
80
+ #
98
81
  # @since 0.2.0
99
82
  def find_by_id(id, options = {})
100
- if item = Dynamoid.adapter.read(self.table_name, id, options)
101
- from_database(item)
83
+ ActiveSupport::Deprecation.warn('[Dynamoid] .find_by_id is deprecated! Call .find instead of', caller[1..-1])
84
+
85
+ _find_by_id(id, options)
86
+ end
87
+
88
+ def _find_all(ids, options = {})
89
+ if range_key
90
+ ids = ids.map do |pk, sk|
91
+ sk_casted = TypeCasting.cast_field(sk, attributes[range_key])
92
+ sk_dumped = Dumping.dump_field(sk_casted, attributes[range_key])
93
+
94
+ [pk, sk_dumped]
95
+ end
96
+ end
97
+
98
+ items = if Dynamoid.config.backoff
99
+ items = []
100
+ backoff = nil
101
+ Dynamoid.adapter.read(table_name, ids, options) do |hash, has_unprocessed_items|
102
+ items += hash[table_name]
103
+
104
+ if has_unprocessed_items
105
+ backoff ||= Dynamoid.config.build_backoff
106
+ backoff.call
107
+ else
108
+ backoff = nil
109
+ end
110
+ end
111
+ items
112
+ else
113
+ items = Dynamoid.adapter.read(table_name, ids, options)
114
+ items ? items[table_name] : []
115
+ end
116
+
117
+ if items.size == ids.size || !options[:raise_error]
118
+ items ? items.map { |i| from_database(i) } : []
102
119
  else
103
- nil
120
+ message = "Couldn't find all #{name.pluralize} with '#{hash_key}': (#{ids.join(', ')}) "
121
+ message += "(found #{items.size} results, but was looking for #{ids.size})"
122
+ raise Errors::RecordNotFound, message
123
+ end
124
+ end
125
+
126
+ def _find_by_id(id, options = {})
127
+ if range_key
128
+ key = options[:range_key]
129
+ key_casted = TypeCasting.cast_field(key, attributes[range_key])
130
+ key_dumped = Dumping.dump_field(key_casted, attributes[range_key])
131
+
132
+ options[:range_key] = key_dumped
133
+ end
134
+
135
+ if item = Dynamoid.adapter.read(table_name, id, options)
136
+ from_database(item)
137
+ elsif options[:raise_error]
138
+ message = "Couldn't find #{name} with '#{hash_key}'=#{id}"
139
+ raise Errors::RecordNotFound, message
104
140
  end
105
141
  end
106
142
 
@@ -110,7 +146,9 @@ module Dynamoid
110
146
  # @param [String/Number] range_key of the object to find
111
147
  #
112
148
  def find_by_composite_key(hash_key, range_key, options = {})
113
- find_by_id(hash_key, options.merge(range_key: range_key))
149
+ ActiveSupport::Deprecation.warn('[Dynamoid] .find_by_composite_key is deprecated! Call .find instead of')
150
+
151
+ _find_by_id(hash_key, options.merge(range_key: range_key))
114
152
  end
115
153
 
116
154
  # Find all objects by hash and range keys.
@@ -133,9 +171,10 @@ module Dynamoid
133
171
  # @option options [Number] :range_lte find range keys less than or equal to this
134
172
  #
135
173
  # @return [Array] an array of all matching items
136
- #
137
174
  def find_all_by_composite_key(hash_key, options = {})
138
- Dynamoid.adapter.query(self.table_name, options.merge(hash_value: hash_key)).collect do |item|
175
+ ActiveSupport::Deprecation.warn('[Dynamoid] .find_all_composite_key is deprecated! Call .where instead of')
176
+
177
+ Dynamoid.adapter.query(table_name, options.merge(hash_value: hash_key)).collect do |item|
139
178
  from_database(item)
140
179
  end
141
180
  end
@@ -161,6 +200,8 @@ module Dynamoid
161
200
  # @param [Hash] options - query filter, projected keys, scan_index_forward etc
162
201
  # @return [Array] an array of all matching items
163
202
  def find_all_by_secondary_index(hash, options = {})
203
+ ActiveSupport::Deprecation.warn('[Dynamoid] .find_all_by_secondary_index is deprecated! Call .where instead of')
204
+
164
205
  range = options[:range] || {}
165
206
  hash_key_field, hash_key_value = hash.first
166
207
  range_key_field, range_key_value = range.first
@@ -176,21 +217,21 @@ module Dynamoid
176
217
  end
177
218
 
178
219
  # Find the index
179
- index = self.find_index(hash_key_field, range_key_field)
180
- raise Dynamoid::Errors::MissingIndex.new("attempted to find #{[hash_key_field, range_key_field]}") if index.nil?
220
+ index = find_index(hash_key_field, range_key_field)
221
+ raise Dynamoid::Errors::MissingIndex, "attempted to find #{[hash_key_field, range_key_field]}" if index.nil?
181
222
 
182
223
  # query
183
224
  opts = {
184
225
  hash_key: hash_key_field.to_s,
185
226
  hash_value: hash_key_value,
186
- index_name: index.name,
227
+ index_name: index.name
187
228
  }
188
229
  if range_key_field
189
230
  opts[:range_key] = range_key_field
190
231
  opts[range_op_mapped] = range_key_value
191
232
  end
192
- dynamo_options = opts.merge(options.reject {|key, _| key == :range })
193
- Dynamoid.adapter.query(self.table_name, dynamo_options).map do |item|
233
+ dynamo_options = opts.merge(options.reject { |key, _| key == :range })
234
+ Dynamoid.adapter.query(table_name, dynamo_options).map do |item|
194
235
  from_database(item)
195
236
  end
196
237
  end
@@ -208,11 +249,13 @@ module Dynamoid
208
249
  # @since 0.2.0
209
250
  def method_missing(method, *args)
210
251
  if method =~ /find/
252
+ ActiveSupport::Deprecation.warn("[Dynamoid] .#{method} is deprecated! Call .where instead of")
253
+
211
254
  finder = method.to_s.split('_by_').first
212
255
  attributes = method.to_s.split('_by_').last.split('_and_')
213
256
 
214
257
  chain = Dynamoid::Criteria::Chain.new(self)
215
- chain.query = Hash.new.tap {|h| attributes.each_with_index {|attr, index| h[attr.to_sym] = args[index]}}
258
+ chain.query = {}.tap { |h| attributes.each_with_index { |attr, index| h[attr.to_sym] = args[index] } }
216
259
 
217
260
  if finder =~ /all/
218
261
  return chain.all
@@ -225,5 +268,4 @@ module Dynamoid
225
268
  end
226
269
  end
227
270
  end
228
-
229
271
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Dynamoid
2
4
  module IdentityMap
3
5
  extend ActiveSupport::Concern
@@ -36,18 +38,12 @@ module Dynamoid
36
38
  key += "::#{range_key}"
37
39
  end
38
40
 
39
- if identity_map[key]
40
- identity_map[key]
41
- else
42
- super
43
- end
41
+ identity_map[key] || super
44
42
  end
45
43
 
46
44
  def identity_map_key(attrs)
47
45
  key = attrs[hash_key].to_s
48
- if range_key
49
- key += "::#{attrs[range_key]}"
50
- end
46
+ key += "::#{attrs[range_key]}" if range_key
51
47
  key
52
48
  end
53
49
 
@@ -82,9 +78,7 @@ module Dynamoid
82
78
 
83
79
  def identity_map_key
84
80
  key = hash_key.to_s
85
- if self.class.range_key
86
- key += "::#{range_value}"
87
- end
81
+ key += "::#{range_value}" if self.class.range_key
88
82
  key
89
83
  end
90
84
  end