dynamoid 3.1.0 → 3.2.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 +5 -5
- data/.rubocop.yml +18 -0
- data/.travis.yml +5 -3
- data/CHANGELOG.md +15 -0
- data/README.md +113 -63
- data/Vagrantfile +2 -2
- data/docker-compose.yml +1 -1
- data/gemfiles/rails_4_2.gemfile +1 -1
- data/gemfiles/rails_5_0.gemfile +1 -1
- data/gemfiles/rails_5_1.gemfile +1 -1
- data/gemfiles/rails_5_2.gemfile +1 -1
- data/lib/dynamoid/adapter.rb +1 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +26 -395
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb +234 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +89 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/backoff.rb +24 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/limit.rb +57 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/start_key.rb +28 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +123 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +85 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/table.rb +52 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/until_past_table_status.rb +60 -0
- data/lib/dynamoid/associations/has_and_belongs_to_many.rb +1 -0
- data/lib/dynamoid/associations/has_many.rb +1 -0
- data/lib/dynamoid/associations/has_one.rb +1 -0
- data/lib/dynamoid/associations/single_association.rb +1 -0
- data/lib/dynamoid/criteria.rb +4 -4
- data/lib/dynamoid/criteria/chain.rb +86 -79
- data/lib/dynamoid/criteria/ignored_conditions_detector.rb +41 -0
- data/lib/dynamoid/criteria/key_fields_detector.rb +61 -0
- data/lib/dynamoid/criteria/nonexistent_fields_detector.rb +41 -0
- data/lib/dynamoid/criteria/overwritten_conditions_detector.rb +40 -0
- data/lib/dynamoid/document.rb +18 -13
- data/lib/dynamoid/dumping.rb +52 -40
- data/lib/dynamoid/fields.rb +4 -3
- data/lib/dynamoid/finders.rb +3 -3
- data/lib/dynamoid/persistence.rb +5 -6
- data/lib/dynamoid/primary_key_type_mapping.rb +1 -1
- data/lib/dynamoid/tasks.rb +1 -0
- data/lib/dynamoid/tasks/database.rake +2 -2
- data/lib/dynamoid/type_casting.rb +37 -19
- data/lib/dynamoid/undumping.rb +53 -42
- data/lib/dynamoid/validations.rb +2 -0
- data/lib/dynamoid/version.rb +1 -1
- metadata +17 -5
- data/lib/dynamoid/adapter_plugin/query.rb +0 -144
- data/lib/dynamoid/adapter_plugin/scan.rb +0 -107
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dynamoid
|
4
|
+
module Criteria
|
5
|
+
class IgnoredConditionsDetector
|
6
|
+
def initialize(conditions)
|
7
|
+
@conditions = conditions
|
8
|
+
@ignored_keys = ignored_keys
|
9
|
+
end
|
10
|
+
|
11
|
+
def found?
|
12
|
+
@ignored_keys.present?
|
13
|
+
end
|
14
|
+
|
15
|
+
def warning_message
|
16
|
+
return unless found?
|
17
|
+
|
18
|
+
'Where conditions may contain only one condition for an attribute. ' \
|
19
|
+
"Following conditions are ignored: #{ignored_conditions}"
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def ignored_keys
|
25
|
+
@conditions.keys
|
26
|
+
.group_by(&method(:key_to_field))
|
27
|
+
.select { |field, ary| ary.size > 1 }
|
28
|
+
.flat_map { |field, ary| ary[0 .. -2] }
|
29
|
+
end
|
30
|
+
|
31
|
+
def key_to_field(key)
|
32
|
+
key.to_s.split('.')[0]
|
33
|
+
end
|
34
|
+
|
35
|
+
def ignored_conditions
|
36
|
+
@conditions.slice(*@ignored_keys)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dynamoid #:nodoc:
|
4
|
+
module Criteria
|
5
|
+
class KeyFieldsDetector
|
6
|
+
attr_reader :hash_key, :range_key, :index_name
|
7
|
+
|
8
|
+
def initialize(query, source)
|
9
|
+
@query = query
|
10
|
+
@source = source
|
11
|
+
|
12
|
+
detect_keys
|
13
|
+
end
|
14
|
+
|
15
|
+
def key_present?
|
16
|
+
@hash_key.present?
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def detect_keys
|
22
|
+
query_keys = @query.keys.collect { |k| k.to_s.split('.').first }
|
23
|
+
|
24
|
+
# See if querying based on table hash key
|
25
|
+
if @query.keys.map(&:to_s).include?(@source.hash_key.to_s)
|
26
|
+
@hash_key = @source.hash_key
|
27
|
+
|
28
|
+
# Use table's default range key
|
29
|
+
if query_keys.include?(@source.range_key.to_s)
|
30
|
+
@range_key = @source.range_key
|
31
|
+
return
|
32
|
+
end
|
33
|
+
|
34
|
+
# See if can use any local secondary index range key
|
35
|
+
# Chooses the first LSI found that can be utilized for the query
|
36
|
+
@source.local_secondary_indexes.each do |_, lsi|
|
37
|
+
next unless query_keys.include?(lsi.range_key.to_s)
|
38
|
+
|
39
|
+
@range_key = lsi.range_key
|
40
|
+
@index_name = lsi.name
|
41
|
+
end
|
42
|
+
|
43
|
+
return
|
44
|
+
end
|
45
|
+
|
46
|
+
# See if can use any global secondary index
|
47
|
+
# Chooses the first GSI found that can be utilized for the query
|
48
|
+
# But only do so if projects ALL attributes otherwise we won't
|
49
|
+
# get back full data
|
50
|
+
@source.global_secondary_indexes.each do |_, gsi|
|
51
|
+
next unless @query.keys.map(&:to_s).include?(gsi.hash_key.to_s) && gsi.projected_attributes == :all
|
52
|
+
next if @range_key.present? && !query_keys.include?(gsi.range_key.to_s)
|
53
|
+
|
54
|
+
@hash_key = gsi.hash_key
|
55
|
+
@range_key = gsi.range_key
|
56
|
+
@index_name = gsi.name
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dynamoid
|
4
|
+
module Criteria
|
5
|
+
class NonexistentFieldsDetector
|
6
|
+
def initialize(conditions, source)
|
7
|
+
@conditions = conditions
|
8
|
+
@source = source
|
9
|
+
@nonexistent_fields = nonexistent_fields
|
10
|
+
end
|
11
|
+
|
12
|
+
def found?
|
13
|
+
@nonexistent_fields.present?
|
14
|
+
end
|
15
|
+
|
16
|
+
def warning_message
|
17
|
+
return unless found?
|
18
|
+
|
19
|
+
fields_list = @nonexistent_fields.map { |s| "`#{s}`" }.join(', ')
|
20
|
+
count = @nonexistent_fields.size
|
21
|
+
|
22
|
+
"where conditions contain nonexistent" \
|
23
|
+
" field #{ 'name'.pluralize(count) } #{ fields_list }"
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def nonexistent_fields
|
29
|
+
fields_from_conditions - fields_existent
|
30
|
+
end
|
31
|
+
|
32
|
+
def fields_from_conditions
|
33
|
+
@conditions.keys.map { |s| s.to_s.split('.')[0].to_sym }
|
34
|
+
end
|
35
|
+
|
36
|
+
def fields_existent
|
37
|
+
@source.attributes.keys.map(&:to_sym)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dynamoid
|
4
|
+
module Criteria
|
5
|
+
class OverwrittenConditionsDetector
|
6
|
+
def initialize(conditions, conditions_new)
|
7
|
+
@conditions = conditions
|
8
|
+
@new_conditions = conditions_new
|
9
|
+
@overwritten_keys = overwritten_keys
|
10
|
+
end
|
11
|
+
|
12
|
+
def found?
|
13
|
+
@overwritten_keys.present?
|
14
|
+
end
|
15
|
+
|
16
|
+
def warning_message
|
17
|
+
return unless found?
|
18
|
+
|
19
|
+
'Where conditions may contain only one condition for an attribute. ' \
|
20
|
+
"Following conditions are ignored: #{ignored_conditions}"
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def overwritten_keys
|
26
|
+
new_fields = @new_conditions.keys.map(&method(:key_to_field))
|
27
|
+
@conditions.keys.select { |key| key_to_field(key).in?(new_fields) }
|
28
|
+
end
|
29
|
+
|
30
|
+
def key_to_field(key)
|
31
|
+
key.to_s.split('.')[0]
|
32
|
+
end
|
33
|
+
|
34
|
+
def ignored_conditions
|
35
|
+
@conditions.slice(*@overwritten_keys.map(&:to_sym))
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
data/lib/dynamoid/document.rb
CHANGED
@@ -33,6 +33,7 @@ module Dynamoid #:nodoc:
|
|
33
33
|
end
|
34
34
|
|
35
35
|
def attr_readonly(*read_only_attributes)
|
36
|
+
ActiveSupport::Deprecation.warn('[Dynamoid] .attr_readonly is deprecated! Call .find instead of')
|
36
37
|
self.read_only_attributes.concat read_only_attributes.map(&:to_s)
|
37
38
|
end
|
38
39
|
|
@@ -112,14 +113,28 @@ module Dynamoid #:nodoc:
|
|
112
113
|
|
113
114
|
# Does this object exist?
|
114
115
|
#
|
116
|
+
# Supports primary key in format that `find` call understands.
|
117
|
+
# Multiple keys and single compound primary key should be passed only as Array explicitily.
|
118
|
+
#
|
119
|
+
# Supports conditions in format that `where` call understands.
|
120
|
+
#
|
115
121
|
# @param [Mixed] id_or_conditions the id of the object or a hash with the options to filter from.
|
116
122
|
#
|
117
123
|
# @return [Boolean] true/false
|
118
124
|
#
|
125
|
+
# @example With id
|
126
|
+
#
|
127
|
+
# Post.exist?(713)
|
128
|
+
# Post.exist?([713, 210])
|
129
|
+
#
|
130
|
+
# @example With attributes conditions
|
131
|
+
#
|
132
|
+
# Post.exist?(version: 1, 'created_at.gt': Time.now - 1.day)
|
133
|
+
#
|
119
134
|
# @since 0.2.0
|
120
135
|
def exists?(id_or_conditions = {})
|
121
136
|
case id_or_conditions
|
122
|
-
when Hash then where(id_or_conditions).
|
137
|
+
when Hash then where(id_or_conditions).count >= 1
|
123
138
|
else
|
124
139
|
begin
|
125
140
|
find(id_or_conditions)
|
@@ -205,7 +220,6 @@ module Dynamoid #:nodoc:
|
|
205
220
|
end
|
206
221
|
end
|
207
222
|
|
208
|
-
|
209
223
|
# Update existing document or create new one.
|
210
224
|
# Similar to `.update_fields`. The only diffirence is creating new document.
|
211
225
|
#
|
@@ -267,7 +281,7 @@ module Dynamoid #:nodoc:
|
|
267
281
|
end
|
268
282
|
end
|
269
283
|
|
270
|
-
def inc(hash_key_value, range_key_value=nil, counters)
|
284
|
+
def inc(hash_key_value, range_key_value = nil, counters)
|
271
285
|
options = if range_key
|
272
286
|
value_casted = TypeCasting.cast_field(range_key_value, attributes[range_key])
|
273
287
|
value_dumped = Dumping.dump_field(value_casted, attributes[range_key])
|
@@ -303,22 +317,12 @@ module Dynamoid #:nodoc:
|
|
303
317
|
#
|
304
318
|
# @since 0.2.0
|
305
319
|
def initialize(attrs = {})
|
306
|
-
# we need this hack for Rails 4.0 only
|
307
|
-
# because `run_callbacks` calls `attributes` getter while it is still nil
|
308
|
-
@attributes = {}
|
309
|
-
|
310
320
|
run_callbacks :initialize do
|
311
321
|
@new_record = true
|
312
322
|
@attributes ||= {}
|
313
323
|
@associations ||= {}
|
314
324
|
@attributes_before_type_cast ||= {}
|
315
325
|
|
316
|
-
self.class.attributes.each do |_, options|
|
317
|
-
if options[:type].is_a?(Class) && options[:default]
|
318
|
-
raise 'Dynamoid class-type fields do not support default values'
|
319
|
-
end
|
320
|
-
end
|
321
|
-
|
322
326
|
attrs_with_defaults = {}
|
323
327
|
self.class.attributes.each do |attribute, options|
|
324
328
|
attrs_with_defaults[attribute] = if attrs.key?(attribute)
|
@@ -348,6 +352,7 @@ module Dynamoid #:nodoc:
|
|
348
352
|
super
|
349
353
|
else
|
350
354
|
return false if other.nil?
|
355
|
+
|
351
356
|
other.is_a?(Dynamoid::Document) && hash_key == other.hash_key && range_value == other.range_value
|
352
357
|
end
|
353
358
|
end
|
data/lib/dynamoid/dumping.rb
CHANGED
@@ -29,6 +29,7 @@ module Dynamoid
|
|
29
29
|
when :number then NumberDumper
|
30
30
|
when :set then SetDumper
|
31
31
|
when :array then ArrayDumper
|
32
|
+
when :map then MapDumper
|
32
33
|
when :datetime then DateTimeDumper
|
33
34
|
when :date then DateDumper
|
34
35
|
when :serialized then SerializedDumper
|
@@ -42,6 +43,35 @@ module Dynamoid
|
|
42
43
|
end
|
43
44
|
end
|
44
45
|
|
46
|
+
module DeepSanitizeHelper
|
47
|
+
extend self
|
48
|
+
|
49
|
+
def deep_sanitize(value)
|
50
|
+
case value
|
51
|
+
when Hash
|
52
|
+
sanitize_hash(value).transform_values { |v| deep_sanitize(v) }
|
53
|
+
when Array
|
54
|
+
sanitize_array(value).map { |v| deep_sanitize(v) }
|
55
|
+
else
|
56
|
+
value
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def sanitize_hash(hash)
|
63
|
+
hash.transform_values { |v| invalid_value?(v) ? nil : v }
|
64
|
+
end
|
65
|
+
|
66
|
+
def sanitize_array(array)
|
67
|
+
array.map { |v| invalid_value?(v) ? nil : v }
|
68
|
+
end
|
69
|
+
|
70
|
+
def invalid_value?(value)
|
71
|
+
(value.is_a?(Set) || value.is_a?(String)) && value.empty?
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
45
75
|
class Base
|
46
76
|
def initialize(options)
|
47
77
|
@options = options
|
@@ -66,7 +96,7 @@ module Dynamoid
|
|
66
96
|
|
67
97
|
# set -> set
|
68
98
|
class SetDumper < Base
|
69
|
-
ALLOWED_TYPES = [
|
99
|
+
ALLOWED_TYPES = %i[string integer number date datetime serialized].freeze
|
70
100
|
|
71
101
|
def process(set)
|
72
102
|
if @options.key?(:of)
|
@@ -98,27 +128,27 @@ module Dynamoid
|
|
98
128
|
end
|
99
129
|
|
100
130
|
def element_type
|
101
|
-
|
102
|
-
@options[:of]
|
103
|
-
else
|
131
|
+
if @options[:of].is_a?(Hash)
|
104
132
|
@options[:of].keys.first
|
133
|
+
else
|
134
|
+
@options[:of]
|
105
135
|
end
|
106
136
|
end
|
107
137
|
|
108
138
|
def element_options
|
109
|
-
|
110
|
-
{ type: element_type }
|
111
|
-
else
|
139
|
+
if @options[:of].is_a?(Hash)
|
112
140
|
@options[:of][element_type].dup.tap do |options|
|
113
141
|
options[:type] = element_type
|
114
142
|
end
|
143
|
+
else
|
144
|
+
{ type: element_type }
|
115
145
|
end
|
116
146
|
end
|
117
147
|
end
|
118
148
|
|
119
149
|
# array -> array
|
120
150
|
class ArrayDumper < Base
|
121
|
-
ALLOWED_TYPES = [
|
151
|
+
ALLOWED_TYPES = %i[string integer number date datetime serialized].freeze
|
122
152
|
|
123
153
|
def process(array)
|
124
154
|
if @options.key?(:of)
|
@@ -150,24 +180,31 @@ module Dynamoid
|
|
150
180
|
end
|
151
181
|
|
152
182
|
def element_type
|
153
|
-
|
154
|
-
@options[:of]
|
155
|
-
else
|
183
|
+
if @options[:of].is_a?(Hash)
|
156
184
|
@options[:of].keys.first
|
185
|
+
else
|
186
|
+
@options[:of]
|
157
187
|
end
|
158
188
|
end
|
159
189
|
|
160
190
|
def element_options
|
161
|
-
|
162
|
-
{ type: element_type }
|
163
|
-
else
|
191
|
+
if @options[:of].is_a?(Hash)
|
164
192
|
@options[:of][element_type].dup.tap do |options|
|
165
193
|
options[:type] = element_type
|
166
194
|
end
|
195
|
+
else
|
196
|
+
{ type: element_type }
|
167
197
|
end
|
168
198
|
end
|
169
199
|
end
|
170
200
|
|
201
|
+
# hash -> map
|
202
|
+
class MapDumper < Base
|
203
|
+
def process(value)
|
204
|
+
DeepSanitizeHelper.deep_sanitize(value)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
171
208
|
# datetime -> integer/string
|
172
209
|
class DateTimeDumper < Base
|
173
210
|
def process(value)
|
@@ -221,32 +258,7 @@ module Dynamoid
|
|
221
258
|
# any standard Ruby object -> self
|
222
259
|
class RawDumper < Base
|
223
260
|
def process(value)
|
224
|
-
deep_sanitize(value)
|
225
|
-
end
|
226
|
-
|
227
|
-
private
|
228
|
-
|
229
|
-
def deep_sanitize(el)
|
230
|
-
case el
|
231
|
-
when Hash
|
232
|
-
sanitize_hash(el).transform_values { |v| deep_sanitize(v) }
|
233
|
-
when Array
|
234
|
-
sanitize_array(el).map { |v| deep_sanitize(v) }
|
235
|
-
else
|
236
|
-
el
|
237
|
-
end
|
238
|
-
end
|
239
|
-
|
240
|
-
def sanitize_hash(h)
|
241
|
-
h.transform_values { |v| invalid_value?(v) ? nil : v }
|
242
|
-
end
|
243
|
-
|
244
|
-
def sanitize_array(a)
|
245
|
-
a.map { |v| invalid_value?(v) ? nil : v }
|
246
|
-
end
|
247
|
-
|
248
|
-
def invalid_value?(v)
|
249
|
-
(v.is_a?(Set) || v.is_a?(String)) && v.empty?
|
261
|
+
DeepSanitizeHelper.deep_sanitize(value)
|
250
262
|
end
|
251
263
|
end
|
252
264
|
|
data/lib/dynamoid/fields.rb
CHANGED
@@ -89,7 +89,7 @@ module Dynamoid #:nodoc:
|
|
89
89
|
remove_method field
|
90
90
|
remove_method :"#{field}="
|
91
91
|
remove_method :"#{field}?"
|
92
|
-
remove_method:"#{field}_before_type_cast"
|
92
|
+
remove_method :"#{field}_before_type_cast"
|
93
93
|
end
|
94
94
|
end
|
95
95
|
|
@@ -148,6 +148,7 @@ module Dynamoid #:nodoc:
|
|
148
148
|
# @param [Symbol] attribute name
|
149
149
|
def read_attribute_before_type_cast(name)
|
150
150
|
return nil unless name.respond_to?(:to_sym)
|
151
|
+
|
151
152
|
@attributes_before_type_cast[name.to_sym]
|
152
153
|
end
|
153
154
|
|
@@ -174,8 +175,8 @@ module Dynamoid #:nodoc:
|
|
174
175
|
# self.type ||= self.class.name if self.class.attributes[:type]
|
175
176
|
|
176
177
|
type = self.class.inheritance_field
|
177
|
-
if self.class.attributes[type] &&
|
178
|
-
|
178
|
+
if self.class.attributes[type] && send(type).nil?
|
179
|
+
send("#{type}=", self.class.name)
|
179
180
|
end
|
180
181
|
end
|
181
182
|
end
|