dynamoid 3.1.0 → 3.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +18 -0
  3. data/.travis.yml +5 -3
  4. data/CHANGELOG.md +15 -0
  5. data/README.md +113 -63
  6. data/Vagrantfile +2 -2
  7. data/docker-compose.yml +1 -1
  8. data/gemfiles/rails_4_2.gemfile +1 -1
  9. data/gemfiles/rails_5_0.gemfile +1 -1
  10. data/gemfiles/rails_5_1.gemfile +1 -1
  11. data/gemfiles/rails_5_2.gemfile +1 -1
  12. data/lib/dynamoid/adapter.rb +1 -0
  13. data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +26 -395
  14. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb +234 -0
  15. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +89 -0
  16. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/backoff.rb +24 -0
  17. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/limit.rb +57 -0
  18. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/start_key.rb +28 -0
  19. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +123 -0
  20. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +85 -0
  21. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/table.rb +52 -0
  22. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/until_past_table_status.rb +60 -0
  23. data/lib/dynamoid/associations/has_and_belongs_to_many.rb +1 -0
  24. data/lib/dynamoid/associations/has_many.rb +1 -0
  25. data/lib/dynamoid/associations/has_one.rb +1 -0
  26. data/lib/dynamoid/associations/single_association.rb +1 -0
  27. data/lib/dynamoid/criteria.rb +4 -4
  28. data/lib/dynamoid/criteria/chain.rb +86 -79
  29. data/lib/dynamoid/criteria/ignored_conditions_detector.rb +41 -0
  30. data/lib/dynamoid/criteria/key_fields_detector.rb +61 -0
  31. data/lib/dynamoid/criteria/nonexistent_fields_detector.rb +41 -0
  32. data/lib/dynamoid/criteria/overwritten_conditions_detector.rb +40 -0
  33. data/lib/dynamoid/document.rb +18 -13
  34. data/lib/dynamoid/dumping.rb +52 -40
  35. data/lib/dynamoid/fields.rb +4 -3
  36. data/lib/dynamoid/finders.rb +3 -3
  37. data/lib/dynamoid/persistence.rb +5 -6
  38. data/lib/dynamoid/primary_key_type_mapping.rb +1 -1
  39. data/lib/dynamoid/tasks.rb +1 -0
  40. data/lib/dynamoid/tasks/database.rake +2 -2
  41. data/lib/dynamoid/type_casting.rb +37 -19
  42. data/lib/dynamoid/undumping.rb +53 -42
  43. data/lib/dynamoid/validations.rb +2 -0
  44. data/lib/dynamoid/version.rb +1 -1
  45. metadata +17 -5
  46. data/lib/dynamoid/adapter_plugin/query.rb +0 -144
  47. 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
+
@@ -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).first.present?
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
@@ -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 = [:string, :integer, :number, :date, :datetime, :serialized]
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
- unless @options[:of].is_a?(Hash)
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
- unless @options[:of].is_a?(Hash)
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 = [:string, :integer, :number, :date, :datetime, :serialized]
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
- unless @options[:of].is_a?(Hash)
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
- unless @options[:of].is_a?(Hash)
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
 
@@ -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] && self.send(type).nil?
178
- self.send("#{type}=", self.class.name)
178
+ if self.class.attributes[type] && send(type).nil?
179
+ send("#{type}=", self.class.name)
179
180
  end
180
181
  end
181
182
  end