dynamoid 2.2.0 → 3.0.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/.rubocop.yml +53 -0
- data/.rubocop_todo.yml +55 -0
- data/.travis.yml +5 -27
- data/Appraisals +17 -15
- data/CHANGELOG.md +26 -3
- data/Gemfile +4 -2
- data/README.md +95 -77
- data/Rakefile +17 -17
- data/Vagrantfile +5 -3
- data/dynamoid.gemspec +39 -45
- data/gemfiles/rails_4_2.gemfile +7 -5
- data/gemfiles/rails_5_0.gemfile +6 -4
- data/gemfiles/rails_5_1.gemfile +6 -4
- data/gemfiles/rails_5_2.gemfile +6 -4
- data/lib/dynamoid.rb +11 -4
- data/lib/dynamoid/adapter.rb +21 -27
- data/lib/dynamoid/adapter_plugin/{aws_sdk_v2.rb → aws_sdk_v3.rb} +118 -113
- data/lib/dynamoid/application_time_zone.rb +27 -0
- data/lib/dynamoid/associations.rb +3 -6
- data/lib/dynamoid/associations/association.rb +3 -6
- data/lib/dynamoid/associations/belongs_to.rb +4 -5
- data/lib/dynamoid/associations/has_and_belongs_to_many.rb +2 -3
- data/lib/dynamoid/associations/has_many.rb +2 -3
- data/lib/dynamoid/associations/has_one.rb +2 -3
- data/lib/dynamoid/associations/many_association.rb +8 -9
- data/lib/dynamoid/associations/single_association.rb +3 -3
- data/lib/dynamoid/components.rb +2 -2
- data/lib/dynamoid/config.rb +9 -5
- data/lib/dynamoid/config/backoff_strategies/constant_backoff.rb +4 -2
- data/lib/dynamoid/config/backoff_strategies/exponential_backoff.rb +3 -1
- data/lib/dynamoid/config/options.rb +4 -4
- data/lib/dynamoid/criteria.rb +3 -5
- data/lib/dynamoid/criteria/chain.rb +42 -49
- data/lib/dynamoid/dirty.rb +5 -4
- data/lib/dynamoid/document.rb +142 -36
- data/lib/dynamoid/dumping.rb +167 -0
- data/lib/dynamoid/dynamodb_time_zone.rb +16 -0
- data/lib/dynamoid/errors.rb +7 -6
- data/lib/dynamoid/fields.rb +24 -23
- data/lib/dynamoid/finders.rb +101 -59
- data/lib/dynamoid/identity_map.rb +5 -11
- data/lib/dynamoid/indexes.rb +45 -46
- data/lib/dynamoid/middleware/identity_map.rb +2 -0
- data/lib/dynamoid/persistence.rb +67 -307
- data/lib/dynamoid/primary_key_type_mapping.rb +34 -0
- data/lib/dynamoid/railtie.rb +3 -1
- data/lib/dynamoid/tasks/database.rake +11 -11
- data/lib/dynamoid/tasks/database.rb +4 -3
- data/lib/dynamoid/type_casting.rb +193 -0
- data/lib/dynamoid/undumping.rb +188 -0
- data/lib/dynamoid/validations.rb +4 -7
- data/lib/dynamoid/version.rb +3 -1
- metadata +59 -53
- data/gemfiles/rails_4_0.gemfile +0 -9
- data/gemfiles/rails_4_1.gemfile +0 -9
data/lib/dynamoid/indexes.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Dynamoid
|
2
4
|
module Indexes
|
3
5
|
extend ActiveSupport::Concern
|
@@ -27,20 +29,18 @@ module Dynamoid
|
|
27
29
|
# index; does not work on existing indexes.
|
28
30
|
# @option options [Integer] :write_capacity set the write capacity for
|
29
31
|
# the index; does not work on existing indexes.
|
30
|
-
def global_secondary_index(options={})
|
32
|
+
def global_secondary_index(options = {})
|
31
33
|
unless options.present?
|
32
|
-
raise Dynamoid::Errors::InvalidIndex
|
34
|
+
raise Dynamoid::Errors::InvalidIndex, 'empty index definition'
|
33
35
|
end
|
34
36
|
|
35
37
|
unless options[:hash_key].present?
|
36
|
-
raise Dynamoid::Errors::InvalidIndex
|
37
|
-
'A global secondary index requires a :hash_key to be specified'
|
38
|
-
)
|
38
|
+
raise Dynamoid::Errors::InvalidIndex, 'A global secondary index requires a :hash_key to be specified'
|
39
39
|
end
|
40
40
|
|
41
41
|
index_opts = {
|
42
|
-
|
43
|
-
|
42
|
+
read_capacity: Dynamoid::Config.read_capacity,
|
43
|
+
write_capacity: Dynamoid::Config.write_capacity
|
44
44
|
}.merge(options)
|
45
45
|
|
46
46
|
index_opts[:dynamoid_class] = self
|
@@ -48,7 +48,7 @@ module Dynamoid
|
|
48
48
|
|
49
49
|
index = Dynamoid::Indexes::Index.new(index_opts)
|
50
50
|
gsi_key = index_key(options[:hash_key], options[:range_key])
|
51
|
-
|
51
|
+
global_secondary_indexes[gsi_key] = index
|
52
52
|
self
|
53
53
|
end
|
54
54
|
|
@@ -63,38 +63,39 @@ module Dynamoid
|
|
63
63
|
# attributes to project for this index. Can be :keys_only, :all
|
64
64
|
# or an array of included fields. If not specified, defaults to
|
65
65
|
# :keys_only.
|
66
|
-
def local_secondary_index(options={})
|
66
|
+
def local_secondary_index(options = {})
|
67
67
|
unless options.present?
|
68
|
-
raise Dynamoid::Errors::InvalidIndex
|
68
|
+
raise Dynamoid::Errors::InvalidIndex, 'empty index definition'
|
69
69
|
end
|
70
70
|
|
71
|
-
primary_hash_key =
|
72
|
-
primary_range_key =
|
71
|
+
primary_hash_key = hash_key
|
72
|
+
primary_range_key = range_key
|
73
73
|
index_range_key = options[:range_key]
|
74
74
|
|
75
75
|
unless index_range_key.present?
|
76
|
-
raise Dynamoid::Errors::InvalidIndex
|
77
|
-
'requires a :range_key to be specified'
|
76
|
+
raise Dynamoid::Errors::InvalidIndex, 'A local secondary index '\
|
77
|
+
'requires a :range_key to be specified'
|
78
78
|
end
|
79
79
|
|
80
80
|
if primary_range_key.present? && index_range_key == primary_range_key
|
81
|
-
raise Dynamoid::Errors::InvalidIndex
|
82
|
-
' must use a different :range_key than the primary key'
|
81
|
+
raise Dynamoid::Errors::InvalidIndex, 'A local secondary index'\
|
82
|
+
' must use a different :range_key than the primary key'
|
83
83
|
end
|
84
84
|
|
85
85
|
index_opts = options.merge(
|
86
86
|
dynamoid_class: self,
|
87
87
|
type: :local_secondary,
|
88
|
-
hash_key: primary_hash_key
|
88
|
+
hash_key: primary_hash_key
|
89
|
+
)
|
89
90
|
|
90
91
|
index = Dynamoid::Indexes::Index.new(index_opts)
|
91
92
|
key = index_key(primary_hash_key, index_range_key)
|
92
|
-
|
93
|
+
local_secondary_indexes[key] = index
|
93
94
|
self
|
94
95
|
end
|
95
96
|
|
96
|
-
def find_index(hash, range=nil)
|
97
|
-
index =
|
97
|
+
def find_index(hash, range = nil)
|
98
|
+
index = indexes[index_key(hash, range)]
|
98
99
|
index
|
99
100
|
end
|
100
101
|
|
@@ -105,8 +106,8 @@ module Dynamoid
|
|
105
106
|
# @param [Symbol] range range key name.
|
106
107
|
# @return [Boolean] true iff provided keys correspond to a local
|
107
108
|
# secondary index.
|
108
|
-
def is_local_secondary_index?(hash, range=nil)
|
109
|
-
|
109
|
+
def is_local_secondary_index?(hash, range = nil)
|
110
|
+
local_secondary_indexes[index_key(hash, range)].present?
|
110
111
|
end
|
111
112
|
|
112
113
|
# Returns true iff the provided hash[,range] key combo is a global
|
@@ -116,8 +117,8 @@ module Dynamoid
|
|
116
117
|
# @param [Symbol] range range key name.
|
117
118
|
# @return [Boolean] true iff provided keys correspond to a global
|
118
119
|
# secondary index.
|
119
|
-
def is_global_secondary_index?(hash, range=nil)
|
120
|
-
|
120
|
+
def is_global_secondary_index?(hash, range = nil)
|
121
|
+
global_secondary_indexes[index_key(hash, range)].present?
|
121
122
|
end
|
122
123
|
|
123
124
|
# Generates a convenient lookup key name for a hash/range index.
|
@@ -126,11 +127,9 @@ module Dynamoid
|
|
126
127
|
# @param [Symbol] hash hash key name.
|
127
128
|
# @param [Symbol] range range key name.
|
128
129
|
# @return [String] returns "hash" if hash only, "hash_range" otherwise.
|
129
|
-
def index_key(hash, range=nil)
|
130
|
+
def index_key(hash, range = nil)
|
130
131
|
name = hash.to_s
|
131
|
-
if range.present?
|
132
|
-
name += "_#{range.to_s}"
|
133
|
-
end
|
132
|
+
name += "_#{range}" if range.present?
|
134
133
|
name
|
135
134
|
end
|
136
135
|
|
@@ -139,8 +138,8 @@ module Dynamoid
|
|
139
138
|
# @param [Symbol] hash hash key name.
|
140
139
|
# @param [Symbol] range range key name.
|
141
140
|
# @return [String] index name of the form "table_name_index_index_key".
|
142
|
-
def index_name(hash, range=nil)
|
143
|
-
"#{
|
141
|
+
def index_name(hash, range = nil)
|
142
|
+
"#{table_name}_index_#{index_key(hash, range)}"
|
144
143
|
end
|
145
144
|
|
146
145
|
# Convenience method to return all indexes on the table.
|
@@ -148,11 +147,11 @@ module Dynamoid
|
|
148
147
|
# @return [Hash<String, Object>] the combined hash of global and local
|
149
148
|
# secondary indexes.
|
150
149
|
def indexes
|
151
|
-
|
150
|
+
local_secondary_indexes.merge(global_secondary_indexes)
|
152
151
|
end
|
153
152
|
|
154
153
|
def indexed_hash_keys
|
155
|
-
|
154
|
+
global_secondary_indexes.map do |_name, index|
|
156
155
|
index.hash_key.to_s
|
157
156
|
end
|
158
157
|
end
|
@@ -162,12 +161,12 @@ module Dynamoid
|
|
162
161
|
class Index
|
163
162
|
include ActiveModel::Validations
|
164
163
|
|
165
|
-
PROJECTION_TYPES = [
|
164
|
+
PROJECTION_TYPES = %i[keys_only all].to_set
|
166
165
|
DEFAULT_PROJECTION_TYPE = :keys_only
|
167
166
|
|
168
167
|
attr_accessor :name, :dynamoid_class, :type, :hash_key, :range_key,
|
169
|
-
|
170
|
-
|
168
|
+
:hash_key_schema, :range_key_schema, :projected_attributes,
|
169
|
+
:read_capacity, :write_capacity
|
171
170
|
|
172
171
|
validate do
|
173
172
|
validate_index_type
|
@@ -176,9 +175,9 @@ module Dynamoid
|
|
176
175
|
validate_projected_attributes
|
177
176
|
end
|
178
177
|
|
179
|
-
def initialize(attrs={})
|
178
|
+
def initialize(attrs = {})
|
180
179
|
unless attrs[:dynamoid_class].present?
|
181
|
-
raise Dynamoid::Errors::InvalidIndex
|
180
|
+
raise Dynamoid::Errors::InvalidIndex, ':dynamoid_class is required'
|
182
181
|
end
|
183
182
|
|
184
183
|
@dynamoid_class = attrs[:dynamoid_class]
|
@@ -187,11 +186,11 @@ module Dynamoid
|
|
187
186
|
@range_key = attrs[:range_key]
|
188
187
|
@name = attrs[:name] || @dynamoid_class.index_name(@hash_key, @range_key)
|
189
188
|
@projected_attributes =
|
190
|
-
|
189
|
+
attrs[:projected_attributes] || DEFAULT_PROJECTION_TYPE
|
191
190
|
@read_capacity = attrs[:read_capacity]
|
192
191
|
@write_capacity = attrs[:write_capacity]
|
193
192
|
|
194
|
-
raise Dynamoid::Errors::InvalidIndex
|
193
|
+
raise Dynamoid::Errors::InvalidIndex, self unless valid?
|
195
194
|
end
|
196
195
|
|
197
196
|
# Convenience method to determine the projection type for an index.
|
@@ -209,16 +208,16 @@ module Dynamoid
|
|
209
208
|
private
|
210
209
|
|
211
210
|
def validate_projected_attributes
|
212
|
-
unless
|
213
|
-
|
211
|
+
unless @projected_attributes.is_a?(Array) ||
|
212
|
+
PROJECTION_TYPES.include?(@projected_attributes)
|
214
213
|
errors.add(:projected_attributes, 'Invalid projected attributes specified.')
|
215
214
|
end
|
216
215
|
end
|
217
216
|
|
218
217
|
def validate_index_type
|
219
|
-
unless
|
220
|
-
|
221
|
-
|
218
|
+
unless @type.present? &&
|
219
|
+
%i[local_secondary global_secondary].include?(@type)
|
220
|
+
errors.add(:type, 'Invalid index :type specified')
|
222
221
|
end
|
223
222
|
end
|
224
223
|
|
@@ -229,7 +228,7 @@ module Dynamoid
|
|
229
228
|
range_key_type = range_field_attributes[:type]
|
230
229
|
if Dynamoid::Fields::PERMITTED_KEY_TYPES.include?(range_key_type)
|
231
230
|
@range_key_schema = {
|
232
|
-
@range_key =>
|
231
|
+
@range_key => PrimaryKeyTypeMapping.dynamodb_type(range_key_type, range_field_attributes)
|
233
232
|
}
|
234
233
|
else
|
235
234
|
errors.add(:range_key, 'Index :range_key is not a valid key type')
|
@@ -246,7 +245,7 @@ module Dynamoid
|
|
246
245
|
hash_field_type = hash_field_attributes[:type]
|
247
246
|
if Dynamoid::Fields::PERMITTED_KEY_TYPES.include?(hash_field_type)
|
248
247
|
@hash_key_schema = {
|
249
|
-
@hash_key =>
|
248
|
+
@hash_key => PrimaryKeyTypeMapping.dynamodb_type(hash_field_type, hash_field_attributes)
|
250
249
|
}
|
251
250
|
else
|
252
251
|
errors.add(:hash_key, 'Index :hash_key is not a valid key type')
|
data/lib/dynamoid/persistence.rb
CHANGED
@@ -1,25 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'bigdecimal'
|
2
4
|
require 'securerandom'
|
3
5
|
require 'yaml'
|
4
6
|
|
5
7
|
# encoding: utf-8
|
6
8
|
module Dynamoid
|
7
|
-
|
8
9
|
# Persistence is responsible for dumping objects to and marshalling objects from the datastore. It tries to reserialize
|
9
10
|
# values to be of the same type as when they were passed in, based on the fields in the class.
|
10
11
|
module Persistence
|
11
12
|
extend ActiveSupport::Concern
|
12
13
|
|
13
14
|
attr_accessor :new_record
|
14
|
-
alias
|
15
|
+
alias new_record? new_record
|
15
16
|
|
16
17
|
UNIX_EPOCH_DATE = Date.new(1970, 1, 1).freeze
|
17
18
|
|
18
19
|
module ClassMethods
|
19
|
-
|
20
20
|
def table_name
|
21
21
|
table_base_name = options[:name] || base_class.name.split('::').last
|
22
|
-
|
22
|
+
.downcase.pluralize
|
23
23
|
|
24
24
|
@table_name ||= [Dynamoid::Config.namespace.to_s, table_base_name].reject(&:empty?).join('_')
|
25
25
|
end
|
@@ -35,20 +35,19 @@ module Dynamoid
|
|
35
35
|
# @option options [Symbol] :hash_key_type the dynamo type of the hash key (:string or :number)
|
36
36
|
# @since 0.4.0
|
37
37
|
def create_table(options = {})
|
38
|
-
if
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
end
|
38
|
+
range_key_hash = if range_key
|
39
|
+
{ range_key => PrimaryKeyTypeMapping.dynamodb_type(attributes[range_key][:type], attributes[range_key]) }
|
40
|
+
end
|
41
|
+
|
43
42
|
options = {
|
44
|
-
id:
|
45
|
-
table_name:
|
46
|
-
write_capacity:
|
47
|
-
read_capacity:
|
43
|
+
id: hash_key,
|
44
|
+
table_name: table_name,
|
45
|
+
write_capacity: write_capacity,
|
46
|
+
read_capacity: read_capacity,
|
48
47
|
range_key: range_key_hash,
|
49
|
-
hash_key_type:
|
50
|
-
local_secondary_indexes:
|
51
|
-
global_secondary_indexes:
|
48
|
+
hash_key_type: PrimaryKeyTypeMapping.dynamodb_type(attributes[hash_key][:type], attributes[hash_key]),
|
49
|
+
local_secondary_indexes: local_secondary_indexes.values,
|
50
|
+
global_secondary_indexes: global_secondary_indexes.values
|
52
51
|
}.merge(options)
|
53
52
|
|
54
53
|
Dynamoid.adapter.create_table(options[:table_name], options[:id], options)
|
@@ -56,160 +55,13 @@ module Dynamoid
|
|
56
55
|
|
57
56
|
# Deletes the table for the model
|
58
57
|
def delete_table
|
59
|
-
Dynamoid.adapter.delete_table(
|
58
|
+
Dynamoid.adapter.delete_table(table_name)
|
60
59
|
end
|
61
60
|
|
62
61
|
def from_database(attrs = {})
|
63
62
|
clazz = attrs[:type] ? obj = attrs[:type].constantize : self
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
# Undump an object into a hash, converting each type from a string representation of itself into the type specified by the field.
|
68
|
-
#
|
69
|
-
# @since 0.2.0
|
70
|
-
def undump(incoming = nil)
|
71
|
-
incoming = (incoming || {}).symbolize_keys
|
72
|
-
Hash.new.tap do |hash|
|
73
|
-
self.attributes.each do |attribute, options|
|
74
|
-
if incoming.has_key?(attribute)
|
75
|
-
hash[attribute] = undump_field(incoming[attribute], options)
|
76
|
-
elsif options.has_key?(:default)
|
77
|
-
hash[attribute] = evaluate_default_value(options[:default])
|
78
|
-
else
|
79
|
-
hash[attribute] = nil
|
80
|
-
end
|
81
|
-
end
|
82
|
-
incoming.each {|attribute, value| hash[attribute] = value unless hash.has_key? attribute }
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
# Undump a string value for a given type.
|
87
|
-
#
|
88
|
-
# @since 0.2.0
|
89
|
-
def undump_field(value, options)
|
90
|
-
if (field_class = options[:type]).is_a?(Class)
|
91
|
-
raise 'Dynamoid class-type fields do not support default values' if options[:default]
|
92
|
-
|
93
|
-
if field_class.respond_to?(:dynamoid_load)
|
94
|
-
field_class.dynamoid_load(value)
|
95
|
-
end
|
96
|
-
elsif options[:type] == :serialized
|
97
|
-
if value.is_a?(String)
|
98
|
-
options[:serializer] ? options[:serializer].load(value) : YAML.load(value)
|
99
|
-
else
|
100
|
-
value
|
101
|
-
end
|
102
|
-
else
|
103
|
-
unless value.nil?
|
104
|
-
case options[:type]
|
105
|
-
when :string
|
106
|
-
value.to_s
|
107
|
-
when :integer
|
108
|
-
Integer(value)
|
109
|
-
when :number
|
110
|
-
BigDecimal.new(value.to_s)
|
111
|
-
when :array
|
112
|
-
value.to_a
|
113
|
-
when :raw
|
114
|
-
if value.is_a?(Hash)
|
115
|
-
undump_hash(value)
|
116
|
-
else
|
117
|
-
value
|
118
|
-
end
|
119
|
-
when :set
|
120
|
-
undump_set(options, value)
|
121
|
-
when :datetime
|
122
|
-
parse_datetime(value, options)
|
123
|
-
when :date
|
124
|
-
if value.is_a?(Date) || value.is_a?(DateTime) || value.is_a?(Time)
|
125
|
-
value.to_date
|
126
|
-
else
|
127
|
-
parse_date(value, options)
|
128
|
-
end
|
129
|
-
when :boolean
|
130
|
-
if value == 't' || value == true
|
131
|
-
true
|
132
|
-
elsif value == 'f' || value == false
|
133
|
-
false
|
134
|
-
else
|
135
|
-
raise ArgumentError, 'Boolean column neither true nor false'
|
136
|
-
end
|
137
|
-
else
|
138
|
-
raise ArgumentError, "Unknown type #{options[:type]}"
|
139
|
-
end
|
140
|
-
end
|
141
|
-
end
|
142
|
-
end
|
143
|
-
|
144
|
-
def undump_set(options, value)
|
145
|
-
case options[:of]
|
146
|
-
when :integer
|
147
|
-
value.map { |v| Integer(v) }.to_set
|
148
|
-
when :number
|
149
|
-
value.map { |v| BigDecimal.new(v.to_s) }.to_set
|
150
|
-
else
|
151
|
-
value.is_a?(Set) ? value : Set.new(value)
|
152
|
-
end
|
153
|
-
end
|
154
|
-
|
155
|
-
def dump_field(value, options)
|
156
|
-
if (field_class = options[:type]).is_a?(Class)
|
157
|
-
if value.respond_to?(:dynamoid_dump)
|
158
|
-
value.dynamoid_dump
|
159
|
-
elsif field_class.respond_to?(:dynamoid_dump)
|
160
|
-
field_class.dynamoid_dump(value)
|
161
|
-
else
|
162
|
-
raise ArgumentError, "Neither #{field_class} nor #{value} support serialization for Dynamoid."
|
163
|
-
end
|
164
|
-
else
|
165
|
-
case options[:type]
|
166
|
-
when :string
|
167
|
-
!value.nil? ? value.to_s : nil
|
168
|
-
when :integer
|
169
|
-
!value.nil? ? Integer(value) : nil
|
170
|
-
when :number
|
171
|
-
!value.nil? ? value : nil
|
172
|
-
when :set
|
173
|
-
!value.nil? ? Set.new(value) : nil
|
174
|
-
when :array
|
175
|
-
!value.nil? ? value : nil
|
176
|
-
when :datetime
|
177
|
-
!value.nil? ? format_datetime(value, options) : nil
|
178
|
-
when :date
|
179
|
-
!value.nil? ? format_date(value, options) : nil
|
180
|
-
when :serialized
|
181
|
-
options[:serializer] ? options[:serializer].dump(value) : value.to_yaml
|
182
|
-
when :raw
|
183
|
-
!value.nil? ? value : nil
|
184
|
-
when :boolean
|
185
|
-
if !value.nil?
|
186
|
-
if options[:store_as_native_boolean]
|
187
|
-
!!value # native boolean type
|
188
|
-
else
|
189
|
-
value.to_s[0] # => "f" or "t"
|
190
|
-
end
|
191
|
-
else
|
192
|
-
nil
|
193
|
-
end
|
194
|
-
else
|
195
|
-
raise ArgumentError, "Unknown type #{options[:type]}"
|
196
|
-
end
|
197
|
-
end
|
198
|
-
end
|
199
|
-
|
200
|
-
def dynamo_type(type)
|
201
|
-
if type.is_a?(Class)
|
202
|
-
type.respond_to?(:dynamoid_field_type) ? type.dynamoid_field_type : :string
|
203
|
-
else
|
204
|
-
case type
|
205
|
-
when :integer, :number, :datetime, :date
|
206
|
-
:number
|
207
|
-
when :string, :serialized
|
208
|
-
:string
|
209
|
-
else
|
210
|
-
raise 'unknown type'
|
211
|
-
end
|
212
|
-
end
|
63
|
+
attrs_undumped = Undumping.undump_attributes(attrs, clazz.attributes)
|
64
|
+
clazz.new(attrs_undumped).tap { |r| r.new_record = false }
|
213
65
|
end
|
214
66
|
|
215
67
|
# Creates several models at once.
|
@@ -226,16 +78,27 @@ module Dynamoid
|
|
226
78
|
# User.import([{ name: 'a' }, { name: 'b' }])
|
227
79
|
def import(objects)
|
228
80
|
documents = objects.map do |attrs|
|
229
|
-
|
81
|
+
attrs = attrs.symbolize_keys
|
82
|
+
|
83
|
+
if Dynamoid::Config.timestamps
|
84
|
+
time_now = DateTime.now.in_time_zone(Time.zone)
|
85
|
+
attrs[:created_at] ||= time_now
|
86
|
+
attrs[:updated_at] ||= time_now
|
87
|
+
end
|
88
|
+
|
89
|
+
build(attrs).tap do |item|
|
230
90
|
item.hash_key = SecureRandom.uuid if item.hash_key.blank?
|
231
91
|
end
|
232
92
|
end
|
233
93
|
|
234
|
-
|
235
|
-
Dynamoid.adapter.batch_write_item(self.table_name, documents.map(&:dump))
|
236
|
-
else
|
94
|
+
if Dynamoid.config.backoff
|
237
95
|
backoff = nil
|
238
|
-
|
96
|
+
|
97
|
+
array = documents.map do |d|
|
98
|
+
Dumping.dump_attributes(d.attributes, attributes)
|
99
|
+
end
|
100
|
+
|
101
|
+
Dynamoid.adapter.batch_write_item(table_name, array) do |has_unprocessed_items|
|
239
102
|
if has_unprocessed_items
|
240
103
|
backoff ||= Dynamoid.config.build_backoff
|
241
104
|
backoff.call
|
@@ -243,106 +106,16 @@ module Dynamoid
|
|
243
106
|
backoff = nil
|
244
107
|
end
|
245
108
|
end
|
246
|
-
end
|
247
|
-
|
248
|
-
documents.each { |d| d.new_record = false }
|
249
|
-
documents
|
250
|
-
end
|
251
|
-
|
252
|
-
private
|
253
|
-
|
254
|
-
def undump_hash(hash)
|
255
|
-
{}.tap do |h|
|
256
|
-
hash.each { |key, value| h[key.to_sym] = undump_hash_value(value) }
|
257
|
-
end
|
258
|
-
end
|
259
|
-
|
260
|
-
def undump_hash_value(val)
|
261
|
-
case val
|
262
|
-
when BigDecimal
|
263
|
-
if Dynamoid::Config.convert_big_decimal
|
264
|
-
val.to_f
|
265
|
-
else
|
266
|
-
val
|
267
|
-
end
|
268
|
-
when Hash
|
269
|
-
undump_hash(val)
|
270
|
-
when Array
|
271
|
-
val.map { |v| undump_hash_value(v) }
|
272
|
-
else
|
273
|
-
val
|
274
|
-
end
|
275
|
-
end
|
276
|
-
|
277
|
-
def format_datetime(value, options)
|
278
|
-
use_string_format = options[:store_as_string].nil? \
|
279
|
-
? Dynamoid.config.store_datetime_as_string \
|
280
|
-
: options[:store_as_string]
|
281
|
-
|
282
|
-
if use_string_format
|
283
|
-
value.to_time.iso8601
|
284
109
|
else
|
285
|
-
|
286
|
-
|
110
|
+
array = documents.map do |d|
|
111
|
+
Dumping.dump_attributes(d.attributes, attributes)
|
287
112
|
end
|
288
|
-
BigDecimal("%d.%09d" % [value.to_i, value.nsec])
|
289
|
-
end
|
290
|
-
end
|
291
|
-
|
292
|
-
def format_date(value, options)
|
293
|
-
use_string_format = options[:store_as_string].nil? \
|
294
|
-
? Dynamoid.config.store_date_as_string \
|
295
|
-
: options[:store_as_string]
|
296
|
-
|
297
|
-
unless use_string_format
|
298
|
-
(value.to_date - UNIX_EPOCH_DATE).to_i
|
299
|
-
else
|
300
|
-
value.to_date.iso8601
|
301
|
-
end
|
302
|
-
end
|
303
|
-
|
304
|
-
def parse_datetime(value, options)
|
305
|
-
return value if value.is_a?(Date) || value.is_a?(DateTime) || value.is_a?(Time)
|
306
|
-
|
307
|
-
use_string_format = options[:store_as_string].nil? \
|
308
|
-
? Dynamoid.config.store_datetime_as_string \
|
309
|
-
: options[:store_as_string]
|
310
|
-
value = DateTime.iso8601(value).to_time.to_i if use_string_format
|
311
|
-
|
312
|
-
case Dynamoid::Config.application_timezone
|
313
|
-
when :utc
|
314
|
-
ActiveSupport::TimeZone['UTC'].at(value).to_datetime
|
315
|
-
when :local
|
316
|
-
Time.at(value).to_datetime
|
317
|
-
when String
|
318
|
-
ActiveSupport::TimeZone[Dynamoid::Config.application_timezone].at(value).to_datetime
|
319
|
-
end
|
320
|
-
end
|
321
|
-
|
322
|
-
def parse_date(value, options)
|
323
|
-
use_string_format = options[:store_as_string].nil? \
|
324
|
-
? Dynamoid.config.store_date_as_string \
|
325
|
-
: options[:store_as_string]
|
326
113
|
|
327
|
-
|
328
|
-
UNIX_EPOCH_DATE + value.to_i
|
329
|
-
else
|
330
|
-
Date.iso8601(value)
|
114
|
+
Dynamoid.adapter.batch_write_item(table_name, array)
|
331
115
|
end
|
332
|
-
end
|
333
116
|
|
334
|
-
|
335
|
-
|
336
|
-
#
|
337
|
-
# @param [Object] :value the attribute's default value
|
338
|
-
def evaluate_default_value(val)
|
339
|
-
if val.respond_to?(:call)
|
340
|
-
val.call
|
341
|
-
elsif val.duplicable?
|
342
|
-
val.dup
|
343
|
-
else
|
344
|
-
val
|
345
|
-
end
|
117
|
+
documents.each { |d| d.new_record = false }
|
118
|
+
documents
|
346
119
|
end
|
347
120
|
end
|
348
121
|
|
@@ -365,12 +138,12 @@ module Dynamoid
|
|
365
138
|
# Run the callbacks and then persist this object in the datastore.
|
366
139
|
#
|
367
140
|
# @since 0.2.0
|
368
|
-
def save(
|
141
|
+
def save(_options = {})
|
369
142
|
self.class.create_table
|
370
143
|
|
371
144
|
if new_record?
|
372
|
-
conditions = { unless_exists: [self.class.hash_key]}
|
373
|
-
conditions[:unless_exists] << range_key if
|
145
|
+
conditions = { unless_exists: [self.class.hash_key] }
|
146
|
+
conditions[:unless_exists] << range_key if range_key
|
374
147
|
|
375
148
|
run_callbacks(:create) { persist(conditions) }
|
376
149
|
else
|
@@ -383,19 +156,23 @@ module Dynamoid
|
|
383
156
|
# never cause an update! to fail, but an update! may cause a concurrent save to fail.
|
384
157
|
#
|
385
158
|
#
|
386
|
-
def update!(conditions = {}
|
159
|
+
def update!(conditions = {})
|
387
160
|
run_callbacks(:update) do
|
388
|
-
options = range_key ? {range_key: dump_field(
|
161
|
+
options = range_key ? { range_key: Dumping.dump_field(read_attribute(range_key), self.class.attributes[range_key]) } : {}
|
389
162
|
|
390
163
|
begin
|
391
|
-
new_attrs = Dynamoid.adapter.update_item(self.class.table_name,
|
392
|
-
if
|
393
|
-
|
164
|
+
new_attrs = Dynamoid.adapter.update_item(self.class.table_name, hash_key, options.merge(conditions: conditions)) do |t|
|
165
|
+
t.add(lock_version: 1) if self.class.attributes[:lock_version]
|
166
|
+
|
167
|
+
if Dynamoid::Config.timestamps
|
168
|
+
time_now = DateTime.now.in_time_zone(Time.zone)
|
169
|
+
time_now_dumped = Dumping.dump_field(time_now, self.class.attributes[:updated_at])
|
170
|
+
t.set(updated_at: time_now_dumped)
|
394
171
|
end
|
395
172
|
|
396
173
|
yield t
|
397
174
|
end
|
398
|
-
load(new_attrs)
|
175
|
+
load(Undumping.undump_attributes(new_attrs, self.class.attributes))
|
399
176
|
rescue Dynamoid::Errors::ConditionalCheckFailedException
|
400
177
|
raise Dynamoid::Errors::StaleObjectError.new(self, 'update')
|
401
178
|
end
|
@@ -414,81 +191,64 @@ module Dynamoid
|
|
414
191
|
# @since 0.2.0
|
415
192
|
def destroy
|
416
193
|
ret = run_callbacks(:destroy) do
|
417
|
-
|
194
|
+
delete
|
418
195
|
end
|
419
|
-
|
196
|
+
ret == false ? false : self
|
420
197
|
end
|
421
198
|
|
422
199
|
def destroy!
|
423
|
-
destroy || raise
|
200
|
+
destroy || (raise Dynamoid::Errors::RecordNotDestroyed, self)
|
424
201
|
end
|
425
202
|
|
426
203
|
# Delete this object from the datastore.
|
427
204
|
#
|
428
205
|
# @since 0.2.0
|
429
206
|
def delete
|
430
|
-
options = range_key ? {range_key: dump_field(
|
207
|
+
options = range_key ? { range_key: Dumping.dump_field(read_attribute(range_key), self.class.attributes[range_key]) } : {}
|
431
208
|
|
432
209
|
# Add an optimistic locking check if the lock_version column exists
|
433
|
-
if
|
434
|
-
conditions = {if: {}}
|
210
|
+
if self.class.attributes[:lock_version]
|
211
|
+
conditions = { if: {} }
|
435
212
|
conditions[:if][:lock_version] =
|
436
213
|
if changes[:lock_version].nil?
|
437
|
-
|
214
|
+
lock_version
|
438
215
|
else
|
439
216
|
changes[:lock_version][0]
|
440
217
|
end
|
441
218
|
options[:conditions] = conditions
|
442
219
|
end
|
443
|
-
Dynamoid.adapter.delete(self.class.table_name,
|
220
|
+
Dynamoid.adapter.delete(self.class.table_name, hash_key, options)
|
444
221
|
rescue Dynamoid::Errors::ConditionalCheckFailedException
|
445
222
|
raise Dynamoid::Errors::StaleObjectError.new(self, 'delete')
|
446
223
|
end
|
447
224
|
|
448
|
-
# Dump this object's attributes into hash form, fit to be persisted into the datastore.
|
449
|
-
#
|
450
|
-
# @since 0.2.0
|
451
|
-
def dump
|
452
|
-
Hash.new.tap do |hash|
|
453
|
-
self.class.attributes.each do |attribute, options|
|
454
|
-
hash[attribute] = dump_field(self.read_attribute(attribute), options)
|
455
|
-
end
|
456
|
-
end
|
457
|
-
end
|
458
|
-
|
459
225
|
private
|
460
226
|
|
461
|
-
# Determine how to dump this field. Given a value, it'll determine how to turn it into a value that can be
|
462
|
-
# persisted into the datastore.
|
463
|
-
#
|
464
|
-
# @since 0.2.0
|
465
|
-
def dump_field(value, options)
|
466
|
-
self.class.dump_field(value, options)
|
467
|
-
end
|
468
|
-
|
469
227
|
# Persist the object into the datastore. Assign it an id first if it doesn't have one.
|
470
228
|
#
|
471
229
|
# @since 0.2.0
|
472
230
|
def persist(conditions = nil)
|
473
231
|
run_callbacks(:save) do
|
474
|
-
self.hash_key = SecureRandom.uuid if
|
232
|
+
self.hash_key = SecureRandom.uuid if hash_key.blank?
|
475
233
|
|
476
234
|
# Add an exists check to prevent overwriting existing records with new ones
|
477
|
-
if
|
235
|
+
if new_record?
|
478
236
|
conditions ||= {}
|
479
237
|
(conditions[:unless_exists] ||= []) << self.class.hash_key
|
480
238
|
end
|
481
239
|
|
482
240
|
# Add an optimistic locking check if the lock_version column exists
|
483
|
-
if
|
241
|
+
if self.class.attributes[:lock_version]
|
484
242
|
conditions ||= {}
|
485
243
|
self.lock_version = (lock_version || 0) + 1
|
486
244
|
# Uses the original lock_version value from ActiveModel::Dirty in case user changed lock_version manually
|
487
|
-
(conditions[:if] ||= {})[:lock_version] = changes[:lock_version][0] if
|
245
|
+
(conditions[:if] ||= {})[:lock_version] = changes[:lock_version][0] if changes[:lock_version][0]
|
488
246
|
end
|
489
247
|
|
248
|
+
attributes_dumped = Dumping.dump_attributes(attributes, self.class.attributes)
|
249
|
+
|
490
250
|
begin
|
491
|
-
Dynamoid.adapter.write(self.class.table_name,
|
251
|
+
Dynamoid.adapter.write(self.class.table_name, attributes_dumped, conditions)
|
492
252
|
@new_record = false
|
493
253
|
true
|
494
254
|
rescue Dynamoid::Errors::ConditionalCheckFailedException => e
|