dynamoid 3.1.0 → 3.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|