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
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dynamoid
|
4
|
+
class PrimaryKeyTypeMapping
|
5
|
+
def self.dynamodb_type(type, options)
|
6
|
+
if Class === type
|
7
|
+
type = type.respond_to?(:dynamoid_field_type) ? type.dynamoid_field_type : :string
|
8
|
+
end
|
9
|
+
|
10
|
+
case type
|
11
|
+
when :string, :serialized
|
12
|
+
:string
|
13
|
+
when :integer, :number
|
14
|
+
:number
|
15
|
+
when :datetime
|
16
|
+
string_format = if options[:store_as_string].nil?
|
17
|
+
Dynamoid::Config.store_datetime_as_string
|
18
|
+
else
|
19
|
+
options[:store_as_string]
|
20
|
+
end
|
21
|
+
string_format ? :string : :number
|
22
|
+
when :date
|
23
|
+
string_format = if options[:store_as_string].nil?
|
24
|
+
Dynamoid::Config.store_date_as_string
|
25
|
+
else
|
26
|
+
options[:store_as_string]
|
27
|
+
end
|
28
|
+
string_format ? :string : :number
|
29
|
+
else
|
30
|
+
raise Errors::UnsupportedKeyType, "#{type} cannot be used as a type of table key attribute"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/dynamoid/railtie.rb
CHANGED
@@ -1,29 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'dynamoid'
|
2
4
|
require 'dynamoid/tasks/database'
|
3
5
|
|
4
6
|
namespace :dynamoid do
|
5
|
-
desc
|
6
|
-
task :
|
7
|
+
desc 'Creates DynamoDB tables, one for each of your Dynamoid models - does not modify pre-existing tables'
|
8
|
+
task create_tables: :environment do
|
7
9
|
# Load models so Dynamoid will be able to discover tables expected.
|
8
|
-
Dir[
|
10
|
+
Dir[File.join(Dynamoid::Config.models_dir, '*.rb')].sort.each { |file| require file }
|
9
11
|
if Dynamoid.included_models.any?
|
10
12
|
tables = Dynamoid::Tasks::Database.create_tables
|
11
|
-
result = tables[:created].map{ |c| "#{c} created" } + tables[:existing].map{ |e| "#{e} already exists" }
|
12
|
-
result.sort.each{ |r| puts r }
|
13
|
+
result = tables[:created].map { |c| "#{c} created" } + tables[:existing].map { |e| "#{e} already exists" }
|
14
|
+
result.sort.each { |r| puts r }
|
13
15
|
else
|
14
|
-
puts
|
16
|
+
puts 'Dynamoid models are not loaded, or you have no Dynamoid models.'
|
15
17
|
end
|
16
18
|
end
|
17
19
|
|
18
20
|
desc 'Tests if the DynamoDB instance can be contacted using your configuration'
|
19
|
-
task :
|
21
|
+
task ping: :environment do
|
20
22
|
success = false
|
21
23
|
failure_reason = nil
|
22
24
|
|
23
25
|
begin
|
24
26
|
Dynamoid::Tasks::Database.ping
|
25
27
|
success = true
|
26
|
-
rescue
|
28
|
+
rescue StandardError => e
|
27
29
|
failure_reason = e.message
|
28
30
|
end
|
29
31
|
|
@@ -33,9 +35,7 @@ namespace :dynamoid do
|
|
33
35
|
else
|
34
36
|
' at remote AWS endpoint'
|
35
37
|
end
|
36
|
-
|
37
|
-
msg << ", reason being '#{failure_reason}'"
|
38
|
-
end
|
38
|
+
msg << ", reason being '#{failure_reason}'" unless success
|
39
39
|
puts msg
|
40
40
|
end
|
41
41
|
end
|
@@ -1,14 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Dynamoid
|
2
4
|
module Tasks
|
3
5
|
module Database
|
4
|
-
|
6
|
+
module_function
|
5
7
|
|
6
8
|
# Create any new tables for the models. Existing tables are not
|
7
9
|
# modified.
|
8
10
|
def create_tables
|
9
11
|
results = { created: [], existing: [] }
|
10
12
|
# We can't quite rely on Dynamoid.included_models alone, we need to select only viable models
|
11
|
-
Dynamoid.included_models.
|
13
|
+
Dynamoid.included_models.reject { |m| m.base_class.try(:name).blank? }.uniq(&:table_name).each do |model|
|
12
14
|
if Dynamoid.adapter.list_tables.include? model.table_name
|
13
15
|
results[:existing] << model.table_name
|
14
16
|
else
|
@@ -24,7 +26,6 @@ module Dynamoid
|
|
24
26
|
Dynamoid.adapter.list_tables
|
25
27
|
true
|
26
28
|
end
|
27
|
-
|
28
29
|
end
|
29
30
|
end
|
30
31
|
end
|
@@ -0,0 +1,193 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dynamoid
|
4
|
+
module TypeCasting
|
5
|
+
def self.cast_attributes(attributes, attributes_options)
|
6
|
+
{}.tap do |h|
|
7
|
+
attributes.symbolize_keys.each do |attribute, value|
|
8
|
+
h[attribute] = cast_field(value, attributes_options[attribute])
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.cast_field(value, options)
|
14
|
+
return value if options.nil?
|
15
|
+
return nil if value.nil?
|
16
|
+
|
17
|
+
type_caster = find_type_caster(options)
|
18
|
+
if type_caster.nil?
|
19
|
+
raise ArgumentError, "Unknown type #{options[:type]}"
|
20
|
+
end
|
21
|
+
|
22
|
+
type_caster.process(value)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.find_type_caster(options)
|
26
|
+
type_caster_class = case options[:type]
|
27
|
+
when :string then StringTypeCaster
|
28
|
+
when :integer then IntegerTypeCaster
|
29
|
+
when :number then NumberTypeCaster
|
30
|
+
when :set then SetTypeCaster
|
31
|
+
when :array then ArrayTypeCaster
|
32
|
+
when :datetime then DateTimeTypeCaster
|
33
|
+
when :date then DateTypeCaster
|
34
|
+
when :raw then RawTypeCaster
|
35
|
+
when :serialized then SerializedTypeCaster
|
36
|
+
when :boolean then BooleanTypeCaster
|
37
|
+
when Class then CustomTypeCaster
|
38
|
+
end
|
39
|
+
|
40
|
+
if type_caster_class.present?
|
41
|
+
type_caster_class.new(options)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class Base
|
46
|
+
def initialize(options)
|
47
|
+
@options = options
|
48
|
+
end
|
49
|
+
|
50
|
+
def process(value)
|
51
|
+
value
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class StringTypeCaster < Base
|
56
|
+
def process(value)
|
57
|
+
if value == true
|
58
|
+
't'
|
59
|
+
elsif value == false
|
60
|
+
'f'
|
61
|
+
elsif value.is_a? String
|
62
|
+
value.dup
|
63
|
+
else
|
64
|
+
value.to_s
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class IntegerTypeCaster < Base
|
70
|
+
def process(value)
|
71
|
+
if value == true
|
72
|
+
1
|
73
|
+
elsif value == false
|
74
|
+
0
|
75
|
+
elsif value.is_a?(String) && value.blank?
|
76
|
+
nil
|
77
|
+
elsif value.is_a?(Float) && !value.finite?
|
78
|
+
nil
|
79
|
+
elsif !value.respond_to?(:to_i)
|
80
|
+
nil
|
81
|
+
else
|
82
|
+
value.to_i
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
class NumberTypeCaster < Base
|
88
|
+
def process(value)
|
89
|
+
if value == true
|
90
|
+
1
|
91
|
+
elsif value == false
|
92
|
+
0
|
93
|
+
elsif value.is_a?(Symbol)
|
94
|
+
value.to_s.to_d
|
95
|
+
elsif value.is_a?(String) && value.blank?
|
96
|
+
nil
|
97
|
+
elsif value.is_a?(Float) && !value.finite?
|
98
|
+
nil
|
99
|
+
elsif !(value.respond_to?(:to_d))
|
100
|
+
nil
|
101
|
+
else
|
102
|
+
value.to_d
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
class SetTypeCaster < Base
|
108
|
+
def process(value)
|
109
|
+
if value.is_a?(Set)
|
110
|
+
value.dup
|
111
|
+
elsif value.respond_to?(:to_set)
|
112
|
+
value.to_set
|
113
|
+
else
|
114
|
+
nil
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
class ArrayTypeCaster < Base
|
120
|
+
def process(value)
|
121
|
+
if value.is_a?(Array)
|
122
|
+
value.dup
|
123
|
+
elsif value.respond_to?(:to_a)
|
124
|
+
value.to_a
|
125
|
+
else
|
126
|
+
nil
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
class DateTimeTypeCaster < Base
|
132
|
+
def process(value)
|
133
|
+
if !value.respond_to?(:to_datetime)
|
134
|
+
nil
|
135
|
+
elsif value.is_a?(String)
|
136
|
+
dt = DateTime.parse(value) rescue nil
|
137
|
+
if dt
|
138
|
+
seconds = string_utc_offset(value) || ApplicationTimeZone.utc_offset
|
139
|
+
offset = seconds_to_offset(seconds)
|
140
|
+
DateTime.new(dt.year, dt.mon, dt.mday, dt.hour, dt.min, dt.sec, offset)
|
141
|
+
end
|
142
|
+
else
|
143
|
+
value.to_datetime
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
|
149
|
+
def string_utc_offset(string)
|
150
|
+
Date._parse(string)[:offset]
|
151
|
+
end
|
152
|
+
|
153
|
+
# 3600 -> "+01:00"
|
154
|
+
def seconds_to_offset(seconds)
|
155
|
+
ActiveSupport::TimeZone.seconds_to_utc_offset(seconds)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
class DateTypeCaster < Base
|
160
|
+
def process(value)
|
161
|
+
if !value.respond_to?(:to_date)
|
162
|
+
nil
|
163
|
+
else
|
164
|
+
begin
|
165
|
+
value.to_date
|
166
|
+
rescue ArgumentError
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
class RawTypeCaster < Base
|
173
|
+
end
|
174
|
+
|
175
|
+
class SerializedTypeCaster < Base
|
176
|
+
end
|
177
|
+
|
178
|
+
class BooleanTypeCaster < Base
|
179
|
+
def process(value)
|
180
|
+
if value == ''
|
181
|
+
nil
|
182
|
+
elsif [false, 'false', 'FALSE', 0, '0', 'f', 'F', 'off', 'OFF'].include? value
|
183
|
+
false
|
184
|
+
else
|
185
|
+
true
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
class CustomTypeCaster < Base
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dynamoid
|
4
|
+
module Undumping
|
5
|
+
def self.undump_attributes(attributes, attributes_options)
|
6
|
+
{}.tap do |h|
|
7
|
+
attributes.symbolize_keys.each do |attribute, value|
|
8
|
+
h[attribute] = undump_field(value, attributes_options[attribute])
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.undump_field(value, options)
|
14
|
+
undumper = find_undumper(options)
|
15
|
+
|
16
|
+
if undumper.nil?
|
17
|
+
raise ArgumentError, "Unknown type #{options[:type]}"
|
18
|
+
end
|
19
|
+
|
20
|
+
return nil if value.nil?
|
21
|
+
undumper.process(value)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.find_undumper(options)
|
25
|
+
undumper_class = case options[:type]
|
26
|
+
when :string then StringUndumper
|
27
|
+
when :integer then IntegerUndumper
|
28
|
+
when :number then NumberUndumper
|
29
|
+
when :set then SetUndumper
|
30
|
+
when :array then ArrayUndumper
|
31
|
+
when :datetime then DateTimeUndumper
|
32
|
+
when :date then DateUndumper
|
33
|
+
when :raw then RawUndumper
|
34
|
+
when :serialized then SerializedUndumper
|
35
|
+
when :boolean then BooleanUndumper
|
36
|
+
when Class then CustomTypeUndumper
|
37
|
+
end
|
38
|
+
|
39
|
+
if undumper_class.present?
|
40
|
+
undumper_class.new(options)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class Base
|
45
|
+
def initialize(options)
|
46
|
+
@options = options
|
47
|
+
end
|
48
|
+
|
49
|
+
def process(value)
|
50
|
+
value
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class StringUndumper < Base
|
55
|
+
end
|
56
|
+
|
57
|
+
class IntegerUndumper < Base
|
58
|
+
def process(value)
|
59
|
+
value.to_i
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class NumberUndumper < Base
|
64
|
+
end
|
65
|
+
|
66
|
+
class SetUndumper < Base
|
67
|
+
def process(value)
|
68
|
+
case @options[:of]
|
69
|
+
when :integer
|
70
|
+
value.map { |v| Integer(v) }.to_set
|
71
|
+
when :number
|
72
|
+
value.map { |v| BigDecimal(v.to_s) }.to_set
|
73
|
+
else
|
74
|
+
value.is_a?(Set) ? value : Set.new(value)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class ArrayUndumper < Base
|
80
|
+
end
|
81
|
+
|
82
|
+
class DateTimeUndumper < Base
|
83
|
+
def process(value)
|
84
|
+
return value if value.is_a?(Date) || value.is_a?(DateTime) || value.is_a?(Time)
|
85
|
+
|
86
|
+
use_string_format = if @options[:store_as_string].nil?
|
87
|
+
Dynamoid.config.store_datetime_as_string
|
88
|
+
else
|
89
|
+
@options[:store_as_string]
|
90
|
+
end
|
91
|
+
value = DateTime.iso8601(value).to_time.to_i if use_string_format
|
92
|
+
ApplicationTimeZone.at(value)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
class DateUndumper < Base
|
97
|
+
def process(value)
|
98
|
+
use_string_format = if @options[:store_as_string].nil?
|
99
|
+
Dynamoid.config.store_date_as_string
|
100
|
+
else
|
101
|
+
@options[:store_as_string]
|
102
|
+
end
|
103
|
+
|
104
|
+
if use_string_format
|
105
|
+
Date.iso8601(value)
|
106
|
+
else
|
107
|
+
Dynamoid::Persistence::UNIX_EPOCH_DATE + value.to_i
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
class RawUndumper < Base
|
113
|
+
def process(value)
|
114
|
+
if value.is_a?(Hash)
|
115
|
+
undump_hash(value)
|
116
|
+
else
|
117
|
+
value
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def undump_hash(hash)
|
124
|
+
{}.tap do |h|
|
125
|
+
hash.each { |key, value| h[key.to_sym] = undump_hash_value(value) }
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def undump_hash_value(val)
|
130
|
+
case val
|
131
|
+
when BigDecimal
|
132
|
+
if Dynamoid::Config.convert_big_decimal
|
133
|
+
val.to_f
|
134
|
+
else
|
135
|
+
val
|
136
|
+
end
|
137
|
+
when Hash
|
138
|
+
undump_hash(val)
|
139
|
+
when Array
|
140
|
+
val.map { |v| undump_hash_value(v) }
|
141
|
+
else
|
142
|
+
val
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
class SerializedUndumper < Base
|
148
|
+
def process(value)
|
149
|
+
if @options[:serializer]
|
150
|
+
@options[:serializer].load(value)
|
151
|
+
else
|
152
|
+
YAML.load(value)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
class BooleanUndumper < Base
|
158
|
+
STRING_VALUES = ['t', 'f']
|
159
|
+
|
160
|
+
def process(value)
|
161
|
+
store_as_boolean = if @options[:store_as_native_boolean].nil?
|
162
|
+
Dynamoid.config.store_boolean_as_native
|
163
|
+
else
|
164
|
+
@options[:store_as_native_boolean]
|
165
|
+
end
|
166
|
+
if store_as_boolean
|
167
|
+
!!value
|
168
|
+
elsif STRING_VALUES.include?(value)
|
169
|
+
value == 't'
|
170
|
+
else
|
171
|
+
raise ArgumentError, 'Boolean column neither true nor false'
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
class CustomTypeUndumper < Base
|
177
|
+
def process(value)
|
178
|
+
field_class = @options[:type]
|
179
|
+
|
180
|
+
unless field_class.respond_to?(:dynamoid_load)
|
181
|
+
raise ArgumentError, "#{field_class} does not support serialization for Dynamoid."
|
182
|
+
end
|
183
|
+
|
184
|
+
field_class.dynamoid_load(value)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|