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
@@ -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