dynamo-record 0.2.0 → 0.3.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/.dockerignore +15 -0
- data/.gitignore +9 -5
- data/.rspec +1 -0
- data/.rubocop.yml +9 -30
- data/.travis.yml +18 -2
- data/Dockerfile +22 -0
- data/Gemfile +0 -1
- data/LICENSE.txt +21 -0
- data/README.md +75 -17
- data/build.sh +10 -20
- data/docker-compose.override.example.yml +19 -0
- data/docker-compose.yml +7 -2
- data/dynamo-record.gemspec +40 -28
- data/lib/dynamo/record.rb +17 -0
- data/lib/dynamo/record/marshalers.rb +46 -0
- data/lib/dynamo/record/model.rb +127 -0
- data/lib/dynamo/record/model_existence_validator.rb +10 -0
- data/lib/{dynamo-record → dynamo}/record/railtie.rb +1 -3
- data/lib/dynamo/record/table_migration.rb +59 -0
- data/lib/dynamo/record/task_helpers/cleanup.rb +21 -0
- data/lib/dynamo/record/task_helpers/drop_all_tables.rb +19 -0
- data/lib/dynamo/record/task_helpers/drop_table.rb +13 -0
- data/lib/dynamo/record/task_helpers/list_tables.rb +15 -0
- data/lib/dynamo/record/task_helpers/migration_runner.rb +72 -0
- data/lib/dynamo/record/task_helpers/scale.rb +90 -0
- data/lib/dynamo/record/version.rb +5 -0
- data/lib/tasks/dynamo.rake +7 -7
- metadata +96 -37
- data/Dockerfile.test +0 -23
- data/Gemfile.lock +0 -178
- data/doc/testing.md +0 -11
- data/docker-compose.dev.override.yml +0 -6
- data/lib/dynamo-record.rb +0 -4
- data/lib/dynamo-record/marshalers.rb +0 -44
- data/lib/dynamo-record/model.rb +0 -127
- data/lib/dynamo-record/record.rb +0 -7
- data/lib/dynamo-record/record/version.rb +0 -5
- data/lib/dynamo-record/table_migration.rb +0 -58
- data/lib/dynamo-record/task_helpers/cleanup.rb +0 -19
- data/lib/dynamo-record/task_helpers/drop_all_tables.rb +0 -17
- data/lib/dynamo-record/task_helpers/drop_table.rb +0 -11
- data/lib/dynamo-record/task_helpers/list_tables.rb +0 -13
- data/lib/dynamo-record/task_helpers/migration_runner.rb +0 -70
- data/lib/dynamo-record/task_helpers/scale.rb +0 -86
- data/lib/model_existence_validator.rb +0 -7
data/docker-compose.yml
CHANGED
@@ -1,15 +1,20 @@
|
|
1
1
|
version: '2'
|
2
2
|
|
3
3
|
services:
|
4
|
-
|
4
|
+
app:
|
5
5
|
build:
|
6
6
|
context: .
|
7
|
-
dockerfile: Dockerfile
|
7
|
+
dockerfile: Dockerfile
|
8
8
|
environment:
|
9
9
|
AWS_ACCESS_KEY_ID: x
|
10
10
|
AWS_SECRET_ACCESS_KEY: x
|
11
11
|
AWS_REGION: us-west-2
|
12
12
|
DYNAMO_ENDPOINT: http://dynamo:8000
|
13
|
+
RAILS_ENV: test
|
14
|
+
logging:
|
15
|
+
options:
|
16
|
+
max-file: '1'
|
17
|
+
max-size: 5m
|
13
18
|
links:
|
14
19
|
- dynamo
|
15
20
|
|
data/dynamo-record.gemspec
CHANGED
@@ -1,38 +1,50 @@
|
|
1
|
-
# coding: utf-8
|
2
1
|
lib = File.expand_path('../lib', __FILE__)
|
3
2
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
-
require 'dynamo
|
3
|
+
require 'dynamo/record/version'
|
5
4
|
|
6
|
-
Gem::Specification.new do |
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
'Augusto Callejas', 'Frank Murphy']
|
13
|
-
spec.email = ['dmcclellan@instructure.com', 'rtaylor@instructure.com', 'bpetty@instructure.com',
|
14
|
-
'mbd@instructure.com', 'mphillips@instructure.com', 'acallejas@instructure.com',
|
15
|
-
'fmurphy@instructure.com']
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = 'dynamo-record'
|
7
|
+
s.version = Dynamo::Record::VERSION
|
8
|
+
s.summary = 'Extensions to Aws::Record for working with DynamoDB.'
|
9
|
+
s.homepage = 'https://github.com/instructure/dynamo-record'
|
10
|
+
s.license = 'MIT'
|
16
11
|
|
17
|
-
|
18
|
-
|
12
|
+
s.authors = [
|
13
|
+
'Davis McClellan',
|
14
|
+
'Ryan Taylor',
|
15
|
+
'Bryan Petty',
|
16
|
+
'Michael Brewer-Davis',
|
17
|
+
'Marc Phillips',
|
18
|
+
'Augusto Callejas',
|
19
|
+
'Frank Murphy'
|
20
|
+
]
|
21
|
+
s.email = [
|
22
|
+
'dmcclellan@instructure.com',
|
23
|
+
'rtaylor@instructure.com',
|
24
|
+
'bpetty@instructure.com',
|
25
|
+
'mbd@instructure.com',
|
26
|
+
'mphillips@instructure.com',
|
27
|
+
'acallejas@instructure.com',
|
28
|
+
'fmurphy@instructure.com'
|
29
|
+
]
|
19
30
|
|
20
|
-
|
31
|
+
s.files = `git ls-files -z`.split("\x0").reject do |f|
|
21
32
|
f.match(%r{^(test|spec|features)/})
|
22
33
|
end
|
23
|
-
|
24
|
-
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
25
|
-
spec.require_paths = ['lib']
|
34
|
+
s.require_paths = ['lib']
|
26
35
|
|
27
|
-
|
28
|
-
|
36
|
+
s.add_dependency 'aws-record', '~> 1.1'
|
37
|
+
s.add_dependency 'activemodel', '>= 4.2', '< 5.2'
|
38
|
+
s.add_dependency 'railties', '>= 4.2', '< 5.2'
|
29
39
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
40
|
+
s.add_development_dependency 'activesupport', '>= 4.2', '< 5.2'
|
41
|
+
s.add_development_dependency 'bundler', '~> 1.15'
|
42
|
+
s.add_development_dependency 'byebug', '~> 9.0'
|
43
|
+
s.add_development_dependency 'combustion', '~> 0.6.0'
|
44
|
+
s.add_development_dependency 'rake', '~> 12.0'
|
45
|
+
s.add_development_dependency 'rspec', '~> 3.6'
|
46
|
+
s.add_development_dependency 'rubocop', '~> 0.50.0'
|
47
|
+
s.add_development_dependency 'simplecov', '~> 0.14'
|
48
|
+
s.add_development_dependency 'webmock', '~> 2.1'
|
49
|
+
s.add_development_dependency 'wwtd', '~> 1.3'
|
38
50
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'active_model'
|
2
|
+
require 'aws-record'
|
3
|
+
require 'rails/railtie'
|
4
|
+
|
5
|
+
require 'dynamo/record/marshalers'
|
6
|
+
require 'dynamo/record/model'
|
7
|
+
require 'dynamo/record/railtie'
|
8
|
+
require 'dynamo/record/model_existence_validator'
|
9
|
+
require 'dynamo/record/table_migration'
|
10
|
+
require 'dynamo/record/version'
|
11
|
+
|
12
|
+
require 'dynamo/record/task_helpers/cleanup'
|
13
|
+
require 'dynamo/record/task_helpers/drop_all_tables'
|
14
|
+
require 'dynamo/record/task_helpers/drop_table'
|
15
|
+
require 'dynamo/record/task_helpers/list_tables'
|
16
|
+
require 'dynamo/record/task_helpers/migration_runner'
|
17
|
+
require 'dynamo/record/task_helpers/scale'
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Dynamo
|
2
|
+
module Record
|
3
|
+
module Marshalers
|
4
|
+
COMPOSITE_DELIMETER = '|'.freeze
|
5
|
+
|
6
|
+
def self.included(sub_class)
|
7
|
+
sub_class.extend(ClassMethods)
|
8
|
+
super(sub_class)
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def composite_integer_attr(name, opts = {})
|
13
|
+
composite_attr(name, opts)
|
14
|
+
define_readers(name, opts[:parts], :to_i) if opts.key? :parts
|
15
|
+
end
|
16
|
+
|
17
|
+
def composite_string_attr(name, opts = {})
|
18
|
+
composite_attr(name, opts)
|
19
|
+
define_readers(name, opts[:parts], :to_s) if opts.key? :parts
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def composite_attr(name, opts = {})
|
25
|
+
opts[:dynamodb_type] = 'S'
|
26
|
+
|
27
|
+
# It is very unfortunate that Aws::Record used `attr`
|
28
|
+
# rubocop:disable Style/Attr
|
29
|
+
attr(name, Aws::Record::Marshalers::StringMarshaler.new(opts), opts)
|
30
|
+
# rubocop:enable Style/Attr
|
31
|
+
end
|
32
|
+
|
33
|
+
def define_readers(name, parts, cast_function)
|
34
|
+
parts.each_with_index do |part, i|
|
35
|
+
raise "#{part} already defined" unless parts.find_index(part) == i
|
36
|
+
next if method_defined?(part)
|
37
|
+
define_method(part) do
|
38
|
+
# @data is used internally by Aws::Record to store all of the attributes
|
39
|
+
@data.get_attribute(name).split(COMPOSITE_DELIMETER)[i].send(cast_function)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
module Dynamo
|
2
|
+
module Record
|
3
|
+
module Model
|
4
|
+
COMPOSITE_DELIMITER = '|'.freeze
|
5
|
+
|
6
|
+
def self.included(klass)
|
7
|
+
klass.include(Aws::Record)
|
8
|
+
klass.include(Dynamo::Record::Marshalers)
|
9
|
+
|
10
|
+
klass.extend ClassMethods
|
11
|
+
klass.send :prepend, InstanceMethods
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
def table_name
|
16
|
+
[Rails.configuration.dynamo['prefix'], name.tableize].join('-').tr('/', '.')
|
17
|
+
end
|
18
|
+
|
19
|
+
def scan
|
20
|
+
raise 'no scanning in production' if Rails.env.production?
|
21
|
+
super
|
22
|
+
end
|
23
|
+
|
24
|
+
def find(opts)
|
25
|
+
super(opts).tap do |record|
|
26
|
+
unless record
|
27
|
+
name = self.name.demodulize
|
28
|
+
conditions = opts.map { |k, v| "#{k}=#{v}" }.join(', ')
|
29
|
+
error = "Couldn't find #{name} with #{conditions}"
|
30
|
+
raise Aws::Record::Errors::NotFound, error
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def find_all_by_hash_key(hash_key_value, opts = {})
|
36
|
+
find_all_by_index_and_hash_key(hash_key, hash_key_value, opts)
|
37
|
+
end
|
38
|
+
|
39
|
+
def find_all_by_gsi_hash_key(gsi_name, hash_key_value, opts = {})
|
40
|
+
hash_key_name = global_secondary_indexes[gsi_name.to_sym][:hash_key]
|
41
|
+
find_all_by_index_and_hash_key(hash_key_name, hash_key_value, opts, gsi_name.to_s)
|
42
|
+
end
|
43
|
+
|
44
|
+
def find_all_by_gsi_hash_and_range_keys(gsi_name, hash_key_value, range_key_value)
|
45
|
+
gsi_config = global_secondary_indexes[gsi_name.to_sym]
|
46
|
+
find_all_by_index_hash_and_range_keys(
|
47
|
+
hash_config: { name: gsi_config[:hash_key], value: hash_key_value },
|
48
|
+
range_config: { name: gsi_config[:range_key], value: range_key_value },
|
49
|
+
index_name: gsi_name.to_s
|
50
|
+
)
|
51
|
+
end
|
52
|
+
|
53
|
+
def find_all_by_lsi_hash_key(lsi_name, hash_key_value, opts = {})
|
54
|
+
hash_key_name = local_secondary_indexes[lsi_name.to_sym][:hash_key]
|
55
|
+
find_all_by_index_and_hash_key(hash_key_name, hash_key_value, opts, lsi_name.to_s)
|
56
|
+
end
|
57
|
+
|
58
|
+
def find_all_by_lsi_hash_and_range_keys(lsi_name, hash_key_value, range_key_value)
|
59
|
+
lsi_config = local_secondary_indexes[lsi_name.to_sym]
|
60
|
+
find_all_by_index_hash_and_range_keys(
|
61
|
+
hash_config: { name: lsi_config[:hash_key], value: hash_key_value },
|
62
|
+
range_config: { name: lsi_config[:range_key], value: range_key_value },
|
63
|
+
index_name: lsi_name.to_s
|
64
|
+
)
|
65
|
+
end
|
66
|
+
|
67
|
+
def find_all_by_index_and_hash_key(hash_key_name, hash_key_value, opts = {}, index_name = nil)
|
68
|
+
query_options = {
|
69
|
+
select: 'ALL_ATTRIBUTES',
|
70
|
+
key_condition_expression: "#{hash_key_name} = :hash_key_value",
|
71
|
+
expression_attribute_values: {
|
72
|
+
':hash_key_value': hash_key_value
|
73
|
+
},
|
74
|
+
scan_index_forward: true
|
75
|
+
}
|
76
|
+
query_options[:index_name] = index_name if index_name
|
77
|
+
query_options.merge!(opts)
|
78
|
+
query(query_options)
|
79
|
+
end
|
80
|
+
|
81
|
+
def find_all_by_index_hash_and_range_keys(hash_config:, range_config:, index_name: nil,
|
82
|
+
scan_index_forward: true, limit: nil)
|
83
|
+
range_expression = range_config[:expression] || "#{range_config[:name]} = :rkv"
|
84
|
+
query_options = {
|
85
|
+
select: 'ALL_ATTRIBUTES',
|
86
|
+
key_condition_expression: "#{hash_config[:name]} = :hkv AND #{range_expression}",
|
87
|
+
expression_attribute_values: {
|
88
|
+
':hkv': hash_config[:value],
|
89
|
+
':rkv': range_config[:value]
|
90
|
+
},
|
91
|
+
scan_index_forward: scan_index_forward,
|
92
|
+
limit: limit
|
93
|
+
}
|
94
|
+
query_options[:index_name] = index_name if index_name
|
95
|
+
query(query_options)
|
96
|
+
end
|
97
|
+
|
98
|
+
def composite_key(*args)
|
99
|
+
args.join(COMPOSITE_DELIMITER)
|
100
|
+
end
|
101
|
+
|
102
|
+
def split_composite(string)
|
103
|
+
string.split(COMPOSITE_DELIMITER)
|
104
|
+
end
|
105
|
+
|
106
|
+
# TODO: Create a batch save method.
|
107
|
+
end
|
108
|
+
|
109
|
+
module InstanceMethods
|
110
|
+
def read_attribute_for_serialization(attribute)
|
111
|
+
send(attribute)
|
112
|
+
end
|
113
|
+
|
114
|
+
def attribute_hash
|
115
|
+
attrs = self.class.attributes.attributes
|
116
|
+
attr_keys = attrs.keys
|
117
|
+
hash = {}
|
118
|
+
|
119
|
+
attr_keys.each do |key|
|
120
|
+
hash[key] = attrs[key].type_cast(send(key))
|
121
|
+
end
|
122
|
+
hash
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Dynamo
|
2
|
+
module Record
|
3
|
+
class ModelExistenceValidator < ActiveModel::EachValidator
|
4
|
+
def validate_each(record, attribute, value)
|
5
|
+
return if options[:model].exists? value
|
6
|
+
record.errors[attribute] << (options[:message] || "#{attribute}:#{value} is not a valid #{options[:model]}")
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Dynamo
|
2
|
+
module Record
|
3
|
+
class TableMigration
|
4
|
+
def self.table_config_check
|
5
|
+
if migrate_table?
|
6
|
+
table_config.migrate!
|
7
|
+
return :migrated
|
8
|
+
end
|
9
|
+
:exists
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.migrate(model)
|
13
|
+
migration = Aws::Record::TableMigration.new(model)
|
14
|
+
begin
|
15
|
+
migration.client.describe_table(table_name: model.table_name)
|
16
|
+
return :exists
|
17
|
+
rescue Aws::DynamoDB::Errors::ResourceNotFoundException
|
18
|
+
yield migration
|
19
|
+
migration.wait_until_available
|
20
|
+
return :migrated
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.migrate_updates(model)
|
25
|
+
migration = Aws::Record::TableMigration.new(model)
|
26
|
+
yield migration
|
27
|
+
migration.wait_until_available
|
28
|
+
:updated
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.add_stream(model)
|
32
|
+
migrate_updates(model) do |migration|
|
33
|
+
migration.update!(
|
34
|
+
stream_specification: {
|
35
|
+
stream_enabled: true,
|
36
|
+
stream_view_type: 'NEW_IMAGE'
|
37
|
+
}
|
38
|
+
)
|
39
|
+
end
|
40
|
+
rescue Aws::DynamoDB::Errors::ValidationException => e
|
41
|
+
return e.message if e.message == 'Table already has an enabled stream'
|
42
|
+
raise e
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.migrate_table?(update_provisioned_throughput = false)
|
46
|
+
unless update_provisioned_throughput
|
47
|
+
table_name = table_config.instance_values['model_class'].table_name
|
48
|
+
described_table = table_config.client.describe_table table_name: table_name
|
49
|
+
provisioned_throughput = described_table.table.provisioned_throughput
|
50
|
+
table_config.read_capacity_units provisioned_throughput.read_capacity_units
|
51
|
+
table_config.write_capacity_units provisioned_throughput.write_capacity_units
|
52
|
+
end
|
53
|
+
!table_config.exact_match?
|
54
|
+
rescue Aws::DynamoDB::Errors::ResourceNotFoundException
|
55
|
+
true
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Dynamo
|
2
|
+
module Record
|
3
|
+
module TaskHelpers
|
4
|
+
class Cleanup
|
5
|
+
def self.run
|
6
|
+
raise 'Task not available on production' if Rails.env.production?
|
7
|
+
Dir[Rails.root.join('app', 'models', '*.rb').to_s].each do |filename|
|
8
|
+
delete_by_class(filename)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.delete_by_class(filename)
|
13
|
+
klass = File.basename(filename, '.rb').camelize.constantize
|
14
|
+
return unless klass.included_modules.include? Dynamo::Record::Model
|
15
|
+
Rails.logger.info "Deleting all items in table: #{klass}"
|
16
|
+
klass.scan.each(&:delete!)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Dynamo
|
2
|
+
module Record
|
3
|
+
module TaskHelpers
|
4
|
+
class DropAllTables
|
5
|
+
def self.run(override = false)
|
6
|
+
raise 'Task not available on production' if Rails.env.production?
|
7
|
+
env = Rails.env
|
8
|
+
dynamodb = Aws::DynamoDB::Client.new
|
9
|
+
tables = dynamodb.list_tables
|
10
|
+
tables.table_names.map do |t|
|
11
|
+
next unless t.include?(env) || override
|
12
|
+
dt = dynamodb.delete_table(table_name: t)
|
13
|
+
dt ? "Deleted: #{t}" : "Delete failed: #{t}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Dynamo
|
2
|
+
module Record
|
3
|
+
module TaskHelpers
|
4
|
+
class DropTable
|
5
|
+
def self.run(table_name)
|
6
|
+
dynamodb = Aws::DynamoDB::Client.new
|
7
|
+
resp = dynamodb.delete_table(table_name: table_name)
|
8
|
+
"Deleted: #{resp.table_description.table_name}"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Dynamo
|
2
|
+
module Record
|
3
|
+
module TaskHelpers
|
4
|
+
class ListTables
|
5
|
+
def self.run
|
6
|
+
dynamodb = Aws::DynamoDB::Client.new
|
7
|
+
tables = dynamodb.list_tables
|
8
|
+
tables.table_names.select do |tn|
|
9
|
+
tn.starts_with?(Rails.configuration.dynamo['prefix'])
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|