dynamoid 3.3.0 → 3.7.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +104 -1
- data/README.md +146 -52
- data/lib/dynamoid.rb +1 -0
- data/lib/dynamoid/adapter.rb +20 -7
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3.rb +70 -37
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/batch_get_item.rb +3 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/create_table.rb +20 -12
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/item_updater.rb +5 -4
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/backoff.rb +2 -2
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/limit.rb +2 -3
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/middleware/start_key.rb +2 -2
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/query.rb +4 -2
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/scan.rb +4 -2
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/table.rb +1 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v3/until_past_table_status.rb +2 -1
- data/lib/dynamoid/application_time_zone.rb +1 -0
- data/lib/dynamoid/associations.rb +182 -19
- data/lib/dynamoid/associations/association.rb +10 -2
- data/lib/dynamoid/associations/belongs_to.rb +2 -1
- data/lib/dynamoid/associations/has_and_belongs_to_many.rb +2 -1
- data/lib/dynamoid/associations/has_many.rb +2 -1
- data/lib/dynamoid/associations/has_one.rb +2 -1
- data/lib/dynamoid/associations/many_association.rb +68 -23
- data/lib/dynamoid/associations/single_association.rb +31 -4
- data/lib/dynamoid/components.rb +2 -0
- data/lib/dynamoid/config.rb +15 -3
- data/lib/dynamoid/config/backoff_strategies/constant_backoff.rb +1 -0
- data/lib/dynamoid/config/backoff_strategies/exponential_backoff.rb +1 -0
- data/lib/dynamoid/config/options.rb +1 -0
- data/lib/dynamoid/criteria.rb +9 -1
- data/lib/dynamoid/criteria/chain.rb +421 -46
- data/lib/dynamoid/criteria/ignored_conditions_detector.rb +3 -3
- data/lib/dynamoid/criteria/key_fields_detector.rb +31 -10
- data/lib/dynamoid/criteria/nonexistent_fields_detector.rb +3 -2
- data/lib/dynamoid/criteria/overwritten_conditions_detector.rb +1 -1
- data/lib/dynamoid/dirty.rb +119 -64
- data/lib/dynamoid/document.rb +133 -46
- data/lib/dynamoid/dumping.rb +9 -0
- data/lib/dynamoid/dynamodb_time_zone.rb +1 -0
- data/lib/dynamoid/errors.rb +2 -0
- data/lib/dynamoid/fields.rb +251 -39
- data/lib/dynamoid/fields/declare.rb +86 -0
- data/lib/dynamoid/finders.rb +69 -32
- data/lib/dynamoid/identity_map.rb +6 -0
- data/lib/dynamoid/indexes.rb +86 -17
- data/lib/dynamoid/loadable.rb +2 -2
- data/lib/dynamoid/log/formatter.rb +26 -0
- data/lib/dynamoid/middleware/identity_map.rb +1 -0
- data/lib/dynamoid/persistence.rb +502 -104
- data/lib/dynamoid/persistence/import.rb +2 -1
- data/lib/dynamoid/persistence/save.rb +1 -0
- data/lib/dynamoid/persistence/update_fields.rb +5 -2
- data/lib/dynamoid/persistence/update_validations.rb +18 -0
- data/lib/dynamoid/persistence/upsert.rb +5 -3
- data/lib/dynamoid/primary_key_type_mapping.rb +1 -0
- data/lib/dynamoid/railtie.rb +1 -0
- data/lib/dynamoid/tasks.rb +3 -1
- data/lib/dynamoid/tasks/database.rb +1 -0
- data/lib/dynamoid/type_casting.rb +12 -2
- data/lib/dynamoid/undumping.rb +8 -0
- data/lib/dynamoid/validations.rb +6 -1
- data/lib/dynamoid/version.rb +1 -1
- metadata +48 -75
- data/.coveralls.yml +0 -1
- data/.document +0 -5
- data/.gitignore +0 -74
- data/.rspec +0 -2
- data/.rubocop.yml +0 -71
- data/.rubocop_todo.yml +0 -55
- data/.travis.yml +0 -44
- data/Appraisals +0 -22
- data/Gemfile +0 -8
- data/Rakefile +0 -46
- data/Vagrantfile +0 -29
- data/docker-compose.yml +0 -7
- data/dynamoid.gemspec +0 -57
- data/gemfiles/rails_4_2.gemfile +0 -9
- data/gemfiles/rails_5_0.gemfile +0 -8
- data/gemfiles/rails_5_1.gemfile +0 -8
- data/gemfiles/rails_5_2.gemfile +0 -8
- data/gemfiles/rails_6_0.gemfile +0 -8
data/lib/dynamoid/document.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module Dynamoid
|
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
|
-
#
|
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
|
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
|
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
|
-
#
|
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
|
-
#
|
94
|
+
# User.build(name: 'A')
|
76
95
|
#
|
77
|
-
#
|
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
|
126
|
+
# Does this model exist in a table?
|
127
|
+
#
|
128
|
+
# User.exists?('713') # => true
|
85
129
|
#
|
86
|
-
#
|
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
|
-
#
|
132
|
+
# User.exists?([['713', 'range-key-value']]) # => true
|
90
133
|
#
|
91
|
-
#
|
134
|
+
# It's possible to check existence of several models at once:
|
92
135
|
#
|
93
|
-
#
|
136
|
+
# User.exists?(['713', '714', '715'])
|
94
137
|
#
|
95
|
-
#
|
138
|
+
# Or in case when a range key is declared:
|
96
139
|
#
|
97
|
-
#
|
98
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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.
|
202
|
+
attrs_with_defaults = self.class.attributes.each_with_object({}) do |(attribute, options), res|
|
142
203
|
if attrs.key?(attribute)
|
143
|
-
res
|
204
|
+
res[attribute] = attrs[attribute]
|
144
205
|
elsif options.key?(:default)
|
145
|
-
res
|
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
|
-
#
|
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
|
256
|
+
# Return a model's hash key value.
|
179
257
|
#
|
180
258
|
# @since 0.4.0
|
181
259
|
def hash_key
|
182
|
-
|
260
|
+
self[self.class.hash_key.to_sym]
|
183
261
|
end
|
184
262
|
|
185
|
-
# Assign
|
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
|
-
|
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
|
194
|
-
|
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
|
-
|
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]
|
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)
|
data/lib/dynamoid/dumping.rb
CHANGED
@@ -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)
|
data/lib/dynamoid/errors.rb
CHANGED
data/lib/dynamoid/fields.rb
CHANGED
@@ -1,11 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
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
|
-
|
25
|
-
|
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
|
-
#
|
34
|
-
#
|
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
|
-
#
|
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
|
-
#
|
97
|
+
# field :age, :integer, default: 1
|
41
98
|
#
|
42
|
-
#
|
43
|
-
#
|
44
|
-
#
|
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
|
-
|
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
|
-
|
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.
|
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
|
-
#
|
120
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
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
|
-
|
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
|