dynamoid 3.2.0 → 3.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +111 -1
  3. data/README.md +580 -241
  4. data/lib/dynamoid.rb +2 -0
  5. data/lib/dynamoid/adapter.rb +15 -15
  6. data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +82 -102
  7. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/batch_get_item.rb +108 -0
  8. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb +29 -16
  9. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +3 -2
  10. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/backoff.rb +2 -2
  11. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/limit.rb +2 -3
  12. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/start_key.rb +2 -2
  13. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +15 -6
  14. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +15 -5
  15. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/table.rb +1 -0
  16. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/until_past_table_status.rb +5 -3
  17. data/lib/dynamoid/application_time_zone.rb +1 -0
  18. data/lib/dynamoid/associations.rb +182 -19
  19. data/lib/dynamoid/associations/association.rb +4 -2
  20. data/lib/dynamoid/associations/belongs_to.rb +2 -1
  21. data/lib/dynamoid/associations/has_and_belongs_to_many.rb +2 -1
  22. data/lib/dynamoid/associations/has_many.rb +2 -1
  23. data/lib/dynamoid/associations/has_one.rb +2 -1
  24. data/lib/dynamoid/associations/many_association.rb +65 -22
  25. data/lib/dynamoid/associations/single_association.rb +28 -1
  26. data/lib/dynamoid/components.rb +8 -3
  27. data/lib/dynamoid/config.rb +16 -3
  28. data/lib/dynamoid/config/backoff_strategies/constant_backoff.rb +1 -0
  29. data/lib/dynamoid/config/backoff_strategies/exponential_backoff.rb +1 -0
  30. data/lib/dynamoid/config/options.rb +1 -0
  31. data/lib/dynamoid/criteria.rb +2 -1
  32. data/lib/dynamoid/criteria/chain.rb +418 -46
  33. data/lib/dynamoid/criteria/ignored_conditions_detector.rb +3 -3
  34. data/lib/dynamoid/criteria/key_fields_detector.rb +109 -32
  35. data/lib/dynamoid/criteria/nonexistent_fields_detector.rb +3 -2
  36. data/lib/dynamoid/criteria/overwritten_conditions_detector.rb +1 -1
  37. data/lib/dynamoid/dirty.rb +239 -32
  38. data/lib/dynamoid/document.rb +130 -251
  39. data/lib/dynamoid/dumping.rb +9 -0
  40. data/lib/dynamoid/dynamodb_time_zone.rb +1 -0
  41. data/lib/dynamoid/fields.rb +246 -20
  42. data/lib/dynamoid/finders.rb +69 -32
  43. data/lib/dynamoid/identity_map.rb +6 -0
  44. data/lib/dynamoid/indexes.rb +76 -17
  45. data/lib/dynamoid/loadable.rb +31 -0
  46. data/lib/dynamoid/log/formatter.rb +26 -0
  47. data/lib/dynamoid/middleware/identity_map.rb +1 -0
  48. data/lib/dynamoid/persistence.rb +592 -122
  49. data/lib/dynamoid/persistence/import.rb +73 -0
  50. data/lib/dynamoid/persistence/save.rb +64 -0
  51. data/lib/dynamoid/persistence/update_fields.rb +63 -0
  52. data/lib/dynamoid/persistence/upsert.rb +60 -0
  53. data/lib/dynamoid/primary_key_type_mapping.rb +1 -0
  54. data/lib/dynamoid/railtie.rb +1 -0
  55. data/lib/dynamoid/tasks.rb +3 -1
  56. data/lib/dynamoid/tasks/database.rb +1 -0
  57. data/lib/dynamoid/type_casting.rb +12 -2
  58. data/lib/dynamoid/undumping.rb +8 -0
  59. data/lib/dynamoid/validations.rb +2 -0
  60. data/lib/dynamoid/version.rb +1 -1
  61. metadata +49 -71
  62. data/.coveralls.yml +0 -1
  63. data/.document +0 -5
  64. data/.gitignore +0 -74
  65. data/.rspec +0 -2
  66. data/.rubocop.yml +0 -71
  67. data/.rubocop_todo.yml +0 -55
  68. data/.travis.yml +0 -41
  69. data/Appraisals +0 -28
  70. data/Gemfile +0 -8
  71. data/Rakefile +0 -46
  72. data/Vagrantfile +0 -29
  73. data/docker-compose.yml +0 -7
  74. data/dynamoid.gemspec +0 -57
  75. data/gemfiles/rails_4_2.gemfile +0 -11
  76. data/gemfiles/rails_5_0.gemfile +0 -10
  77. data/gemfiles/rails_5_1.gemfile +0 -10
  78. data/gemfiles/rails_5_2.gemfile +0 -10
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dynamoid
4
+ # @private
4
5
  module Dumping
5
6
  def self.dump_attributes(attributes, attributes_options)
6
7
  {}.tap do |h|
@@ -35,6 +36,7 @@ module Dynamoid
35
36
  when :serialized then SerializedDumper
36
37
  when :raw then RawDumper
37
38
  when :boolean then BooleanDumper
39
+ when :binary then BinaryDumper
38
40
  when Class then CustomTypeDumper
39
41
  end
40
42
 
@@ -287,6 +289,13 @@ module Dynamoid
287
289
  end
288
290
  end
289
291
 
292
+ # string -> string
293
+ class BinaryDumper < Base
294
+ def process(value)
295
+ Base64.strict_encode64(value)
296
+ end
297
+ end
298
+
290
299
  # any object -> string
291
300
  class CustomTypeDumper < Base
292
301
  def process(value)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dynamoid
4
+ # @private
4
5
  module DynamodbTimeZone
5
6
  def self.in_time_zone(value)
6
7
  case Dynamoid::Config.dynamodb_timezone
@@ -1,11 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Dynamoid #:nodoc:
3
+ module Dynamoid
4
4
  # All fields on a Dynamoid::Document must be explicitly defined -- if you have fields in the database that are not
5
5
  # specified with field, then they will be ignored.
6
6
  module Fields
7
7
  extend ActiveSupport::Concern
8
8
 
9
+ # @private
9
10
  # Types allowed in indexes:
10
11
  PERMITTED_KEY_TYPES = %i[
11
12
  number
@@ -21,8 +22,11 @@ module Dynamoid #:nodoc:
21
22
  class_attribute :range_key
22
23
 
23
24
  self.attributes = {}
24
- field :created_at, :datetime
25
- field :updated_at, :datetime
25
+
26
+ # Timestamp fields could be disabled later in `table` method call.
27
+ # So let's declare them here and remove them later if it will be necessary
28
+ field :created_at, :datetime if Dynamoid::Config.timestamps
29
+ field :updated_at, :datetime if Dynamoid::Config.timestamps
26
30
 
27
31
  field :id # Default primary key
28
32
  end
@@ -30,18 +34,101 @@ module Dynamoid #:nodoc:
30
34
  module ClassMethods
31
35
  # Specify a field for a document.
32
36
  #
33
- # Its type determines how it is coerced when read in and out of the datastore.
34
- # You can specify :integer, :number, :set, :array, :datetime, :date and :serialized,
37
+ # class User
38
+ # include Dynamoid::Document
39
+ #
40
+ # field :last_name
41
+ # field :age, :integer
42
+ # field :last_sign_in, :datetime
43
+ # end
44
+ #
45
+ # Its type determines how it is coerced when read in and out of the
46
+ # datastore. You can specify +string+, +integer+, +number+, +set+, +array+,
47
+ # +map+, +datetime+, +date+, +serialized+, +raw+, +boolean+ and +binary+
35
48
  # or specify a class that defines a serialization strategy.
36
49
  #
50
+ # By default field type is +string+.
51
+ #
52
+ # Set can store elements of the same type only (it's a limitation of
53
+ # DynamoDB itself). If a set should store elements only some particular
54
+ # type +of+ option should be specified:
55
+ #
56
+ # field :hobbies, :set, of: :string
57
+ #
58
+ # Only +string+, +integer+, +number+, +date+, +datetime+ and +serialized+
59
+ # element types are supported.
60
+ #
61
+ # Element type can have own options - they should be specified in the
62
+ # form of +Hash+:
63
+ #
64
+ # field :hobbies, :set, of: { serialized: { serializer: JSON } }
65
+ #
66
+ # Array can contain element of different types but if supports the same
67
+ # +of+ option to convert all the provided elements to the declared type.
68
+ #
69
+ # field :rates, :array, of: :number
70
+ #
71
+ # By default +date+ and +datetime+ fields are stored as integer values.
72
+ # The format can be changed to string with option +store_as_string+:
73
+ #
74
+ # field :published_on, :datetime, store_as_string: true
75
+ #
76
+ # Boolean field by default is stored as a string +t+ or +f+. But DynamoDB
77
+ # supports boolean type natively. In order to switch to the native
78
+ # boolean type an option +store_as_native_boolean+ should be specified:
79
+ #
80
+ # field :active, :boolean, store_as_native_boolean: true
81
+ #
82
+ # If you specify the +serialized+ type a value will be serialized to
83
+ # string in Yaml format by default. Custom way to serialize value to
84
+ # string can be specified with +serializer+ option. Custom serializer
85
+ # should have +dump+ and +load+ methods.
86
+ #
37
87
  # If you specify a class for field type, Dynamoid will serialize using
38
- # `dynamoid_dump` or `dump` methods, and load using `dynamoid_load` or `load` methods.
88
+ # +dynamoid_dump+ method and load using +dynamoid_load+ method.
89
+ #
90
+ # Default field type is +string+.
91
+ #
92
+ # A field can have a default value. It's assigned at initializing a model
93
+ # if no value is specified:
39
94
  #
40
- # Default field type is :string.
95
+ # field :age, :integer, default: 1
41
96
  #
42
- # @param [Symbol] name the name of the field
43
- # @param [Symbol] type the type of the field (refer to method description for details)
44
- # @param [Hash] options any additional options for the field
97
+ # If a defautl value should be recalculated every time it can be
98
+ # specified as a callable object (it should implement a +call+ method
99
+ # e.g. +Proc+ object):
100
+ #
101
+ # field :date_of_birth, :date, default: -> { Date.today }
102
+ #
103
+ # For every field Dynamoid creates several methods:
104
+ #
105
+ # * getter
106
+ # * setter
107
+ # * predicate +<name>?+ to check whether a value set
108
+ # * +<name>_before_type_cast?+ to get an original field value before it was type casted
109
+ #
110
+ # It works in the following way:
111
+ #
112
+ # class User
113
+ # include Dynamoid::Document
114
+ #
115
+ # field :age, :integer
116
+ # end
117
+ #
118
+ # user = User.new
119
+ # user.age # => nil
120
+ # user.age? # => false
121
+ #
122
+ # user.age = 20
123
+ # user.age? # => true
124
+ #
125
+ # user.age = '21'
126
+ # user.age # => 21 - integer
127
+ # user.age_before_type_cast # => '21' - string
128
+ #
129
+ # @param name [Symbol] name of the field
130
+ # @param type [Symbol] type of the field (optional)
131
+ # @param options [Hash] any additional options for the field type (optional)
45
132
  #
46
133
  # @since 0.2.0
47
134
  def field(name, type = :string, options = {})
@@ -52,6 +139,14 @@ module Dynamoid #:nodoc:
52
139
  end
53
140
  self.attributes = attributes.merge(name => { type: type }.merge(options))
54
141
 
142
+ # should be called before `define_attribute_methods` method because it defines a getter itself
143
+ warn_about_method_overriding(name, name)
144
+ warn_about_method_overriding("#{named}=", name)
145
+ warn_about_method_overriding("#{named}?", name)
146
+ warn_about_method_overriding("#{named}_before_type_cast?", name)
147
+
148
+ define_attribute_method(name) # Dirty API
149
+
55
150
  generated_methods.module_eval do
56
151
  define_method(named) { read_attribute(named) }
57
152
  define_method("#{named}?") do
@@ -68,23 +163,103 @@ module Dynamoid #:nodoc:
68
163
  end
69
164
  end
70
165
 
166
+ # Declare a table range key.
167
+ #
168
+ # class User
169
+ # include Dynamoid::Document
170
+ #
171
+ # range :last_name
172
+ # end
173
+ #
174
+ # By default a range key is a string. In order to use any other type it
175
+ # should be specified as a second argument:
176
+ #
177
+ # range :age, :integer
178
+ #
179
+ # Type options can be specified as well:
180
+ #
181
+ # range :date_of_birth, :date, store_as_string: true
182
+ #
183
+ # @param name [Symbol] a range key attribute name
184
+ # @param type [Symbol] a range key type (optional)
185
+ # @param options [Symbol] type options (optional)
71
186
  def range(name, type = :string, options = {})
72
187
  field(name, type, options)
73
188
  self.range_key = name
74
189
  end
75
190
 
76
- def table(_options)
191
+ # Set table level properties.
192
+ #
193
+ # There are some sensible defaults:
194
+ #
195
+ # * table name is based on a model class e.g. +users+ for +User+ class
196
+ # * hash key name - +id+ by default
197
+ # * hash key type - +string+ by default
198
+ # * generating timestamp fields +created_at+ and +updated_at+
199
+ # * billing mode and read/write capacity units
200
+ #
201
+ # The +table+ method can be used to override the defaults:
202
+ #
203
+ # class User
204
+ # include Dynamoid::Document
205
+ #
206
+ # table name: :customers, key: :uuid
207
+ # end
208
+ #
209
+ # The hash key field is declared by default and a type is a string. If
210
+ # another type is needed the field should be declared explicitly:
211
+ #
212
+ # class User
213
+ # include Dynamoid::Document
214
+ #
215
+ # field :id, :integer
216
+ # end
217
+ #
218
+ # @param options [Hash] options to override default table settings
219
+ # @option options [Symbol] :name name of a table
220
+ # @option options [Symbol] :key name of a hash key attribute
221
+ # @option options [Symbol] :inheritance_field name of an attribute used for STI
222
+ # @option options [Symbol] :capacity_mode table billing mode - either +provisioned+ or +on_demand+
223
+ # @option options [Integer] :write_capacity table write capacity units
224
+ # @option options [Integer] :read_capacity table read capacity units
225
+ # @option options [true|false] :timestamps whether generate +created_at+ and +updated_at+ fields or not
226
+ # @option options [Hash] :expires set up a table TTL and should have following structure +{ field: <attriubute name>, after: <seconds> }+
227
+ #
228
+ # @since 0.4.0
229
+ def table(options)
77
230
  # a default 'id' column is created when Dynamoid::Document is included
78
231
  unless attributes.key? hash_key
79
232
  remove_field :id
80
233
  field(hash_key)
81
234
  end
235
+
236
+ if options[:timestamps] && !Dynamoid::Config.timestamps
237
+ # Timestamp fields weren't declared in `included` hook because they
238
+ # are disabled globaly
239
+ field :created_at, :datetime
240
+ field :updated_at, :datetime
241
+ elsif options[:timestamps] == false && Dynamoid::Config.timestamps
242
+ # Timestamp fields were declared in `included` hook but they are
243
+ # disabled for a table
244
+ remove_field :created_at
245
+ remove_field :updated_at
246
+ end
82
247
  end
83
248
 
249
+ # Remove a field declaration
250
+ #
251
+ # Removes a field from the list of fields and removes all te generated
252
+ # for a field methods.
253
+ #
254
+ # @param field [Symbol] a field name
84
255
  def remove_field(field)
85
256
  field = field.to_sym
86
257
  attributes.delete(field) || raise('No such field')
87
258
 
259
+ # Dirty API
260
+ undefine_attribute_methods
261
+ define_attribute_methods attributes.keys
262
+
88
263
  generated_methods.module_eval do
89
264
  remove_method field
90
265
  remove_method :"#{field}="
@@ -93,6 +268,11 @@ module Dynamoid #:nodoc:
93
268
  end
94
269
  end
95
270
 
271
+ # @private
272
+ def timestamps_enabled?
273
+ options[:timestamps] || (options[:timestamps].nil? && Dynamoid::Config.timestamps)
274
+ end
275
+
96
276
  private
97
277
 
98
278
  def generated_methods
@@ -102,16 +282,28 @@ module Dynamoid #:nodoc:
102
282
  end
103
283
  end
104
284
  end
285
+
286
+ def warn_about_method_overriding(method_name, field_name)
287
+ if instance_methods.include?(method_name.to_sym)
288
+ Dynamoid.logger.warn("Method #{method_name} generated for the field #{field_name} overrides already existing method")
289
+ end
290
+ end
105
291
  end
106
292
 
107
293
  # You can access the attributes of an object directly on its attributes method, which is by default an empty hash.
108
294
  attr_accessor :attributes
109
295
  alias raw_attributes attributes
110
296
 
111
- # Write an attribute on the object. Also marks the previous value as dirty.
297
+ # Write an attribute on the object.
298
+ #
299
+ # user.age = 20
300
+ # user.write_attribute(:age, 21)
301
+ # user.age # => 21
302
+ #
303
+ # Also marks the previous value as dirty.
112
304
  #
113
- # @param [Symbol] name the name of the field
114
- # @param [Object] value the value to assign to that field
305
+ # @param name [Symbol] the name of the field
306
+ # @param value [Object] the value to assign to that field
115
307
  #
116
308
  # @since 0.2.0
117
309
  def write_attribute(name, value)
@@ -121,6 +313,8 @@ module Dynamoid #:nodoc:
121
313
  association.reset
122
314
  end
123
315
 
316
+ attribute_will_change!(name) # Dirty API
317
+
124
318
  @attributes_before_type_cast[name] = value
125
319
 
126
320
  value_casted = TypeCasting.cast_field(value, self.class.attributes[name])
@@ -130,22 +324,40 @@ module Dynamoid #:nodoc:
130
324
 
131
325
  # Read an attribute from an object.
132
326
  #
133
- # @param [Symbol] name the name of the field
327
+ # user.age = 20
328
+ # user.read_attribute(:age) # => 20
134
329
  #
330
+ # @param name [Symbol] the name of the field
331
+ # @return attribute value
135
332
  # @since 0.2.0
136
333
  def read_attribute(name)
137
334
  attributes[name.to_sym]
138
335
  end
139
336
  alias [] read_attribute
140
337
 
141
- # Returns a hash of attributes before typecasting
338
+ # Return attributes values before type casting.
339
+ #
340
+ # user = User.new
341
+ # user.age = '21'
342
+ # user.age # => 21
343
+ #
344
+ # user.attributes_before_type_cast # => { age: '21' }
345
+ #
346
+ # @return [Hash] original attribute values
142
347
  def attributes_before_type_cast
143
348
  @attributes_before_type_cast
144
349
  end
145
350
 
146
- # Returns the value of the attribute identified by name before typecasting
351
+ # Return the value of the attribute identified by name before type casting.
147
352
  #
148
- # @param [Symbol] attribute name
353
+ # user = User.new
354
+ # user.age = '21'
355
+ # user.age # => 21
356
+ #
357
+ # user.read_attribute_before_type_cast(:age) # => '21'
358
+ #
359
+ # @param name [Symbol] attribute name
360
+ # @return original attribute value
149
361
  def read_attribute_before_type_cast(name)
150
362
  return nil unless name.respond_to?(:to_sym)
151
363
 
@@ -158,18 +370,32 @@ module Dynamoid #:nodoc:
158
370
  #
159
371
  # @since 0.2.0
160
372
  def set_created_at
161
- self.created_at ||= DateTime.now.in_time_zone(Time.zone) if Dynamoid::Config.timestamps
373
+ self.created_at ||= DateTime.now.in_time_zone(Time.zone) if self.class.timestamps_enabled?
162
374
  end
163
375
 
164
376
  # Automatically called during the save callback to set the updated_at time.
165
377
  #
166
378
  # @since 0.2.0
167
379
  def set_updated_at
168
- if Dynamoid::Config.timestamps && !updated_at_changed?
380
+ # @_touch_record=false means explicit disabling
381
+ if self.class.timestamps_enabled? && !updated_at_changed? && @_touch_record != false
169
382
  self.updated_at = DateTime.now.in_time_zone(Time.zone)
170
383
  end
171
384
  end
172
385
 
386
+ def set_expires_field
387
+ options = self.class.options[:expires]
388
+
389
+ if options.present?
390
+ name = options[:field]
391
+ seconds = options[:after]
392
+
393
+ if self[name].blank?
394
+ send("#{name}=", Time.now.to_i + seconds)
395
+ end
396
+ end
397
+ end
398
+
173
399
  def set_inheritance_field
174
400
  # actually it does only following logic:
175
401
  # self.type ||= self.class.name if self.class.attributes[:type]
@@ -6,6 +6,7 @@ module Dynamoid
6
6
  module Finders
7
7
  extend ActiveSupport::Concern
8
8
 
9
+ # @private
9
10
  RANGE_MAP = {
10
11
  'gt' => :range_greater_than,
11
12
  'lt' => :range_less_than,
@@ -19,9 +20,25 @@ module Dynamoid
19
20
  module ClassMethods
20
21
  # Find one or many objects, specified by one id or an array of ids.
21
22
  #
22
- # @param [Array/String] *id an array of ids or one single id
23
- # @param [Hash] options
23
+ # By default it raises +RecordNotFound+ exception if at least one model
24
+ # isn't found. This behavior can be changed with +raise_error+ option. If
25
+ # specified +raise_error: false+ option then +find+ will not raise the
26
+ # exception.
24
27
  #
28
+ # When a document schema includes range key it always should be specified
29
+ # in +find+ method call. In case it's missing +MissingRangeKey+ exception
30
+ # will be raised.
31
+ #
32
+ # Please note that +find+ doesn't preserve order of models in result when
33
+ # passes multiple ids.
34
+ #
35
+ # Supported following options:
36
+ # * +consistent_read+
37
+ # * +range_key+
38
+ # * +raise_error+
39
+ #
40
+ # @param ids [String|Array] hash key or an array of hash keys
41
+ # @param options [Hash]
25
42
  # @return [Dynamoid::Document] one object or an array of objects, depending on whether the input was an array or not
26
43
  #
27
44
  # @example Find by partition key
@@ -45,36 +62,45 @@ module Dynamoid
45
62
  # @since 0.2.0
46
63
  def find(*ids, **options)
47
64
  if ids.size == 1 && !ids[0].is_a?(Array)
48
- _find_by_id(ids[0], options.merge(raise_error: true))
65
+ _find_by_id(ids[0], options.reverse_merge(raise_error: true))
49
66
  else
50
- _find_all(ids.flatten(1), options.merge(raise_error: true))
67
+ _find_all(ids.flatten(1), options.reverse_merge(raise_error: true))
51
68
  end
52
69
  end
53
70
 
54
- # Return objects found by the given array of ids, either hash keys, or hash/range key combinations using BatchGetItem.
71
+ # Find several models at once.
72
+ #
73
+ # Returns objects found by the given array of ids, either hash keys, or
74
+ # hash/range key combinations using +BatchGetItem+.
75
+ #
55
76
  # Returns empty array if no results found.
56
77
  #
57
- # Uses backoff specified by `Dynamoid::Config.backoff` config option
78
+ # Uses backoff specified by +Dynamoid::Config.backoff+ config option.
58
79
  #
59
- # @param [Array<ID>] ids
60
- # @param [Hash] options: Passed to the underlying query.
80
+ # @param ids [Array] array of primary keys
81
+ # @param options [Hash]
82
+ # @option options [true|false] :consistent_read
83
+ # @option options [true|false] :raise_error
61
84
  #
62
85
  # @example
63
- # find all the user with hash key
86
+ # # Find all the user with hash key
64
87
  # User.find_all(['1', '2', '3'])
65
88
  #
66
- # find all the tweets using hash key and range key with consistent read
67
- # Tweet.find_all([['1', 'red'], ['1', 'green']], :consistent_read => true)
89
+ # # Find all the tweets using hash key and range key with consistent read
90
+ # Tweet.find_all([['1', 'red'], ['1', 'green']], consistent_read: true)
68
91
  def find_all(ids, options = {})
69
92
  ActiveSupport::Deprecation.warn('[Dynamoid] .find_all is deprecated! Call .find instead of')
70
93
 
71
94
  _find_all(ids, options)
72
95
  end
73
96
 
74
- # Find one object directly by id.
75
- #
76
- # @param [String] id the id of the object to find
97
+ # Find one object directly by primary key.
77
98
  #
99
+ # @param id [String] the id of the object to find
100
+ # @param options [Hash]
101
+ # @option options [true|false] :consistent_read
102
+ # @option options [true|false] :raise_error
103
+ # @option options [Scalar value] :range_key
78
104
  # @return [Dynamoid::Document] the found object, or nil if nothing was found
79
105
  #
80
106
  # @example Find by partition key
@@ -90,7 +116,10 @@ module Dynamoid
90
116
  _find_by_id(id, options)
91
117
  end
92
118
 
119
+ # @private
93
120
  def _find_all(ids, options = {})
121
+ raise Errors::MissingRangeKey if range_key && ids.any? { |pk, sk| sk.nil? }
122
+
94
123
  if range_key
95
124
  ids = ids.map do |pk, sk|
96
125
  sk_casted = TypeCasting.cast_field(sk, attributes[range_key])
@@ -131,7 +160,10 @@ module Dynamoid
131
160
  end
132
161
  end
133
162
 
163
+ # @private
134
164
  def _find_by_id(id, options = {})
165
+ raise Errors::MissingRangeKey if range_key && options[:range_key].nil?
166
+
135
167
  if range_key
136
168
  key = options[:range_key]
137
169
  key_casted = TypeCasting.cast_field(key, attributes[range_key])
@@ -149,10 +181,10 @@ module Dynamoid
149
181
  end
150
182
  end
151
183
 
152
- # Find one object directly by hash and range keys
184
+ # Find one object directly by hash and range keys.
153
185
  #
154
- # @param [String] hash_key of the object to find
155
- # @param [String/Number] range_key of the object to find
186
+ # @param hash_key [Scalar value] hash key of the object to find
187
+ # @param range_key [Scalar value] range key of the object to find
156
188
  #
157
189
  def find_by_composite_key(hash_key, range_key, options = {})
158
190
  ActiveSupport::Deprecation.warn('[Dynamoid] .find_by_composite_key is deprecated! Call .find instead of')
@@ -169,6 +201,7 @@ module Dynamoid
169
201
  # range :level, :integer
170
202
  # table :key => :chamber_type
171
203
  # end
204
+ #
172
205
  # ChamberType.find_all_by_composite_key('DustVault', range_greater_than: 1)
173
206
  #
174
207
  # @param [String] hash_key of the objects to find
@@ -183,7 +216,7 @@ module Dynamoid
183
216
  def find_all_by_composite_key(hash_key, options = {})
184
217
  ActiveSupport::Deprecation.warn('[Dynamoid] .find_all_composite_key is deprecated! Call .where instead of')
185
218
 
186
- Dynamoid.adapter.query(table_name, options.merge(hash_value: hash_key)).flat_map{ |i| i }.collect do |item|
219
+ Dynamoid.adapter.query(table_name, options.merge(hash_value: hash_key)).flat_map { |i| i }.collect do |item|
187
220
  from_database(item)
188
221
  end
189
222
  end
@@ -193,20 +226,22 @@ module Dynamoid
193
226
  # @example
194
227
  # class User
195
228
  # include Dynamoid::Document
196
- # field :email, :string
197
- # field :age, :integer
198
- # field :gender, :string
199
- # field :rank :number
229
+ #
200
230
  # table :key => :email
201
- # global_secondary_index :hash_key => :age, :range_key => :rank
231
+ # global_secondary_index hash_key: :age, range_key: :rank
232
+ #
233
+ # field :email, :string
234
+ # field :age, :integer
235
+ # field :gender, :string
236
+ # field :rank :number
202
237
  # end
238
+ #
203
239
  # # NOTE: the first param and the second param are both hashes,
204
240
  # # so curly braces must be used on first hash param if sending both params
205
- # User.find_all_by_secondary_index({:age => 5}, :range => {"rank.lte" => 10})
241
+ # User.find_all_by_secondary_index({ age: 5 }, range: { "rank.lte": 10 })
206
242
  #
207
- # @param [Hash] eg: {:age => 5}
208
- # @param [Hash] eg: {"rank.lte" => 10}
209
- # @param [Hash] options - query filter, projected keys, scan_index_forward etc
243
+ # @param hash [Hash] conditions for the hash key e.g. +{ age: 5 }+
244
+ # @param options [Hash] conditions on range key e.g. +{ "rank.lte": 10 }, query filter, projected keys, scan_index_forward etc.
210
245
  # @return [Array] an array of all matching items
211
246
  def find_all_by_secondary_index(hash, options = {})
212
247
  ActiveSupport::Deprecation.warn('[Dynamoid] .find_all_by_secondary_index is deprecated! Call .where instead of')
@@ -240,12 +275,13 @@ module Dynamoid
240
275
  opts[range_op_mapped] = range_key_value
241
276
  end
242
277
  dynamo_options = opts.merge(options.reject { |key, _| key == :range })
243
- Dynamoid.adapter.query(table_name, dynamo_options).flat_map{ |i| i }.map do |item|
278
+ Dynamoid.adapter.query(table_name, dynamo_options).flat_map { |i| i }.map do |item|
244
279
  from_database(item)
245
280
  end
246
281
  end
247
282
 
248
- # Find using exciting method_missing finders attributes. Uses criteria chains under the hood to accomplish this neatness.
283
+ # Find using exciting method_missing finders attributes. Uses criteria
284
+ # chains under the hood to accomplish this neatness.
249
285
  #
250
286
  # @example find a user by a first name
251
287
  # User.find_by_first_name('Josh')
@@ -253,8 +289,9 @@ module Dynamoid
253
289
  # @example find all users by first and last name
254
290
  # User.find_all_by_first_name_and_last_name('Josh', 'Symonds')
255
291
  #
256
- # @return [Dynamoid::Document/Array] the found object, or an array of found objects if all was somewhere in the method
292
+ # @return [Dynamoid::Document|Array] the found object, or an array of found objects if all was somewhere in the method
257
293
  #
294
+ # @private
258
295
  # @since 0.2.0
259
296
  def method_missing(method, *args)
260
297
  if method =~ /find/
@@ -267,9 +304,9 @@ module Dynamoid
267
304
  chain = chain.where({}.tap { |h| attributes.each_with_index { |attr, index| h[attr.to_sym] = args[index] } })
268
305
 
269
306
  if finder =~ /all/
270
- return chain.all
307
+ chain.all
271
308
  else
272
- return chain.first
309
+ chain.first
273
310
  end
274
311
  else
275
312
  super