dynamoid 3.0.0 → 3.1.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/CHANGELOG.md +34 -0
- data/README.md +99 -8
- data/dynamoid.gemspec +1 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +80 -172
- data/lib/dynamoid/adapter_plugin/query.rb +144 -0
- data/lib/dynamoid/adapter_plugin/scan.rb +107 -0
- data/lib/dynamoid/components.rb +1 -1
- data/lib/dynamoid/config.rb +6 -1
- data/lib/dynamoid/criteria/chain.rb +19 -2
- data/lib/dynamoid/document.rb +32 -1
- data/lib/dynamoid/dumping.rb +128 -2
- data/lib/dynamoid/fields.rb +20 -18
- data/lib/dynamoid/finders.rb +15 -6
- data/lib/dynamoid/persistence.rb +61 -1
- data/lib/dynamoid/tasks/database.rake +1 -1
- data/lib/dynamoid/tasks/database.rb +1 -1
- data/lib/dynamoid/type_casting.rb +80 -0
- data/lib/dynamoid/undumping.rb +88 -10
- data/lib/dynamoid/version.rb +1 -1
- metadata +18 -2
data/lib/dynamoid/fields.rb
CHANGED
@@ -64,6 +64,7 @@ module Dynamoid #:nodoc:
|
|
64
64
|
end
|
65
65
|
end
|
66
66
|
define_method("#{named}=") { |value| write_attribute(named, value) }
|
67
|
+
define_method("#{named}_before_type_cast") { read_attribute_before_type_cast(named) }
|
67
68
|
end
|
68
69
|
end
|
69
70
|
|
@@ -88,6 +89,7 @@ module Dynamoid #:nodoc:
|
|
88
89
|
remove_method field
|
89
90
|
remove_method :"#{field}="
|
90
91
|
remove_method :"#{field}?"
|
92
|
+
remove_method:"#{field}_before_type_cast"
|
91
93
|
end
|
92
94
|
end
|
93
95
|
|
@@ -119,6 +121,8 @@ module Dynamoid #:nodoc:
|
|
119
121
|
association.reset
|
120
122
|
end
|
121
123
|
|
124
|
+
@attributes_before_type_cast[name] = value
|
125
|
+
|
122
126
|
value_casted = TypeCasting.cast_field(value, self.class.attributes[name])
|
123
127
|
attributes[name] = value_casted
|
124
128
|
end
|
@@ -134,25 +138,17 @@ module Dynamoid #:nodoc:
|
|
134
138
|
end
|
135
139
|
alias [] read_attribute
|
136
140
|
|
137
|
-
#
|
138
|
-
|
139
|
-
|
140
|
-
#
|
141
|
-
# @since 0.2.0
|
142
|
-
def update_attributes(attributes)
|
143
|
-
attributes.each { |attribute, value| write_attribute(attribute, value) } unless attributes.nil? || attributes.empty?
|
144
|
-
save
|
141
|
+
# Returns a hash of attributes before typecasting
|
142
|
+
def attributes_before_type_cast
|
143
|
+
@attributes_before_type_cast
|
145
144
|
end
|
146
145
|
|
147
|
-
#
|
146
|
+
# Returns the value of the attribute identified by name before typecasting
|
148
147
|
#
|
149
|
-
# @param [Symbol] attribute
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
def update_attribute(attribute, value)
|
154
|
-
write_attribute(attribute, value)
|
155
|
-
save
|
148
|
+
# @param [Symbol] attribute name
|
149
|
+
def read_attribute_before_type_cast(name)
|
150
|
+
return nil unless name.respond_to?(:to_sym)
|
151
|
+
@attributes_before_type_cast[name.to_sym]
|
156
152
|
end
|
157
153
|
|
158
154
|
private
|
@@ -173,8 +169,14 @@ module Dynamoid #:nodoc:
|
|
173
169
|
end
|
174
170
|
end
|
175
171
|
|
176
|
-
def
|
177
|
-
|
172
|
+
def set_inheritance_field
|
173
|
+
# actually it does only following logic:
|
174
|
+
# self.type ||= self.class.name if self.class.attributes[:type]
|
175
|
+
|
176
|
+
type = self.class.inheritance_field
|
177
|
+
if self.class.attributes[type] && self.send(type).nil?
|
178
|
+
self.send("#{type}=", self.class.name)
|
179
|
+
end
|
178
180
|
end
|
179
181
|
end
|
180
182
|
end
|
data/lib/dynamoid/finders.rb
CHANGED
@@ -37,12 +37,17 @@ module Dynamoid
|
|
37
37
|
# @example Find several documents by partition key and sort key
|
38
38
|
# Document.find([[101, 'archived'], [102, 'new'], [103, 'deleted']])
|
39
39
|
#
|
40
|
+
# @example Perform strong consistent reads
|
41
|
+
# Document.find(101, consistent_read: true)
|
42
|
+
# Document.find(101, 102, 103, consistent_read: true)
|
43
|
+
# Document.find(101, range_key: 'archived', consistent_read: true)
|
44
|
+
#
|
40
45
|
# @since 0.2.0
|
41
46
|
def find(*ids, **options)
|
42
47
|
if ids.size == 1 && !ids[0].is_a?(Array)
|
43
48
|
_find_by_id(ids[0], options.merge(raise_error: true))
|
44
49
|
else
|
45
|
-
_find_all(ids.flatten(1), raise_error: true)
|
50
|
+
_find_all(ids.flatten(1), options.merge(raise_error: true))
|
46
51
|
end
|
47
52
|
end
|
48
53
|
|
@@ -95,10 +100,12 @@ module Dynamoid
|
|
95
100
|
end
|
96
101
|
end
|
97
102
|
|
103
|
+
read_options = options.slice(:consistent_read)
|
104
|
+
|
98
105
|
items = if Dynamoid.config.backoff
|
99
106
|
items = []
|
100
107
|
backoff = nil
|
101
|
-
Dynamoid.adapter.read(table_name, ids,
|
108
|
+
Dynamoid.adapter.read(table_name, ids, read_options) do |hash, has_unprocessed_items|
|
102
109
|
items += hash[table_name]
|
103
110
|
|
104
111
|
if has_unprocessed_items
|
@@ -110,14 +117,15 @@ module Dynamoid
|
|
110
117
|
end
|
111
118
|
items
|
112
119
|
else
|
113
|
-
items = Dynamoid.adapter.read(table_name, ids,
|
120
|
+
items = Dynamoid.adapter.read(table_name, ids, read_options)
|
114
121
|
items ? items[table_name] : []
|
115
122
|
end
|
116
123
|
|
117
124
|
if items.size == ids.size || !options[:raise_error]
|
118
125
|
items ? items.map { |i| from_database(i) } : []
|
119
126
|
else
|
120
|
-
|
127
|
+
ids_list = range_key ? ids.map { |pk, sk| "(#{pk},#{sk})" } : ids.map(&:to_s)
|
128
|
+
message = "Couldn't find all #{name.pluralize} with primary keys [#{ids_list.join(', ')}] "
|
121
129
|
message += "(found #{items.size} results, but was looking for #{ids.size})"
|
122
130
|
raise Errors::RecordNotFound, message
|
123
131
|
end
|
@@ -132,10 +140,11 @@ module Dynamoid
|
|
132
140
|
options[:range_key] = key_dumped
|
133
141
|
end
|
134
142
|
|
135
|
-
if item = Dynamoid.adapter.read(table_name, id, options)
|
143
|
+
if item = Dynamoid.adapter.read(table_name, id, options.slice(:range_key, :consistent_read))
|
136
144
|
from_database(item)
|
137
145
|
elsif options[:raise_error]
|
138
|
-
|
146
|
+
primary_key = range_key ? "(#{id},#{options[:range_key]})" : id
|
147
|
+
message = "Couldn't find #{name} with primary key #{primary_key}"
|
139
148
|
raise Errors::RecordNotFound, message
|
140
149
|
end
|
141
150
|
end
|
data/lib/dynamoid/persistence.rb
CHANGED
@@ -59,7 +59,7 @@ module Dynamoid
|
|
59
59
|
end
|
60
60
|
|
61
61
|
def from_database(attrs = {})
|
62
|
-
clazz = attrs
|
62
|
+
clazz = choose_right_class(attrs)
|
63
63
|
attrs_undumped = Undumping.undump_attributes(attrs, clazz.attributes)
|
64
64
|
clazz.new(attrs_undumped).tap { |r| r.new_record = false }
|
65
65
|
end
|
@@ -151,6 +151,36 @@ module Dynamoid
|
|
151
151
|
end
|
152
152
|
end
|
153
153
|
|
154
|
+
# Updates multiple attibutes at once, saving the object once the updates are complete.
|
155
|
+
#
|
156
|
+
# @param [Hash] attributes a hash of attributes to update
|
157
|
+
#
|
158
|
+
# @since 0.2.0
|
159
|
+
def update_attributes(attributes)
|
160
|
+
attributes.each { |attribute, value| write_attribute(attribute, value) } unless attributes.nil? || attributes.empty?
|
161
|
+
save
|
162
|
+
end
|
163
|
+
|
164
|
+
# Updates multiple attibutes at once, saving the object once the updates are complete.
|
165
|
+
# Raises a Dynamoid::Errors::DocumentNotValid exception if there is vaidation and it fails.
|
166
|
+
#
|
167
|
+
# @param [Hash] attributes a hash of attributes to update
|
168
|
+
def update_attributes!(attributes)
|
169
|
+
attributes.each { |attribute, value| write_attribute(attribute, value) } unless attributes.nil? || attributes.empty?
|
170
|
+
save!
|
171
|
+
end
|
172
|
+
|
173
|
+
# Update a single attribute, saving the object afterwards.
|
174
|
+
#
|
175
|
+
# @param [Symbol] attribute the attribute to update
|
176
|
+
# @param [Object] value the value to assign it
|
177
|
+
#
|
178
|
+
# @since 0.2.0
|
179
|
+
def update_attribute(attribute, value)
|
180
|
+
write_attribute(attribute, value)
|
181
|
+
save
|
182
|
+
end
|
183
|
+
|
154
184
|
#
|
155
185
|
# update!() will increment the lock_version if the table has the column, but will not check it. Thus, a concurrent save will
|
156
186
|
# never cause an update! to fail, but an update! may cause a concurrent save to fail.
|
@@ -186,6 +216,36 @@ module Dynamoid
|
|
186
216
|
false
|
187
217
|
end
|
188
218
|
|
219
|
+
# Initializes attribute to zero if nil and adds the value passed as by (default is 1).
|
220
|
+
# Only makes sense for number-based attributes. Returns self.
|
221
|
+
def increment(attribute, by = 1)
|
222
|
+
self[attribute] ||= 0
|
223
|
+
self[attribute] += by
|
224
|
+
self
|
225
|
+
end
|
226
|
+
|
227
|
+
# Wrapper around increment that saves the record.
|
228
|
+
# Returns true if the record could be saved.
|
229
|
+
def increment!(attribute, by = 1)
|
230
|
+
increment(attribute, by)
|
231
|
+
save
|
232
|
+
end
|
233
|
+
|
234
|
+
# Initializes attribute to zero if nil and subtracts the value passed as by (default is 1).
|
235
|
+
# Only makes sense for number-based attributes. Returns self.
|
236
|
+
def decrement(attribute, by = 1)
|
237
|
+
self[attribute] ||= 0
|
238
|
+
self[attribute] -= by
|
239
|
+
self
|
240
|
+
end
|
241
|
+
|
242
|
+
# Wrapper around decrement that saves the record.
|
243
|
+
# Returns true if the record could be saved.
|
244
|
+
def decrement!(attribute, by = 1)
|
245
|
+
decrement(attribute, by)
|
246
|
+
save
|
247
|
+
end
|
248
|
+
|
189
249
|
# Delete this object, but only after running callbacks for it.
|
190
250
|
#
|
191
251
|
# @since 0.2.0
|
@@ -7,7 +7,7 @@ namespace :dynamoid do
|
|
7
7
|
desc 'Creates DynamoDB tables, one for each of your Dynamoid models - does not modify pre-existing tables'
|
8
8
|
task create_tables: :environment do
|
9
9
|
# Load models so Dynamoid will be able to discover tables expected.
|
10
|
-
Dir[File.join(Dynamoid::Config.models_dir, '
|
10
|
+
Dir[File.join(Dynamoid::Config.models_dir, '**/*.rb')].sort.each { |file| require file }
|
11
11
|
if Dynamoid.included_models.any?
|
12
12
|
tables = Dynamoid::Tasks::Database.create_tables
|
13
13
|
result = tables[:created].map { |c| "#{c} created" } + tables[:existing].map { |e| "#{e} already exists" }
|
@@ -106,6 +106,18 @@ module Dynamoid
|
|
106
106
|
|
107
107
|
class SetTypeCaster < Base
|
108
108
|
def process(value)
|
109
|
+
set = type_cast_to_set(value)
|
110
|
+
|
111
|
+
if set.present? && @options[:of].present?
|
112
|
+
process_typed_set(set)
|
113
|
+
else
|
114
|
+
set
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
def type_cast_to_set(value)
|
109
121
|
if value.is_a?(Set)
|
110
122
|
value.dup
|
111
123
|
elsif value.respond_to?(:to_set)
|
@@ -114,10 +126,50 @@ module Dynamoid
|
|
114
126
|
nil
|
115
127
|
end
|
116
128
|
end
|
129
|
+
|
130
|
+
def process_typed_set(set)
|
131
|
+
type_caster = TypeCasting.find_type_caster(element_options)
|
132
|
+
|
133
|
+
if type_caster.nil?
|
134
|
+
raise ArgumentError, "Set element type #{element_type} isn't supported"
|
135
|
+
end
|
136
|
+
|
137
|
+
set.map { |el| type_caster.process(el) }.to_set
|
138
|
+
end
|
139
|
+
|
140
|
+
def element_type
|
141
|
+
unless @options[:of].is_a?(Hash)
|
142
|
+
@options[:of]
|
143
|
+
else
|
144
|
+
@options[:of].keys.first
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def element_options
|
149
|
+
unless @options[:of].is_a?(Hash)
|
150
|
+
{ type: element_type }
|
151
|
+
else
|
152
|
+
@options[:of][element_type].dup.tap do |options|
|
153
|
+
options[:type] = element_type
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
117
157
|
end
|
118
158
|
|
119
159
|
class ArrayTypeCaster < Base
|
120
160
|
def process(value)
|
161
|
+
array = type_cast_to_array(value)
|
162
|
+
|
163
|
+
if array.present? && @options[:of].present?
|
164
|
+
process_typed_array(array)
|
165
|
+
else
|
166
|
+
array
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
private
|
171
|
+
|
172
|
+
def type_cast_to_array(value)
|
121
173
|
if value.is_a?(Array)
|
122
174
|
value.dup
|
123
175
|
elsif value.respond_to?(:to_a)
|
@@ -126,6 +178,34 @@ module Dynamoid
|
|
126
178
|
nil
|
127
179
|
end
|
128
180
|
end
|
181
|
+
|
182
|
+
def process_typed_array(array)
|
183
|
+
type_caster = TypeCasting.find_type_caster(element_options)
|
184
|
+
|
185
|
+
if type_caster.nil?
|
186
|
+
raise ArgumentError, "Set element type #{element_type} isn't supported"
|
187
|
+
end
|
188
|
+
|
189
|
+
array.map { |el| type_caster.process(el) }
|
190
|
+
end
|
191
|
+
|
192
|
+
def element_type
|
193
|
+
unless @options[:of].is_a?(Hash)
|
194
|
+
@options[:of]
|
195
|
+
else
|
196
|
+
@options[:of].keys.first
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def element_options
|
201
|
+
unless @options[:of].is_a?(Hash)
|
202
|
+
{ type: element_type }
|
203
|
+
else
|
204
|
+
@options[:of][element_type].dup.tap do |options|
|
205
|
+
options[:type] = element_type
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
129
209
|
end
|
130
210
|
|
131
211
|
class DateTimeTypeCaster < Base
|
data/lib/dynamoid/undumping.rb
CHANGED
@@ -4,20 +4,24 @@ module Dynamoid
|
|
4
4
|
module Undumping
|
5
5
|
def self.undump_attributes(attributes, attributes_options)
|
6
6
|
{}.tap do |h|
|
7
|
-
attributes
|
8
|
-
|
7
|
+
# ignore existing attributes not declared in document class
|
8
|
+
attributes.symbolize_keys
|
9
|
+
.select { |attribute| attributes_options.key?(attribute) }
|
10
|
+
.each do |attribute, value|
|
11
|
+
h[attribute] = undump_field(value, attributes_options[attribute])
|
9
12
|
end
|
10
13
|
end
|
11
14
|
end
|
12
15
|
|
13
16
|
def self.undump_field(value, options)
|
17
|
+
return nil if value.nil?
|
18
|
+
|
14
19
|
undumper = find_undumper(options)
|
15
20
|
|
16
21
|
if undumper.nil?
|
17
22
|
raise ArgumentError, "Unknown type #{options[:type]}"
|
18
23
|
end
|
19
24
|
|
20
|
-
return nil if value.nil?
|
21
25
|
undumper.process(value)
|
22
26
|
end
|
23
27
|
|
@@ -64,19 +68,93 @@ module Dynamoid
|
|
64
68
|
end
|
65
69
|
|
66
70
|
class SetUndumper < Base
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
71
|
+
ALLOWED_TYPES = [:string, :integer, :number, :date, :datetime, :serialized]
|
72
|
+
|
73
|
+
def process(set)
|
74
|
+
if @options.key?(:of)
|
75
|
+
process_typed_collection(set)
|
76
|
+
else
|
77
|
+
set.is_a?(Set) ? set : Set.new(set)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def process_typed_collection(set)
|
84
|
+
if allowed_type?
|
85
|
+
undumper = Undumping.find_undumper(element_options)
|
86
|
+
set.map { |el| undumper.process(el) }.to_set
|
73
87
|
else
|
74
|
-
|
88
|
+
raise ArgumentError, "Set element type #{element_type} isn't supported"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def allowed_type?
|
93
|
+
ALLOWED_TYPES.include?(element_type) || element_type.is_a?(Class)
|
94
|
+
end
|
95
|
+
|
96
|
+
def element_type
|
97
|
+
unless @options[:of].is_a?(Hash)
|
98
|
+
@options[:of]
|
99
|
+
else
|
100
|
+
@options[:of].keys.first
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def element_options
|
105
|
+
unless @options[:of].is_a?(Hash)
|
106
|
+
{ type: element_type }
|
107
|
+
else
|
108
|
+
@options[:of][element_type].dup.tap do |options|
|
109
|
+
options[:type] = element_type
|
110
|
+
end
|
75
111
|
end
|
76
112
|
end
|
77
113
|
end
|
78
114
|
|
79
115
|
class ArrayUndumper < Base
|
116
|
+
ALLOWED_TYPES = [:string, :integer, :number, :date, :datetime, :serialized]
|
117
|
+
|
118
|
+
def process(array)
|
119
|
+
if @options.key?(:of)
|
120
|
+
process_typed_collection(array)
|
121
|
+
else
|
122
|
+
array.is_a?(Array) ? array : Array(array)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
def process_typed_collection(array)
|
129
|
+
if allowed_type?
|
130
|
+
undumper = Undumping.find_undumper(element_options)
|
131
|
+
array.map { |el| undumper.process(el) }
|
132
|
+
else
|
133
|
+
raise ArgumentError, "Array element type #{element_type} isn't supported"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def allowed_type?
|
138
|
+
ALLOWED_TYPES.include?(element_type) || element_type.is_a?(Class)
|
139
|
+
end
|
140
|
+
|
141
|
+
def element_type
|
142
|
+
unless @options[:of].is_a?(Hash)
|
143
|
+
@options[:of]
|
144
|
+
else
|
145
|
+
@options[:of].keys.first
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def element_options
|
150
|
+
unless @options[:of].is_a?(Hash)
|
151
|
+
{ type: element_type }
|
152
|
+
else
|
153
|
+
@options[:of][element_type].dup.tap do |options|
|
154
|
+
options[:type] = element_type
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
80
158
|
end
|
81
159
|
|
82
160
|
class DateTimeUndumper < Base
|