dynamoid 2.2.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +53 -0
- data/.rubocop_todo.yml +55 -0
- data/.travis.yml +5 -27
- data/Appraisals +17 -15
- data/CHANGELOG.md +26 -3
- data/Gemfile +4 -2
- data/README.md +95 -77
- data/Rakefile +17 -17
- data/Vagrantfile +5 -3
- data/dynamoid.gemspec +39 -45
- data/gemfiles/rails_4_2.gemfile +7 -5
- data/gemfiles/rails_5_0.gemfile +6 -4
- data/gemfiles/rails_5_1.gemfile +6 -4
- data/gemfiles/rails_5_2.gemfile +6 -4
- data/lib/dynamoid.rb +11 -4
- data/lib/dynamoid/adapter.rb +21 -27
- data/lib/dynamoid/adapter_plugin/{aws_sdk_v2.rb → aws_sdk_v3.rb} +118 -113
- data/lib/dynamoid/application_time_zone.rb +27 -0
- data/lib/dynamoid/associations.rb +3 -6
- data/lib/dynamoid/associations/association.rb +3 -6
- data/lib/dynamoid/associations/belongs_to.rb +4 -5
- data/lib/dynamoid/associations/has_and_belongs_to_many.rb +2 -3
- data/lib/dynamoid/associations/has_many.rb +2 -3
- data/lib/dynamoid/associations/has_one.rb +2 -3
- data/lib/dynamoid/associations/many_association.rb +8 -9
- data/lib/dynamoid/associations/single_association.rb +3 -3
- data/lib/dynamoid/components.rb +2 -2
- data/lib/dynamoid/config.rb +9 -5
- data/lib/dynamoid/config/backoff_strategies/constant_backoff.rb +4 -2
- data/lib/dynamoid/config/backoff_strategies/exponential_backoff.rb +3 -1
- data/lib/dynamoid/config/options.rb +4 -4
- data/lib/dynamoid/criteria.rb +3 -5
- data/lib/dynamoid/criteria/chain.rb +42 -49
- data/lib/dynamoid/dirty.rb +5 -4
- data/lib/dynamoid/document.rb +142 -36
- data/lib/dynamoid/dumping.rb +167 -0
- data/lib/dynamoid/dynamodb_time_zone.rb +16 -0
- data/lib/dynamoid/errors.rb +7 -6
- data/lib/dynamoid/fields.rb +24 -23
- data/lib/dynamoid/finders.rb +101 -59
- data/lib/dynamoid/identity_map.rb +5 -11
- data/lib/dynamoid/indexes.rb +45 -46
- data/lib/dynamoid/middleware/identity_map.rb +2 -0
- data/lib/dynamoid/persistence.rb +67 -307
- data/lib/dynamoid/primary_key_type_mapping.rb +34 -0
- data/lib/dynamoid/railtie.rb +3 -1
- data/lib/dynamoid/tasks/database.rake +11 -11
- data/lib/dynamoid/tasks/database.rb +4 -3
- data/lib/dynamoid/type_casting.rb +193 -0
- data/lib/dynamoid/undumping.rb +188 -0
- data/lib/dynamoid/validations.rb +4 -7
- data/lib/dynamoid/version.rb +3 -1
- metadata +59 -53
- data/gemfiles/rails_4_0.gemfile +0 -9
- data/gemfiles/rails_4_1.gemfile +0 -9
data/lib/dynamoid/dirty.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Dynamoid
|
2
4
|
module Dirty
|
3
5
|
extend ActiveSupport::Concern
|
@@ -34,14 +36,14 @@ module Dynamoid
|
|
34
36
|
end
|
35
37
|
|
36
38
|
def write_attribute(name, value)
|
37
|
-
attribute_will_change!(name) unless
|
39
|
+
attribute_will_change!(name) unless read_attribute(name) == value
|
38
40
|
super
|
39
41
|
end
|
40
42
|
|
41
43
|
protected
|
42
44
|
|
43
45
|
def attribute_method?(attr)
|
44
|
-
super || self.class.attributes.
|
46
|
+
super || self.class.attributes.key?(attr.to_sym)
|
45
47
|
end
|
46
48
|
|
47
49
|
if ActiveModel::VERSION::STRING >= '5.2.0'
|
@@ -53,8 +55,7 @@ module Dynamoid
|
|
53
55
|
@mutations_from_database ||= ActiveModel::NullMutationTracker.instance
|
54
56
|
end
|
55
57
|
|
56
|
-
def forget_attribute_assignments
|
57
|
-
end
|
58
|
+
def forget_attribute_assignments; end
|
58
59
|
end
|
59
60
|
|
60
61
|
if ActiveModel::VERSION::STRING < '4.2.0'
|
data/lib/dynamoid/document.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
#
|
2
|
-
module Dynamoid #:nodoc:
|
1
|
+
# frozen_string_literal: true
|
3
2
|
|
3
|
+
module Dynamoid #:nodoc:
|
4
4
|
# This is the base module for all domain objects that need to be persisted to
|
5
5
|
# the database as documents.
|
6
6
|
module Document
|
@@ -114,35 +114,66 @@ module Dynamoid #:nodoc:
|
|
114
114
|
# @since 0.2.0
|
115
115
|
def exists?(id_or_conditions = {})
|
116
116
|
case id_or_conditions
|
117
|
-
|
118
|
-
else !! find_by_id(id_or_conditions)
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
def update(hash_key, range_key_value=nil, attrs)
|
123
|
-
if range_key.present?
|
124
|
-
range_key_value = dump_field(range_key_value, attributes[self.range_key])
|
117
|
+
when Hash then where(id_or_conditions).first.present?
|
125
118
|
else
|
126
|
-
|
119
|
+
begin
|
120
|
+
find(id_or_conditions)
|
121
|
+
true
|
122
|
+
rescue Dynamoid::Errors::RecordNotFound
|
123
|
+
false
|
124
|
+
end
|
127
125
|
end
|
126
|
+
end
|
128
127
|
|
128
|
+
# Update document with provided values.
|
129
|
+
# Instantiates document and saves changes. Runs validations and callbacks.
|
130
|
+
#
|
131
|
+
# @param [Scalar value] partition key
|
132
|
+
# @param [Scalar value] sort key, optional
|
133
|
+
# @param [Hash] attributes
|
134
|
+
#
|
135
|
+
# @return [Dynamoid::Doument] updated document
|
136
|
+
#
|
137
|
+
# @example Update document
|
138
|
+
# Post.update(101, read: true)
|
139
|
+
def update(hash_key, range_key_value = nil, attrs)
|
129
140
|
model = find(hash_key, range_key: range_key_value, consistent_read: true)
|
130
141
|
model.update_attributes(attrs)
|
131
142
|
model
|
132
143
|
end
|
133
144
|
|
134
|
-
|
145
|
+
# Update document.
|
146
|
+
# Uses efficient low-level `UpdateItem` API call.
|
147
|
+
# Changes attibutes and loads new document version with one API call.
|
148
|
+
# Doesn't run validations and callbacks. Can make conditional update.
|
149
|
+
# If a document doesn't exist or specified conditions failed - returns `nil`
|
150
|
+
#
|
151
|
+
# @param [Scalar value] partition key
|
152
|
+
# @param [Scalar value] sort key (optional)
|
153
|
+
# @param [Hash] attributes
|
154
|
+
# @param [Hash] conditions
|
155
|
+
#
|
156
|
+
# @return [Dynamoid::Document/nil] updated document
|
157
|
+
#
|
158
|
+
# @example Update document
|
159
|
+
# Post.update_fields(101, read: true)
|
160
|
+
#
|
161
|
+
# @example Update document with condition
|
162
|
+
# Post.update_fields(101, { read: true }, if: { version: 1 })
|
163
|
+
def update_fields(hash_key_value, range_key_value = nil, attrs = {}, conditions = {})
|
135
164
|
optional_params = [range_key_value, attrs, conditions].compact
|
136
165
|
if optional_params.first.is_a?(Hash)
|
137
166
|
range_key_value = nil
|
138
|
-
attrs, conditions = optional_params[0
|
167
|
+
attrs, conditions = optional_params[0..1]
|
139
168
|
else
|
140
169
|
range_key_value = optional_params.first
|
141
|
-
attrs, conditions = optional_params[1
|
170
|
+
attrs, conditions = optional_params[1..2]
|
142
171
|
end
|
143
172
|
|
144
173
|
options = if range_key
|
145
|
-
|
174
|
+
value_casted = TypeCasting.cast_field(range_key_value, attributes[range_key])
|
175
|
+
value_dumped = Dumping.dump_field(value_casted, attributes[range_key])
|
176
|
+
{ range_key: value_dumped }
|
146
177
|
else
|
147
178
|
{}
|
148
179
|
end
|
@@ -150,42 +181,81 @@ module Dynamoid #:nodoc:
|
|
150
181
|
(conditions[:if_exists] ||= {})[hash_key] = hash_key_value
|
151
182
|
options[:conditions] = conditions
|
152
183
|
|
184
|
+
attrs = attrs.symbolize_keys
|
185
|
+
if Dynamoid::Config.timestamps
|
186
|
+
attrs[:updated_at] ||= DateTime.now.in_time_zone(Time.zone)
|
187
|
+
end
|
188
|
+
|
153
189
|
begin
|
154
190
|
new_attrs = Dynamoid.adapter.update_item(table_name, hash_key_value, options) do |t|
|
155
|
-
attrs.
|
156
|
-
|
191
|
+
attrs.each do |k, v|
|
192
|
+
value_casted = TypeCasting.cast_field(v, attributes[k])
|
193
|
+
value_dumped = Dumping.dump_field(value_casted, attributes[k])
|
194
|
+
t.set(k => value_dumped)
|
157
195
|
end
|
158
196
|
end
|
159
|
-
|
197
|
+
attrs_undumped = Undumping.undump_attributes(new_attrs, attributes)
|
198
|
+
new(attrs_undumped)
|
160
199
|
rescue Dynamoid::Errors::ConditionalCheckFailedException
|
161
200
|
end
|
162
201
|
end
|
163
202
|
|
164
|
-
|
203
|
+
|
204
|
+
# Update existing document or create new one.
|
205
|
+
# Similar to `.update_fields`. The only diffirence is creating new document.
|
206
|
+
#
|
207
|
+
# Uses efficient low-level `UpdateItem` API call.
|
208
|
+
# Changes attibutes and loads new document version with one API call.
|
209
|
+
# Doesn't run validations and callbacks. Can make conditional update.
|
210
|
+
# If specified conditions failed - returns `nil`
|
211
|
+
#
|
212
|
+
# @param [Scalar value] partition key
|
213
|
+
# @param [Scalar value] sort key (optional)
|
214
|
+
# @param [Hash] attributes
|
215
|
+
# @param [Hash] conditions
|
216
|
+
#
|
217
|
+
# @return [Dynamoid::Document/nil] updated document
|
218
|
+
#
|
219
|
+
# @example Update document
|
220
|
+
# Post.update(101, read: true)
|
221
|
+
#
|
222
|
+
# @example Update document
|
223
|
+
# Post.upsert(101, read: true)
|
224
|
+
def upsert(hash_key_value, range_key_value = nil, attrs = {}, conditions = {})
|
165
225
|
optional_params = [range_key_value, attrs, conditions].compact
|
166
226
|
if optional_params.first.is_a?(Hash)
|
167
227
|
range_key_value = nil
|
168
|
-
attrs, conditions = optional_params[0
|
228
|
+
attrs, conditions = optional_params[0..1]
|
169
229
|
else
|
170
230
|
range_key_value = optional_params.first
|
171
|
-
attrs, conditions = optional_params[1
|
231
|
+
attrs, conditions = optional_params[1..2]
|
172
232
|
end
|
173
233
|
|
174
234
|
options = if range_key
|
175
|
-
|
235
|
+
value_casted = TypeCasting.cast_field(range_key_value, attributes[range_key])
|
236
|
+
value_dumped = Dumping.dump_field(value_casted, attributes[range_key])
|
237
|
+
{ range_key: value_dumped }
|
176
238
|
else
|
177
239
|
{}
|
178
240
|
end
|
179
241
|
|
180
242
|
options[:conditions] = conditions
|
181
243
|
|
244
|
+
attrs = attrs.symbolize_keys
|
245
|
+
if Dynamoid::Config.timestamps
|
246
|
+
attrs[:updated_at] ||= DateTime.now.in_time_zone(Time.zone)
|
247
|
+
end
|
248
|
+
|
182
249
|
begin
|
183
250
|
new_attrs = Dynamoid.adapter.update_item(table_name, hash_key_value, options) do |t|
|
184
|
-
attrs.
|
185
|
-
|
251
|
+
attrs.each do |k, v|
|
252
|
+
value_casted = TypeCasting.cast_field(v, attributes[k])
|
253
|
+
value_dumped = Dumping.dump_field(value_casted, attributes[k])
|
254
|
+
t.set(k => value_dumped)
|
186
255
|
end
|
187
256
|
end
|
188
|
-
|
257
|
+
attrs_undumped = Undumping.undump_attributes(new_attrs, attributes)
|
258
|
+
new(attrs_undumped)
|
189
259
|
rescue Dynamoid::Errors::ConditionalCheckFailedException
|
190
260
|
end
|
191
261
|
end
|
@@ -212,13 +282,30 @@ module Dynamoid #:nodoc:
|
|
212
282
|
@attributes ||= {}
|
213
283
|
@associations ||= {}
|
214
284
|
|
215
|
-
|
285
|
+
self.class.attributes.each do |_, options|
|
286
|
+
if options[:type].is_a?(Class) && options[:default]
|
287
|
+
raise 'Dynamoid class-type fields do not support default values'
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
attrs_with_defaults = {}
|
292
|
+
self.class.attributes.each do |attribute, options|
|
293
|
+
attrs_with_defaults[attribute] = if attrs.key?(attribute)
|
294
|
+
attrs[attribute]
|
295
|
+
elsif options.key?(:default)
|
296
|
+
evaluate_default_value(options[:default])
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
attrs_virtual = attrs.slice(*(attrs.keys - self.class.attributes.keys))
|
301
|
+
|
302
|
+
load(attrs_with_defaults.merge(attrs_virtual))
|
216
303
|
end
|
217
304
|
end
|
218
305
|
|
219
306
|
def load(attrs)
|
220
|
-
|
221
|
-
send("#{key}=", value) if
|
307
|
+
attrs.each do |key, value|
|
308
|
+
send("#{key}=", value) if respond_to?("#{key}=")
|
222
309
|
end
|
223
310
|
end
|
224
311
|
|
@@ -230,7 +317,7 @@ module Dynamoid #:nodoc:
|
|
230
317
|
super
|
231
318
|
else
|
232
319
|
return false if other.nil?
|
233
|
-
other.is_a?(Dynamoid::Document) &&
|
320
|
+
other.is_a?(Dynamoid::Document) && hash_key == other.hash_key && range_value == other.range_value
|
234
321
|
end
|
235
322
|
end
|
236
323
|
|
@@ -249,8 +336,13 @@ module Dynamoid #:nodoc:
|
|
249
336
|
#
|
250
337
|
# @since 0.2.0
|
251
338
|
def reload
|
252
|
-
|
253
|
-
|
339
|
+
options = { consistent_read: true }
|
340
|
+
|
341
|
+
if self.class.range_key
|
342
|
+
options[:range_key] = range_value
|
343
|
+
end
|
344
|
+
|
345
|
+
self.attributes = self.class.find(hash_key, options).attributes
|
254
346
|
@associations.values.each(&:reset)
|
255
347
|
self
|
256
348
|
end
|
@@ -259,30 +351,44 @@ module Dynamoid #:nodoc:
|
|
259
351
|
#
|
260
352
|
# @since 0.4.0
|
261
353
|
def hash_key
|
262
|
-
|
354
|
+
send(self.class.hash_key)
|
263
355
|
end
|
264
356
|
|
265
357
|
# Assign an object's hash key, regardless of what it might be called to the object.
|
266
358
|
#
|
267
359
|
# @since 0.4.0
|
268
360
|
def hash_key=(value)
|
269
|
-
|
361
|
+
send("#{self.class.hash_key}=", value)
|
270
362
|
end
|
271
363
|
|
272
364
|
def range_value
|
273
365
|
if range_key = self.class.range_key
|
274
|
-
|
366
|
+
send(range_key)
|
275
367
|
end
|
276
368
|
end
|
277
369
|
|
278
370
|
def range_value=(value)
|
279
|
-
|
371
|
+
send("#{self.class.range_key}=", value)
|
280
372
|
end
|
281
373
|
|
282
374
|
private
|
283
375
|
|
284
376
|
def dumped_range_value
|
285
|
-
dump_field(range_value, self.class.attributes[self.class.range_key])
|
377
|
+
Dumping.dump_field(range_value, self.class.attributes[self.class.range_key])
|
378
|
+
end
|
379
|
+
|
380
|
+
# Evaluates the default value given, this is used by undump
|
381
|
+
# when determining the value of the default given for a field options.
|
382
|
+
#
|
383
|
+
# @param [Object] :value the attribute's default value
|
384
|
+
def evaluate_default_value(val)
|
385
|
+
if val.respond_to?(:call)
|
386
|
+
val.call
|
387
|
+
elsif val.duplicable?
|
388
|
+
val.dup
|
389
|
+
else
|
390
|
+
val
|
391
|
+
end
|
286
392
|
end
|
287
393
|
end
|
288
394
|
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dynamoid
|
4
|
+
module Dumping
|
5
|
+
def self.dump_attributes(attributes, attributes_options)
|
6
|
+
{}.tap do |h|
|
7
|
+
attributes.each do |attribute, value|
|
8
|
+
h[attribute] = dump_field(value, attributes_options[attribute])
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.dump_field(value, options)
|
14
|
+
dumper = field_dumper(options)
|
15
|
+
|
16
|
+
if dumper.nil?
|
17
|
+
raise ArgumentError, "Unknown type #{options[:type]}"
|
18
|
+
end
|
19
|
+
|
20
|
+
dumper.process(value)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.field_dumper(options)
|
24
|
+
dumper_class = case options[:type]
|
25
|
+
when :string then StringDumper
|
26
|
+
when :integer then IntegerDumper
|
27
|
+
when :number then NumberDumper
|
28
|
+
when :set then SetDumper
|
29
|
+
when :array then ArrayDumper
|
30
|
+
when :datetime then DateTimeDumper
|
31
|
+
when :date then DateDumper
|
32
|
+
when :serialized then SerializedDumper
|
33
|
+
when :raw then RawDumper
|
34
|
+
when :boolean then BooleanDumper
|
35
|
+
when Class then CustomTypeDumper
|
36
|
+
end
|
37
|
+
|
38
|
+
if dumper_class.present?
|
39
|
+
dumper_class.new(options)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class Base
|
44
|
+
def initialize(options)
|
45
|
+
@options = options
|
46
|
+
end
|
47
|
+
|
48
|
+
def process(value)
|
49
|
+
value
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# string -> string
|
54
|
+
class StringDumper < Base
|
55
|
+
end
|
56
|
+
|
57
|
+
# integer -> number
|
58
|
+
class IntegerDumper < Base
|
59
|
+
end
|
60
|
+
|
61
|
+
# number -> number
|
62
|
+
class NumberDumper < Base
|
63
|
+
end
|
64
|
+
|
65
|
+
# set -> set
|
66
|
+
class SetDumper < Base
|
67
|
+
end
|
68
|
+
|
69
|
+
# array -> array
|
70
|
+
class ArrayDumper < Base
|
71
|
+
end
|
72
|
+
|
73
|
+
# datetime -> integer/string
|
74
|
+
class DateTimeDumper < Base
|
75
|
+
def process(value)
|
76
|
+
!value.nil? ? format_datetime(value, @options) : nil
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def format_datetime(value, options)
|
82
|
+
use_string_format = if options[:store_as_string].nil?
|
83
|
+
Dynamoid.config.store_datetime_as_string
|
84
|
+
else
|
85
|
+
options[:store_as_string]
|
86
|
+
end
|
87
|
+
|
88
|
+
if use_string_format
|
89
|
+
value_in_time_zone = Dynamoid::DynamodbTimeZone.in_time_zone(value)
|
90
|
+
value_in_time_zone.iso8601
|
91
|
+
else
|
92
|
+
unless value.respond_to?(:to_i) && value.respond_to?(:nsec)
|
93
|
+
value = value.to_time
|
94
|
+
end
|
95
|
+
BigDecimal(format('%d.%09d', value.to_i, value.nsec))
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# date -> integer/string
|
101
|
+
class DateDumper < Base
|
102
|
+
def process(value)
|
103
|
+
!value.nil? ? format_date(value, @options) : nil
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def format_date(value, options)
|
109
|
+
use_string_format = if options[:store_as_string].nil?
|
110
|
+
Dynamoid.config.store_date_as_string
|
111
|
+
else
|
112
|
+
options[:store_as_string]
|
113
|
+
end
|
114
|
+
|
115
|
+
if use_string_format
|
116
|
+
value.to_date.iso8601
|
117
|
+
else
|
118
|
+
(value.to_date - Dynamoid::Persistence::UNIX_EPOCH_DATE).to_i
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# any standard Ruby object -> self
|
124
|
+
class RawDumper < Base
|
125
|
+
end
|
126
|
+
|
127
|
+
# object -> string
|
128
|
+
class SerializedDumper < Base
|
129
|
+
def process(value)
|
130
|
+
@options[:serializer] ? @options[:serializer].dump(value) : value.to_yaml
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# True/False -> True/False/string
|
135
|
+
class BooleanDumper < Base
|
136
|
+
def process(value)
|
137
|
+
unless value.nil?
|
138
|
+
store_as_boolean = if @options[:store_as_native_boolean].nil?
|
139
|
+
Dynamoid.config.store_boolean_as_native
|
140
|
+
else
|
141
|
+
@options[:store_as_native_boolean]
|
142
|
+
end
|
143
|
+
if store_as_boolean
|
144
|
+
!!value
|
145
|
+
else
|
146
|
+
value.to_s[0] # => "f" or "t"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# any object -> string
|
153
|
+
class CustomTypeDumper < Base
|
154
|
+
def process(value)
|
155
|
+
field_class = @options[:type]
|
156
|
+
|
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} supports serialization for Dynamoid."
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|