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