dynamoid 3.3.0 → 3.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +104 -1
  3. data/README.md +146 -52
  4. data/lib/dynamoid.rb +1 -0
  5. data/lib/dynamoid/adapter.rb +20 -7
  6. data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +70 -37
  7. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/batch_get_item.rb +3 -0
  8. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb +20 -12
  9. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +5 -4
  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 +4 -2
  14. data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +4 -2
  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 +2 -1
  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 +10 -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 +68 -23
  25. data/lib/dynamoid/associations/single_association.rb +31 -4
  26. data/lib/dynamoid/components.rb +2 -0
  27. data/lib/dynamoid/config.rb +15 -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 +9 -1
  32. data/lib/dynamoid/criteria/chain.rb +421 -46
  33. data/lib/dynamoid/criteria/ignored_conditions_detector.rb +3 -3
  34. data/lib/dynamoid/criteria/key_fields_detector.rb +31 -10
  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 +119 -64
  38. data/lib/dynamoid/document.rb +133 -46
  39. data/lib/dynamoid/dumping.rb +9 -0
  40. data/lib/dynamoid/dynamodb_time_zone.rb +1 -0
  41. data/lib/dynamoid/errors.rb +2 -0
  42. data/lib/dynamoid/fields.rb +251 -39
  43. data/lib/dynamoid/fields/declare.rb +86 -0
  44. data/lib/dynamoid/finders.rb +69 -32
  45. data/lib/dynamoid/identity_map.rb +6 -0
  46. data/lib/dynamoid/indexes.rb +86 -17
  47. data/lib/dynamoid/loadable.rb +2 -2
  48. data/lib/dynamoid/log/formatter.rb +26 -0
  49. data/lib/dynamoid/middleware/identity_map.rb +1 -0
  50. data/lib/dynamoid/persistence.rb +502 -104
  51. data/lib/dynamoid/persistence/import.rb +2 -1
  52. data/lib/dynamoid/persistence/save.rb +1 -0
  53. data/lib/dynamoid/persistence/update_fields.rb +5 -2
  54. data/lib/dynamoid/persistence/update_validations.rb +18 -0
  55. data/lib/dynamoid/persistence/upsert.rb +5 -3
  56. data/lib/dynamoid/primary_key_type_mapping.rb +1 -0
  57. data/lib/dynamoid/railtie.rb +1 -0
  58. data/lib/dynamoid/tasks.rb +3 -1
  59. data/lib/dynamoid/tasks/database.rb +1 -0
  60. data/lib/dynamoid/type_casting.rb +12 -2
  61. data/lib/dynamoid/undumping.rb +8 -0
  62. data/lib/dynamoid/validations.rb +6 -1
  63. data/lib/dynamoid/version.rb +1 -1
  64. metadata +48 -75
  65. data/.coveralls.yml +0 -1
  66. data/.document +0 -5
  67. data/.gitignore +0 -74
  68. data/.rspec +0 -2
  69. data/.rubocop.yml +0 -71
  70. data/.rubocop_todo.yml +0 -55
  71. data/.travis.yml +0 -44
  72. data/Appraisals +0 -22
  73. data/Gemfile +0 -8
  74. data/Rakefile +0 -46
  75. data/Vagrantfile +0 -29
  76. data/docker-compose.yml +0 -7
  77. data/dynamoid.gemspec +0 -57
  78. data/gemfiles/rails_4_2.gemfile +0 -9
  79. data/gemfiles/rails_5_0.gemfile +0 -8
  80. data/gemfiles/rails_5_1.gemfile +0 -8
  81. data/gemfiles/rails_5_2.gemfile +0 -8
  82. data/gemfiles/rails_6_0.gemfile +0 -8
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Dynamoid #:nodoc:
3
+ module Dynamoid
4
4
  # This is the base module for all domain objects that need to be persisted to
5
5
  # the database as documents.
6
6
  module Document
@@ -17,28 +17,19 @@ module Dynamoid #:nodoc:
17
17
  end
18
18
 
19
19
  module ClassMethods
20
- # Set up table options, including naming it whatever you want, setting the id key, and manually overriding read and
21
- # write capacity.
22
- #
23
- # @param [Hash] options options to pass for this table
24
- # @option options [Symbol] :name the name for the table; this still gets namespaced
25
- # @option options [Symbol] :id id column for the table
26
- # @option options [Integer] :read_capacity set the read capacity for the table; does not work on existing tables
27
- # @option options [Integer] :write_capacity set the write capacity for the table; does not work on existing tables
28
- #
29
- # @since 0.4.0
20
+ # @private
30
21
  def table(options = {})
31
22
  self.options = options
32
23
  super if defined? super
33
24
  end
34
25
 
35
26
  def attr_readonly(*read_only_attributes)
36
- ActiveSupport::Deprecation.warn('[Dynamoid] .attr_readonly is deprecated! Call .find instead of')
37
27
  self.read_only_attributes.concat read_only_attributes.map(&:to_s)
38
28
  end
39
29
 
40
- # Returns the read_capacity for this table.
30
+ # Returns the read capacity for this table.
41
31
  #
32
+ # @return [Integer] read capacity units
42
33
  # @since 0.4.0
43
34
  def read_capacity
44
35
  options[:read_capacity] || Dynamoid::Config.read_capacity
@@ -46,25 +37,53 @@ module Dynamoid #:nodoc:
46
37
 
47
38
  # Returns the write_capacity for this table.
48
39
  #
40
+ # @return [Integer] write capacity units
49
41
  # @since 0.4.0
50
42
  def write_capacity
51
43
  options[:write_capacity] || Dynamoid::Config.write_capacity
52
44
  end
53
45
 
46
+ # Returns the billing (capacity) mode for this table.
47
+ #
48
+ # Could be either +provisioned+ or +on_demand+.
49
+ #
50
+ # @return [Symbol]
51
+ def capacity_mode
52
+ options[:capacity_mode] || Dynamoid::Config.capacity_mode
53
+ end
54
+
54
55
  # Returns the field name used to support STI for this table.
56
+ #
57
+ # Default field name is +type+ but it can be overrided in the +table+
58
+ # method call.
59
+ #
60
+ # User.inheritance_field # => :type
55
61
  def inheritance_field
56
62
  options[:inheritance_field] || :type
57
63
  end
58
64
 
59
- # Returns the id field for this class.
65
+ # Returns the hash key field name for this class.
60
66
  #
67
+ # By default +id+ field is used. But it can be overriden in the +table+
68
+ # method call.
69
+ #
70
+ # User.hash_key # => :id
71
+ #
72
+ # @return [Symbol] a hash key name
61
73
  # @since 0.4.0
62
74
  def hash_key
63
75
  options[:key] || :id
64
76
  end
65
77
 
66
- # Returns the number of items for this class.
78
+ # Return the count of items for this class.
79
+ #
80
+ # It returns aproximate value based on DynamoDB statistic. DynamoDB
81
+ # updates it periodicaly so the value can be no accurate.
82
+ #
83
+ # It's a reletivly cheap operation and doesn't read all the items in a
84
+ # table. It makes just one HTTP request to DynamoDB.
67
85
  #
86
+ # @return [Integer] items count in a table
68
87
  # @since 0.6.1
69
88
  def count
70
89
  Dynamoid.adapter.count(table_name)
@@ -72,35 +91,67 @@ module Dynamoid #:nodoc:
72
91
 
73
92
  # Initialize a new object.
74
93
  #
75
- # @param [Hash] attrs Attributes with which to create the object.
94
+ # User.build(name: 'A')
76
95
  #
77
- # @return [Dynamoid::Document] the new document
96
+ # Initialize an object and pass it into a block to set other attributes.
97
+ #
98
+ # User.build(name: 'A') do |u|
99
+ # u.age = 21
100
+ # end
101
+ #
102
+ # The only difference between +build+ and +new+ methods is that +build+
103
+ # supports STI (Single table inheritance) and looks at the inheritance
104
+ # field. So it can build a model of actual class. For instance:
105
+ #
106
+ # class Employee
107
+ # include Dynamoid::Document
78
108
  #
109
+ # field :type
110
+ # field :name
111
+ # end
112
+ #
113
+ # class Manager < Employee
114
+ # end
115
+ #
116
+ # Employee.build(name: 'Alice', type: 'Manager') # => #<Manager:0x00007f945756e3f0 ...>
117
+ #
118
+ # @param attrs [Hash] Attributes with which to create the document
119
+ # @param block [Proc] Block to process a document after initialization
120
+ # @return [Dynamoid::Document] the new document
79
121
  # @since 0.2.0
80
- def build(attrs = {})
81
- choose_right_class(attrs).new(attrs)
122
+ def build(attrs = {}, &block)
123
+ choose_right_class(attrs).new(attrs, &block)
82
124
  end
83
125
 
84
- # Does this object exist?
126
+ # Does this model exist in a table?
127
+ #
128
+ # User.exists?('713') # => true
85
129
  #
86
- # Supports primary key in format that `find` call understands.
87
- # Multiple keys and single compound primary key should be passed only as Array explicitily.
130
+ # If a range key is declared it should be specified in the following way:
88
131
  #
89
- # Supports conditions in format that `where` call understands.
132
+ # User.exists?([['713', 'range-key-value']]) # => true
90
133
  #
91
- # @param [Mixed] id_or_conditions the id of the object or a hash with the options to filter from.
134
+ # It's possible to check existence of several models at once:
92
135
  #
93
- # @return [Boolean] true/false
136
+ # User.exists?(['713', '714', '715'])
94
137
  #
95
- # @example With id
138
+ # Or in case when a range key is declared:
96
139
  #
97
- # Post.exist?(713)
98
- # Post.exist?([713, 210])
140
+ # User.exists?(
141
+ # [
142
+ # ['713', 'range-key-value-1'],
143
+ # ['714', 'range-key-value-2'],
144
+ # ['715', 'range-key-value-3']
145
+ # ]
146
+ # )
99
147
  #
100
- # @example With attributes conditions
148
+ # It's also possible to specify models not with primary key but with
149
+ # conditions on the attributes (in the +where+ method style):
101
150
  #
102
- # Post.exist?(version: 1, 'created_at.gt': Time.now - 1.day)
151
+ # User.exists?(age: 20, 'created_at.gt': Time.now - 1.day)
103
152
  #
153
+ # @param id_or_conditions [String|Array[String]|Array[Array]|Hash] the primary id of the model, a list of primary ids or a hash with the options to filter from.
154
+ # @return [true|false]
104
155
  # @since 0.2.0
105
156
  def exists?(id_or_conditions = {})
106
157
  case id_or_conditions
@@ -115,10 +166,12 @@ module Dynamoid #:nodoc:
115
166
  end
116
167
  end
117
168
 
169
+ # @private
118
170
  def deep_subclasses
119
171
  subclasses + subclasses.map(&:deep_subclasses).flatten
120
172
  end
121
173
 
174
+ # @private
122
175
  def choose_right_class(attrs)
123
176
  attrs[inheritance_field] ? attrs[inheritance_field].constantize : self
124
177
  end
@@ -126,36 +179,50 @@ module Dynamoid #:nodoc:
126
179
 
127
180
  # Initialize a new object.
128
181
  #
129
- # @param [Hash] attrs Attributes with which to create the object.
182
+ # User.new(name: 'A')
130
183
  #
184
+ # Initialize an object and pass it into a block to set other attributes.
185
+ #
186
+ # User.new(name: 'A') do |u|
187
+ # u.age = 21
188
+ # end
189
+ #
190
+ # @param attrs [Hash] Attributes with which to create the document
191
+ # @param block [Proc] Block to process a document after initialization
131
192
  # @return [Dynamoid::Document] the new document
132
193
  #
133
194
  # @since 0.2.0
134
- def initialize(attrs = {})
195
+ def initialize(attrs = {}, &block)
135
196
  run_callbacks :initialize do
136
197
  @new_record = true
137
198
  @attributes ||= {}
138
199
  @associations ||= {}
139
200
  @attributes_before_type_cast ||= {}
140
201
 
141
- attrs_with_defaults = self.class.attributes.reduce({}) do |res, (attribute, options)|
202
+ attrs_with_defaults = self.class.attributes.each_with_object({}) do |(attribute, options), res|
142
203
  if attrs.key?(attribute)
143
- res.merge(attribute => attrs[attribute])
204
+ res[attribute] = attrs[attribute]
144
205
  elsif options.key?(:default)
145
- res.merge(attribute => evaluate_default_value(options[:default]))
146
- else
147
- res
206
+ res[attribute] = evaluate_default_value(options[:default])
148
207
  end
149
208
  end
150
209
 
151
210
  attrs_virtual = attrs.slice(*(attrs.keys - self.class.attributes.keys))
152
211
 
153
212
  load(attrs_with_defaults.merge(attrs_virtual))
213
+
214
+ if block
215
+ block.call(self)
216
+ end
154
217
  end
155
218
  end
156
219
 
157
- # An object is equal to another object if their ids are equal.
220
+ # Check equality of two models.
158
221
  #
222
+ # A model is equal to another model only if their primary keys (hash key
223
+ # and optionaly range key) are equal.
224
+ #
225
+ # @return [true|false]
159
226
  # @since 0.2.0
160
227
  def ==(other)
161
228
  if self.class.identity_map_on?
@@ -167,36 +234,54 @@ module Dynamoid #:nodoc:
167
234
  end
168
235
  end
169
236
 
237
+ # Check equality of two models.
238
+ #
239
+ # Works exactly like +==+ does.
240
+ #
241
+ # @return [true|false]
170
242
  def eql?(other)
171
243
  self == other
172
244
  end
173
245
 
246
+ # Generate an Integer hash value for this model.
247
+ #
248
+ # Hash value is based on primary key. So models can be used safely as a
249
+ # +Hash+ keys.
250
+ #
251
+ # @return [Integer]
174
252
  def hash
175
253
  hash_key.hash ^ range_value.hash
176
254
  end
177
255
 
178
- # Return an object's hash key, regardless of what it might be called to the object.
256
+ # Return a model's hash key value.
179
257
  #
180
258
  # @since 0.4.0
181
259
  def hash_key
182
- send(self.class.hash_key)
260
+ self[self.class.hash_key.to_sym]
183
261
  end
184
262
 
185
- # Assign an object's hash key, regardless of what it might be called to the object.
263
+ # Assign a model's hash key value, regardless of what it might be called to
264
+ # the object.
186
265
  #
187
266
  # @since 0.4.0
188
267
  def hash_key=(value)
189
- send("#{self.class.hash_key}=", value)
268
+ self[self.class.hash_key.to_sym] = value
190
269
  end
191
270
 
271
+ # Return a model's range key value.
272
+ #
273
+ # Returns +nil+ if a range key isn't declared for a model.
192
274
  def range_value
193
- if range_key = self.class.range_key
194
- send(range_key)
275
+ if self.class.range_key
276
+ self[self.class.range_key.to_sym]
195
277
  end
196
278
  end
197
279
 
280
+ # Assign a model's range key value.
198
281
  def range_value=(value)
199
- send("#{self.class.range_key}=", value)
282
+ if self.class.range_key
283
+ self[self.class.range_key.to_sym] = value
284
+ end
200
285
  end
201
286
 
202
287
  private
@@ -208,7 +293,7 @@ module Dynamoid #:nodoc:
208
293
  # Evaluates the default value given, this is used by undump
209
294
  # when determining the value of the default given for a field options.
210
295
  #
211
- # @param [Object] :value the attribute's default value
296
+ # @param val [Object] the attribute's default value
212
297
  def evaluate_default_value(val)
213
298
  if val.respond_to?(:call)
214
299
  val.call
@@ -220,3 +305,5 @@ module Dynamoid #:nodoc:
220
305
  end
221
306
  end
222
307
  end
308
+
309
+ ActiveSupport.run_load_hooks(:dynamoid, Dynamoid::Document)
@@ -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
@@ -75,5 +75,7 @@ module Dynamoid
75
75
  class InvalidQuery < Error; end
76
76
 
77
77
  class UnsupportedKeyType < Error; end
78
+
79
+ class UnknownAttribute < Error; end
78
80
  end
79
81
  end
@@ -1,11 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Dynamoid #:nodoc:
3
+ require 'dynamoid/fields/declare'
4
+
5
+ module Dynamoid
4
6
  # All fields on a Dynamoid::Document must be explicitly defined -- if you have fields in the database that are not
5
7
  # specified with field, then they will be ignored.
6
8
  module Fields
7
9
  extend ActiveSupport::Concern
8
10
 
11
+ # @private
9
12
  # Types allowed in indexes:
10
13
  PERMITTED_KEY_TYPES = %i[
11
14
  number
@@ -21,8 +24,11 @@ module Dynamoid #:nodoc:
21
24
  class_attribute :range_key
22
25
 
23
26
  self.attributes = {}
24
- field :created_at, :datetime
25
- field :updated_at, :datetime
27
+
28
+ # Timestamp fields could be disabled later in `table` method call.
29
+ # So let's declare them here and remove them later if it will be necessary
30
+ field :created_at, :datetime if Dynamoid::Config.timestamps
31
+ field :updated_at, :datetime if Dynamoid::Config.timestamps
26
32
 
27
33
  field :id # Default primary key
28
34
  end
@@ -30,59 +36,214 @@ module Dynamoid #:nodoc:
30
36
  module ClassMethods
31
37
  # Specify a field for a document.
32
38
  #
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,
39
+ # class User
40
+ # include Dynamoid::Document
41
+ #
42
+ # field :last_name
43
+ # field :age, :integer
44
+ # field :last_sign_in, :datetime
45
+ # end
46
+ #
47
+ # Its type determines how it is coerced when read in and out of the
48
+ # data store. You can specify +string+, +integer+, +number+, +set+, +array+,
49
+ # +map+, +datetime+, +date+, +serialized+, +raw+, +boolean+ and +binary+
35
50
  # or specify a class that defines a serialization strategy.
36
51
  #
52
+ # By default field type is +string+.
53
+ #
54
+ # Set can store elements of the same type only (it's a limitation of
55
+ # DynamoDB itself). If a set should store elements only of some particular
56
+ # type then +of+ option should be specified:
57
+ #
58
+ # field :hobbies, :set, of: :string
59
+ #
60
+ # Only +string+, +integer+, +number+, +date+, +datetime+ and +serialized+
61
+ # element types are supported.
62
+ #
63
+ # Element type can have own options - they should be specified in the
64
+ # form of +Hash+:
65
+ #
66
+ # field :hobbies, :set, of: { serialized: { serializer: JSON } }
67
+ #
68
+ # Array can contain element of different types but if supports the same
69
+ # +of+ option to convert all the provided elements to the declared type.
70
+ #
71
+ # field :rates, :array, of: :number
72
+ #
73
+ # By default +date+ and +datetime+ fields are stored as integer values.
74
+ # The format can be changed to string with option +store_as_string+:
75
+ #
76
+ # field :published_on, :datetime, store_as_string: true
77
+ #
78
+ # Boolean field by default is stored as a string +t+ or +f+. But DynamoDB
79
+ # supports boolean type natively. In order to switch to the native
80
+ # boolean type an option +store_as_native_boolean+ should be specified:
81
+ #
82
+ # field :active, :boolean, store_as_native_boolean: true
83
+ #
84
+ # If you specify the +serialized+ type a value will be serialized to
85
+ # string in Yaml format by default. Custom way to serialize value to
86
+ # string can be specified with +serializer+ option. Custom serializer
87
+ # should have +dump+ and +load+ methods.
88
+ #
37
89
  # 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.
90
+ # +dynamoid_dump+ method and load using +dynamoid_load+ method.
91
+ #
92
+ # Default field type is +string+.
93
+ #
94
+ # A field can have a default value. It's assigned at initializing a model
95
+ # if no value is specified:
39
96
  #
40
- # Default field type is :string.
97
+ # field :age, :integer, default: 1
41
98
  #
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
99
+ # If a defautl value should be recalculated every time it can be
100
+ # specified as a callable object (it should implement a +call+ method
101
+ # e.g. +Proc+ object):
102
+ #
103
+ # field :date_of_birth, :date, default: -> { Date.today }
104
+ #
105
+ # For every field Dynamoid creates several methods:
106
+ #
107
+ # * getter
108
+ # * setter
109
+ # * predicate +<name>?+ to check whether a value set
110
+ # * +<name>_before_type_cast?+ to get an original field value before it was type casted
111
+ #
112
+ # It works in the following way:
113
+ #
114
+ # class User
115
+ # include Dynamoid::Document
116
+ #
117
+ # field :age, :integer
118
+ # end
119
+ #
120
+ # user = User.new
121
+ # user.age # => nil
122
+ # user.age? # => false
123
+ #
124
+ # user.age = 20
125
+ # user.age? # => true
126
+ #
127
+ # user.age = '21'
128
+ # user.age # => 21 - integer
129
+ # user.age_before_type_cast # => '21' - string
130
+ #
131
+ # There is also an option +alias+ which allows to use another name for a
132
+ # field:
133
+ #
134
+ # class User
135
+ # include Dynamoid::Document
136
+ #
137
+ # field :firstName, :string, alias: :first_name
138
+ # end
139
+ #
140
+ # user = User.new(firstName: 'Michael')
141
+ # user.firstName # Michael
142
+ # user.first_name # Michael
143
+ #
144
+ # @param name [Symbol] name of the field
145
+ # @param type [Symbol] type of the field (optional)
146
+ # @param options [Hash] any additional options for the field type (optional)
45
147
  #
46
148
  # @since 0.2.0
47
149
  def field(name, type = :string, options = {})
48
- named = name.to_s
49
150
  if type == :float
50
151
  Dynamoid.logger.warn("Field type :float, which you declared for '#{name}', is deprecated in favor of :number.")
51
152
  type = :number
52
153
  end
53
- self.attributes = attributes.merge(name => { type: type }.merge(options))
54
-
55
- define_attribute_method(name) # Dirty API
56
154
 
57
- generated_methods.module_eval do
58
- define_method(named) { read_attribute(named) }
59
- define_method("#{named}?") do
60
- value = read_attribute(named)
61
- case value
62
- when true then true
63
- when false, nil then false
64
- else
65
- !value.nil?
66
- end
67
- end
68
- define_method("#{named}=") { |value| write_attribute(named, value) }
69
- define_method("#{named}_before_type_cast") { read_attribute_before_type_cast(named) }
70
- end
155
+ Dynamoid::Fields::Declare.new(self, name, type, options).call
71
156
  end
72
157
 
158
+ # Declare a table range key.
159
+ #
160
+ # class User
161
+ # include Dynamoid::Document
162
+ #
163
+ # range :last_name
164
+ # end
165
+ #
166
+ # By default a range key is a string. In order to use any other type it
167
+ # should be specified as a second argument:
168
+ #
169
+ # range :age, :integer
170
+ #
171
+ # Type options can be specified as well:
172
+ #
173
+ # range :date_of_birth, :date, store_as_string: true
174
+ #
175
+ # @param name [Symbol] a range key attribute name
176
+ # @param type [Symbol] a range key type (optional)
177
+ # @param options [Symbol] type options (optional)
73
178
  def range(name, type = :string, options = {})
74
179
  field(name, type, options)
75
180
  self.range_key = name
76
181
  end
77
182
 
78
- def table(_options)
183
+ # Set table level properties.
184
+ #
185
+ # There are some sensible defaults:
186
+ #
187
+ # * table name is based on a model class e.g. +users+ for +User+ class
188
+ # * hash key name - +id+ by default
189
+ # * hash key type - +string+ by default
190
+ # * generating timestamp fields +created_at+ and +updated_at+
191
+ # * billing mode and read/write capacity units
192
+ #
193
+ # The +table+ method can be used to override the defaults:
194
+ #
195
+ # class User
196
+ # include Dynamoid::Document
197
+ #
198
+ # table name: :customers, key: :uuid
199
+ # end
200
+ #
201
+ # The hash key field is declared by default and a type is a string. If
202
+ # another type is needed the field should be declared explicitly:
203
+ #
204
+ # class User
205
+ # include Dynamoid::Document
206
+ #
207
+ # field :id, :integer
208
+ # end
209
+ #
210
+ # @param options [Hash] options to override default table settings
211
+ # @option options [Symbol] :name name of a table
212
+ # @option options [Symbol] :key name of a hash key attribute
213
+ # @option options [Symbol] :inheritance_field name of an attribute used for STI
214
+ # @option options [Symbol] :capacity_mode table billing mode - either +provisioned+ or +on_demand+
215
+ # @option options [Integer] :write_capacity table write capacity units
216
+ # @option options [Integer] :read_capacity table read capacity units
217
+ # @option options [true|false] :timestamps whether generate +created_at+ and +updated_at+ fields or not
218
+ # @option options [Hash] :expires set up a table TTL and should have following structure +{ field: <attriubute name>, after: <seconds> }+
219
+ #
220
+ # @since 0.4.0
221
+ def table(options)
79
222
  # a default 'id' column is created when Dynamoid::Document is included
80
223
  unless attributes.key? hash_key
81
224
  remove_field :id
82
225
  field(hash_key)
83
226
  end
227
+
228
+ if options[:timestamps] && !Dynamoid::Config.timestamps
229
+ # Timestamp fields weren't declared in `included` hook because they
230
+ # are disabled globaly
231
+ field :created_at, :datetime
232
+ field :updated_at, :datetime
233
+ elsif options[:timestamps] == false && Dynamoid::Config.timestamps
234
+ # Timestamp fields were declared in `included` hook but they are
235
+ # disabled for a table
236
+ remove_field :created_at
237
+ remove_field :updated_at
238
+ end
84
239
  end
85
240
 
241
+ # Remove a field declaration
242
+ #
243
+ # Removes a field from the list of fields and removes all te generated
244
+ # for a field methods.
245
+ #
246
+ # @param field [Symbol] a field name
86
247
  def remove_field(field)
87
248
  field = field.to_sym
88
249
  attributes.delete(field) || raise('No such field')
@@ -99,8 +260,12 @@ module Dynamoid #:nodoc:
99
260
  end
100
261
  end
101
262
 
102
- private
263
+ # @private
264
+ def timestamps_enabled?
265
+ options[:timestamps] || (options[:timestamps].nil? && Dynamoid::Config.timestamps)
266
+ end
103
267
 
268
+ # @private
104
269
  def generated_methods
105
270
  @generated_methods ||= begin
106
271
  Module.new.tap do |mod|
@@ -114,15 +279,25 @@ module Dynamoid #:nodoc:
114
279
  attr_accessor :attributes
115
280
  alias raw_attributes attributes
116
281
 
117
- # Write an attribute on the object. Also marks the previous value as dirty.
282
+ # Write an attribute on the object.
283
+ #
284
+ # user.age = 20
285
+ # user.write_attribute(:age, 21)
286
+ # user.age # => 21
118
287
  #
119
- # @param [Symbol] name the name of the field
120
- # @param [Object] value the value to assign to that field
288
+ # Also marks the previous value as dirty.
289
+ #
290
+ # @param name [Symbol] the name of the field
291
+ # @param value [Object] the value to assign to that field
121
292
  #
122
293
  # @since 0.2.0
123
294
  def write_attribute(name, value)
124
295
  name = name.to_sym
125
296
 
297
+ unless attribute_is_present_on_model?(name)
298
+ raise Dynamoid::Errors::UnknownAttribute.new("Attribute #{name} is not part of the model")
299
+ end
300
+
126
301
  if association = @associations[name]
127
302
  association.reset
128
303
  end
@@ -138,22 +313,40 @@ module Dynamoid #:nodoc:
138
313
 
139
314
  # Read an attribute from an object.
140
315
  #
141
- # @param [Symbol] name the name of the field
316
+ # user.age = 20
317
+ # user.read_attribute(:age) # => 20
142
318
  #
319
+ # @param name [Symbol] the name of the field
320
+ # @return attribute value
143
321
  # @since 0.2.0
144
322
  def read_attribute(name)
145
323
  attributes[name.to_sym]
146
324
  end
147
325
  alias [] read_attribute
148
326
 
149
- # Returns a hash of attributes before typecasting
327
+ # Return attributes values before type casting.
328
+ #
329
+ # user = User.new
330
+ # user.age = '21'
331
+ # user.age # => 21
332
+ #
333
+ # user.attributes_before_type_cast # => { age: '21' }
334
+ #
335
+ # @return [Hash] original attribute values
150
336
  def attributes_before_type_cast
151
337
  @attributes_before_type_cast
152
338
  end
153
339
 
154
- # Returns the value of the attribute identified by name before typecasting
340
+ # Return the value of the attribute identified by name before type casting.
341
+ #
342
+ # user = User.new
343
+ # user.age = '21'
344
+ # user.age # => 21
155
345
  #
156
- # @param [Symbol] attribute name
346
+ # user.read_attribute_before_type_cast(:age) # => '21'
347
+ #
348
+ # @param name [Symbol] attribute name
349
+ # @return original attribute value
157
350
  def read_attribute_before_type_cast(name)
158
351
  return nil unless name.respond_to?(:to_sym)
159
352
 
@@ -166,18 +359,32 @@ module Dynamoid #:nodoc:
166
359
  #
167
360
  # @since 0.2.0
168
361
  def set_created_at
169
- self.created_at ||= DateTime.now.in_time_zone(Time.zone) if Dynamoid::Config.timestamps
362
+ self.created_at ||= DateTime.now.in_time_zone(Time.zone) if self.class.timestamps_enabled?
170
363
  end
171
364
 
172
365
  # Automatically called during the save callback to set the updated_at time.
173
366
  #
174
367
  # @since 0.2.0
175
368
  def set_updated_at
176
- if Dynamoid::Config.timestamps && !updated_at_changed?
369
+ # @_touch_record=false means explicit disabling
370
+ if self.class.timestamps_enabled? && !updated_at_changed? && @_touch_record != false
177
371
  self.updated_at = DateTime.now.in_time_zone(Time.zone)
178
372
  end
179
373
  end
180
374
 
375
+ def set_expires_field
376
+ options = self.class.options[:expires]
377
+
378
+ if options.present?
379
+ name = options[:field]
380
+ seconds = options[:after]
381
+
382
+ if self[name].blank?
383
+ send("#{name}=", Time.now.to_i + seconds)
384
+ end
385
+ end
386
+ end
387
+
181
388
  def set_inheritance_field
182
389
  # actually it does only following logic:
183
390
  # self.type ||= self.class.name if self.class.attributes[:type]
@@ -187,5 +394,10 @@ module Dynamoid #:nodoc:
187
394
  send("#{type}=", self.class.name)
188
395
  end
189
396
  end
397
+
398
+ def attribute_is_present_on_model?(attribute_name)
399
+ setter = "#{attribute_name}=".to_sym
400
+ respond_to?(setter)
401
+ end
190
402
  end
191
403
  end