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