dynamo-record 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 77cdbe44e4be22d2294565970b108d66c48ffb87
4
+ data.tar.gz: 70653d3d5cddc536eeefb74d47ea8522665f4b3b
5
+ SHA512:
6
+ metadata.gz: b7fe3b6bd23079f734531545ee1ae42079d5053b99b058d9cc1c96a0ba78804de27b8f3fca6ed7609d299d7c20bf0bfd21c38b34823099b8da6cf254197c2537
7
+ data.tar.gz: ec96773a915b361673ee1af591ea13a84b8cb5724cc5fc76168412d75cb43bd590d7b721462c2a682a677237459bc0449ee0c5e8f660806aa33097e3157c3731
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
@@ -0,0 +1,81 @@
1
+ Rails:
2
+ Enabled: true
3
+
4
+ Rails/HttpPositionalArguments:
5
+ # Renable once we are on Rails v5
6
+ Enabled: false
7
+
8
+ AllCops:
9
+ TargetRubyVersion: 2.3
10
+
11
+ Metrics/ClassLength:
12
+ Max: 200 # Default: 100
13
+
14
+ Metrics/LineLength:
15
+ Max: 120 # Default: 80
16
+
17
+ Metrics/MethodLength:
18
+ Max: 20 # Default: 10
19
+
20
+ Metrics/BlockLength:
21
+ Max: 30
22
+ Exclude:
23
+ - 'spec/**/*.rb'
24
+
25
+ Style/AlignParameters:
26
+ # Alignment of parameters in multi-line method calls.
27
+ #
28
+ # The `with_fixed_indentation` style aligns the following lines with one
29
+ # level of indentation relative to the start of the line with the method call.
30
+ #
31
+ # method_call(a,
32
+ # b)
33
+ EnforcedStyle: with_fixed_indentation
34
+
35
+ Style/ClassAndModuleChildren:
36
+ # Checks the style of children definitions at classes and modules.
37
+ #
38
+ # Basically there are two different styles:
39
+ #
40
+ # `nested` - have each child on a separate line
41
+ # class Foo
42
+ # class Bar
43
+ # end
44
+ # end
45
+ #
46
+ # `compact` - combine definitions as much as possible
47
+ # class Foo::Bar
48
+ # end
49
+ #
50
+ # The compact style is only forced, for classes / modules with one child.
51
+ EnforcedStyle: nested
52
+ Enabled: false
53
+
54
+ Style/Documentation:
55
+ # This cop checks for missing top-level documentation of classes and modules.
56
+ # Classes with no body and namespace modules are exempt from the check.
57
+ # Namespace modules are modules that have nothing in their bodies except
58
+ # classes or other modules.
59
+ Enabled: false
60
+
61
+ Lint/EndAlignment:
62
+ AlignWith: variable
63
+
64
+ Style/CaseIndentation:
65
+ IndentWhenRelativeTo: end
66
+
67
+ Style/FrozenStringLiteralComment:
68
+ # `when_needed` will add the frozen string literal comment to files
69
+ # only when the `TargetRubyVersion` is set to 2.3+.
70
+ # `always` will always add the frozen string literal comment to a file
71
+ # regardless of the Ruby version or if `freeze` or `<<` are called on a
72
+ # string literal. If you run code against multiple versions of Ruby, it is
73
+ # possible that this will create errors in Ruby 2.3.0+.
74
+ #
75
+ # See: https://wyeworks.com/blog/2015/12/1/immutable-strings-in-ruby-2-dot-3
76
+ EnforcedStyle: when_needed
77
+ Enabled: false
78
+
79
+ Style/NumericPredicate:
80
+ Exclude:
81
+ - 'spec/**/*'
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.0
5
+ before_install: gem install bundler -v 1.14.6
@@ -0,0 +1,23 @@
1
+ FROM instructure/ruby:2.3
2
+
3
+ ENV APP_HOME "/usr/src/app/"
4
+
5
+ USER root
6
+
7
+ COPY dynamo-record.gemspec Gemfile Gemfile.lock $APP_HOME
8
+ RUN mkdir -p $APP_HOME/lib/dynamo-record/record
9
+ COPY lib/dynamo-record/record/version.rb $APP_HOME/lib/dynamo-record/record
10
+ RUN chown -R docker:docker $APP_HOME
11
+
12
+ USER docker
13
+ RUN gem install bundler
14
+ RUN bundle install --quiet --jobs 8
15
+ USER root
16
+
17
+ COPY . $APP_HOME
18
+ RUN mkdir -p $APP_HOME/coverage && \
19
+ mkdir -p $APP_HOME/spec/internal/log && \
20
+ chown -R docker:docker $APP_HOME
21
+ USER docker
22
+
23
+ CMD ["bundle", "exec", "rspec"]
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in dynamo-record.gemspec
4
+ gemspec
@@ -0,0 +1,163 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ dynamo-record (0.1.0)
5
+ aws-record (~> 1.1)
6
+ rails (~> 4.2)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ actionmailer (4.2.8)
12
+ actionpack (= 4.2.8)
13
+ actionview (= 4.2.8)
14
+ activejob (= 4.2.8)
15
+ mail (~> 2.5, >= 2.5.4)
16
+ rails-dom-testing (~> 1.0, >= 1.0.5)
17
+ actionpack (4.2.8)
18
+ actionview (= 4.2.8)
19
+ activesupport (= 4.2.8)
20
+ rack (~> 1.6)
21
+ rack-test (~> 0.6.2)
22
+ rails-dom-testing (~> 1.0, >= 1.0.5)
23
+ rails-html-sanitizer (~> 1.0, >= 1.0.2)
24
+ actionview (4.2.8)
25
+ activesupport (= 4.2.8)
26
+ builder (~> 3.1)
27
+ erubis (~> 2.7.0)
28
+ rails-dom-testing (~> 1.0, >= 1.0.5)
29
+ rails-html-sanitizer (~> 1.0, >= 1.0.3)
30
+ activejob (4.2.8)
31
+ activesupport (= 4.2.8)
32
+ globalid (>= 0.3.0)
33
+ activemodel (4.2.8)
34
+ activesupport (= 4.2.8)
35
+ builder (~> 3.1)
36
+ activerecord (4.2.8)
37
+ activemodel (= 4.2.8)
38
+ activesupport (= 4.2.8)
39
+ arel (~> 6.0)
40
+ activesupport (4.2.8)
41
+ i18n (~> 0.7)
42
+ minitest (~> 5.1)
43
+ thread_safe (~> 0.3, >= 0.3.4)
44
+ tzinfo (~> 1.1)
45
+ addressable (2.5.1)
46
+ public_suffix (~> 2.0, >= 2.0.2)
47
+ arel (6.0.4)
48
+ aws-record (1.1.0)
49
+ aws-sdk-resources (~> 2.0)
50
+ aws-sdk-core (2.9.11)
51
+ aws-sigv4 (~> 1.0)
52
+ jmespath (~> 1.0)
53
+ aws-sdk-resources (2.9.11)
54
+ aws-sdk-core (= 2.9.11)
55
+ aws-sigv4 (1.0.0)
56
+ builder (3.2.3)
57
+ byebug (9.0.6)
58
+ combustion (0.6.0)
59
+ activesupport (>= 3.0.0)
60
+ railties (>= 3.0.0)
61
+ thor (>= 0.14.6)
62
+ concurrent-ruby (1.0.5)
63
+ crack (0.4.3)
64
+ safe_yaml (~> 1.0.0)
65
+ diff-lcs (1.3)
66
+ docile (1.1.5)
67
+ erubis (2.7.0)
68
+ globalid (0.4.0)
69
+ activesupport (>= 4.2.0)
70
+ hashdiff (0.3.2)
71
+ i18n (0.8.1)
72
+ jmespath (1.3.1)
73
+ json (2.1.0)
74
+ loofah (2.0.3)
75
+ nokogiri (>= 1.5.9)
76
+ mail (2.6.4)
77
+ mime-types (>= 1.16, < 4)
78
+ mime-types (3.1)
79
+ mime-types-data (~> 3.2015)
80
+ mime-types-data (3.2016.0521)
81
+ mini_portile2 (2.1.0)
82
+ minitest (5.10.1)
83
+ nokogiri (1.7.1)
84
+ mini_portile2 (~> 2.1.0)
85
+ public_suffix (2.0.5)
86
+ rack (1.6.5)
87
+ rack-test (0.6.3)
88
+ rack (>= 1.0)
89
+ rails (4.2.8)
90
+ actionmailer (= 4.2.8)
91
+ actionpack (= 4.2.8)
92
+ actionview (= 4.2.8)
93
+ activejob (= 4.2.8)
94
+ activemodel (= 4.2.8)
95
+ activerecord (= 4.2.8)
96
+ activesupport (= 4.2.8)
97
+ bundler (>= 1.3.0, < 2.0)
98
+ railties (= 4.2.8)
99
+ sprockets-rails
100
+ rails-deprecated_sanitizer (1.0.3)
101
+ activesupport (>= 4.2.0.alpha)
102
+ rails-dom-testing (1.0.8)
103
+ activesupport (>= 4.2.0.beta, < 5.0)
104
+ nokogiri (~> 1.6)
105
+ rails-deprecated_sanitizer (>= 1.0.1)
106
+ rails-html-sanitizer (1.0.3)
107
+ loofah (~> 2.0)
108
+ railties (4.2.8)
109
+ actionpack (= 4.2.8)
110
+ activesupport (= 4.2.8)
111
+ rake (>= 0.8.7)
112
+ thor (>= 0.18.1, < 2.0)
113
+ rake (10.5.0)
114
+ rspec (3.5.0)
115
+ rspec-core (~> 3.5.0)
116
+ rspec-expectations (~> 3.5.0)
117
+ rspec-mocks (~> 3.5.0)
118
+ rspec-core (3.5.4)
119
+ rspec-support (~> 3.5.0)
120
+ rspec-expectations (3.5.0)
121
+ diff-lcs (>= 1.2.0, < 2.0)
122
+ rspec-support (~> 3.5.0)
123
+ rspec-mocks (3.5.0)
124
+ diff-lcs (>= 1.2.0, < 2.0)
125
+ rspec-support (~> 3.5.0)
126
+ rspec-support (3.5.0)
127
+ safe_yaml (1.0.4)
128
+ simplecov (0.14.1)
129
+ docile (~> 1.1.0)
130
+ json (>= 1.8, < 3)
131
+ simplecov-html (~> 0.10.0)
132
+ simplecov-html (0.10.0)
133
+ sprockets (3.7.1)
134
+ concurrent-ruby (~> 1.0)
135
+ rack (> 1, < 3)
136
+ sprockets-rails (3.2.0)
137
+ actionpack (>= 4.0)
138
+ activesupport (>= 4.0)
139
+ sprockets (>= 3.0.0)
140
+ thor (0.19.4)
141
+ thread_safe (0.3.6)
142
+ tzinfo (1.2.3)
143
+ thread_safe (~> 0.1)
144
+ webmock (2.3.2)
145
+ addressable (>= 2.3.6)
146
+ crack (>= 0.3.2)
147
+ hashdiff
148
+
149
+ PLATFORMS
150
+ ruby
151
+
152
+ DEPENDENCIES
153
+ bundler (~> 1.14)
154
+ byebug (~> 9.0)
155
+ combustion (~> 0.6.0)
156
+ dynamo-record!
157
+ rake (~> 10.0)
158
+ rspec (~> 3.0)
159
+ simplecov (~> 0.12)
160
+ webmock (~> 2.1)
161
+
162
+ BUNDLED WITH
163
+ 1.14.6
@@ -0,0 +1,110 @@
1
+ # DynamoRecord
2
+
3
+ Provides helpful rake tasks and model extensions on top of [aws-record](https://github.com/aws/aws-sdk-ruby-record).
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'dynamo-record'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install dynamo-record
20
+
21
+ ## Usage
22
+
23
+ ### Models
24
+
25
+ In `app/models`, create a class that includes `DynamoRecord::Model` and contains one or more of the following attribute declarations:
26
+
27
+ * `integer_attr`
28
+ * `float_attr`
29
+ * `map_attr`
30
+ * `composite_string_attr`
31
+ * `composite_integer_attr`
32
+
33
+ An example file:
34
+
35
+ ```
36
+ class Model1
37
+ include DynamoRecord::Model
38
+ composite_string_attr(
39
+ :model_key,
40
+ hash_key: true,
41
+ parts: [:model_id, :account_id]
42
+ )
43
+ integer_attr :position, range_key: true
44
+ string_attr :user_id
45
+ float_attr :score
46
+ map_attr :map_value
47
+ composite_string_attr :quiz_key, parts: [:quiz_id]
48
+ end
49
+ ```
50
+
51
+ The partition key is labeled with `hash_key: true`. The sort key is labeled with `range_key: true`.
52
+
53
+ A secondary index can be defined as follows:
54
+
55
+ ```
56
+ global_secondary_index(
57
+ :secondary_idx,
58
+ hash_key: :user_id,
59
+ range_key: :score,
60
+ projection: {
61
+ projection_type: 'INCLUDE',
62
+ non_key_attributes: [
63
+ :map_value
64
+ ]
65
+ }
66
+ )
67
+ ```
68
+
69
+ The full documentation for `projection_type` and `non_key_attributes` can be found [here](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-dynamodb-projectionobject.html).
70
+
71
+ ### Migrations
72
+
73
+ Dynamo migration files are stored in `db/dynamo_migrate`. The name of the file follows the style of standard Rails migration files like `YYYYMMDDHHMMSS_create_model_1.rb`. A migration file that creates a Dynamo table looks like:
74
+
75
+ ```
76
+ module DynamoMigrate
77
+ class CreateModel1 < DynamoRecord::TableMigration
78
+ def self.up
79
+ migrate(Model1) do |migration|
80
+ migration.create!(
81
+ provisioned_throughput: {
82
+ read_capacity_units: 1,
83
+ write_capacity_units: 1
84
+ },
85
+ global_secondary_index_throughput: {
86
+ secondary_idx: {
87
+ read_capacity_units: 1,
88
+ write_capacity_units: 1
89
+ }
90
+ }
91
+ )
92
+ end
93
+ end
94
+ end
95
+ end
96
+ ```
97
+
98
+ Note that the throughput of the table can be configured separately from the throughput of the secondary index.
99
+
100
+ A migration file that creates a Dynamo stream looks like:
101
+
102
+ ```
103
+ module DynamoMigrate
104
+ class AddModel1Stream < DynamoRecord::TableMigration
105
+ def self.update
106
+ add_stream(Model1)
107
+ end
108
+ end
109
+ end
110
+ ```
@@ -0,0 +1,28 @@
1
+ #!/bin/bash
2
+
3
+ export COMPOSE_PROJECT_NAME=dynamorecord
4
+ export COMPOSE_FILE=docker-compose.yml
5
+
6
+ function cleanup() {
7
+ exit_code=$?
8
+ set +e
9
+ docker-compose stop test
10
+ docker-compose rm -fa test
11
+ docker rmi -f $(docker images -qf "dangling=true") &>/dev/null
12
+ exit $exit_code
13
+ }
14
+ trap cleanup INT TERM EXIT
15
+
16
+ set -e
17
+
18
+ docker-compose build --pull
19
+ docker-compose run --name test test
20
+ docker cp test:/usr/src/app/coverage .
21
+
22
+ #echo "Reporting test coverage"
23
+ #if [[ $? -eq 0 && -a 'coverage/.last_run.json' ]]; then
24
+ # ruby_coverage=$(cat coverage/api/.last_run.json | grep 'covered_percent' | egrep -o '[0-9]+(\.[0-9]+)?')
25
+ # echo "Sending to gergich"
26
+ # gergich message ":ruby: <http://jenkins.instructure.com/job/dynamo-record/${BUILD_ID}/|${ruby_coverage}%>"
27
+ # gergich publish
28
+ #fi
@@ -0,0 +1,6 @@
1
+ version: '2'
2
+
3
+ services:
4
+ test:
5
+ volumes:
6
+ - .:/usr/src/app
@@ -0,0 +1,17 @@
1
+ version: '2'
2
+
3
+ services:
4
+ test:
5
+ build:
6
+ context: .
7
+ dockerfile: Dockerfile.test
8
+ environment:
9
+ AWS_ACCESS_KEY_ID: x
10
+ AWS_SECRET_ACCESS_KEY: x
11
+ AWS_REGION: us-west-2
12
+ DYNAMO_ENDPOINT: http://dynamo:8000
13
+ links:
14
+ - dynamo
15
+
16
+ dynamo:
17
+ image: instructure/dynamodb
@@ -0,0 +1,35 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'dynamo-record/record/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'dynamo-record'
8
+ spec.version = DynamoRecord::Record::VERSION
9
+ spec.license = 'MIT'
10
+ spec.homepage = 'https://instructure.com'
11
+ spec.authors = ['Davis McClellan', 'Ryan Taylor', 'Bryan Petty', 'Michael Brewer-Davis', 'Marc Phillips', 'Augusto Callejas']
12
+ spec.email = ['dmcclellan@instructure.com', 'rtaylor@instructure.com', 'bpetty@instructure.com',
13
+ 'mbd@instructure.com', 'mphillips@instructure.com', 'acallejas@instructure.com']
14
+
15
+ spec.summary = 'Extensions for working with dynamo via aws-record'
16
+ spec.description = 'A set of extensions simplifying database operations in aws-record'
17
+
18
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
19
+ f.match(%r{^(test|spec|features)/})
20
+ end
21
+ spec.bindir = 'exe'
22
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
+ spec.require_paths = ['lib']
24
+
25
+ spec.add_dependency 'aws-record', '~> 1.1'
26
+ spec.add_dependency 'rails', '~> 4.2'
27
+
28
+ spec.add_development_dependency 'bundler', '~> 1.14'
29
+ spec.add_development_dependency 'byebug', '~> 9.0'
30
+ spec.add_development_dependency 'combustion', '~> 0.6.0'
31
+ spec.add_development_dependency 'rake', '~> 10.0'
32
+ spec.add_development_dependency 'rspec', '~> 3.0'
33
+ spec.add_development_dependency 'simplecov', '~> 0.12'
34
+ spec.add_development_dependency 'webmock', '~> 2.1'
35
+ end
@@ -0,0 +1,4 @@
1
+ Gem.find_files("#{File.dirname(__FILE__)}/dynamo-record/**/*.rb").each do |path|
2
+ require path.gsub(/\.rb$/, '')
3
+ end
4
+ require 'model_existence_validator'
@@ -0,0 +1,44 @@
1
+ module DynamoRecord
2
+ module Marshalers
3
+ COMPOSITE_DELIMETER = '|'.freeze
4
+
5
+ def self.included(sub_class)
6
+ sub_class.extend(ClassMethods)
7
+ super(sub_class)
8
+ end
9
+
10
+ module ClassMethods
11
+ def composite_integer_attr(name, opts = {})
12
+ composite_attr(name, opts)
13
+ define_readers(name, opts[:parts], :to_i) if opts.key? :parts
14
+ end
15
+
16
+ def composite_string_attr(name, opts = {})
17
+ composite_attr(name, opts)
18
+ define_readers(name, opts[:parts], :to_s) if opts.key? :parts
19
+ end
20
+
21
+ private
22
+
23
+ def composite_attr(name, opts = {})
24
+ opts[:dynamodb_type] = 'S'
25
+
26
+ # It is very unfortunate that Aws::Record used `attr`
27
+ # rubocop:disable Style/Attr
28
+ attr(name, Aws::Record::Marshalers::StringMarshaler.new(opts), opts)
29
+ # rubocop:enable Style/Attr
30
+ end
31
+
32
+ def define_readers(name, parts, cast_function, allowed_repeats = [:account_uuid])
33
+ parts.each_with_index do |part, i|
34
+ raise "#{part} already defined" unless parts.find_index(part) == i
35
+ next if method_defined?(part)
36
+ define_method(part) do
37
+ # @data is used internally by Aws::Record to store all of the attributes
38
+ @data.get_attribute(name).split(COMPOSITE_DELIMETER)[i].send(cast_function)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,122 @@
1
+ require 'aws-record'
2
+
3
+ module DynamoRecord::Model
4
+ COMPOSITE_DELIMITER = '|'.freeze
5
+
6
+ def self.included(klass)
7
+ klass.include(Aws::Record)
8
+ klass.include(DynamoRecord::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_index_and_hash_key(hash_key_name, hash_key_value, opts = {}, index_name = nil)
54
+ query_options = {
55
+ select: 'ALL_ATTRIBUTES',
56
+ key_condition_expression: "#{hash_key_name} = :hash_key_value",
57
+ expression_attribute_values: {
58
+ ':hash_key_value': hash_key_value
59
+ },
60
+ scan_index_forward: true
61
+ }
62
+ query_options[:index_name] = index_name if index_name
63
+ query_options.merge!(opts)
64
+ query(query_options)
65
+ end
66
+
67
+ def find_all_by_index_hash_and_range_keys(hash_config:, range_config:, index_name: nil,
68
+ scan_index_forward: true, limit: nil)
69
+ query_options = {
70
+ select: 'ALL_ATTRIBUTES',
71
+ key_condition_expression: "#{hash_config[:name]} = :hkv AND #{range_config[:name]} = :rkv",
72
+ expression_attribute_values: {
73
+ ':hkv': hash_config[:value],
74
+ ':rkv': range_config[:value]
75
+ },
76
+ scan_index_forward: scan_index_forward,
77
+ limit: limit
78
+ }
79
+ query_options[:index_name] = index_name if index_name
80
+ query(query_options)
81
+ end
82
+
83
+ def composite_key(*args)
84
+ args.join(COMPOSITE_DELIMITER)
85
+ end
86
+
87
+ def split_composite(string)
88
+ string.split(COMPOSITE_DELIMITER)
89
+ end
90
+
91
+ def find_all_by_model(model, opts = {})
92
+ find_all_by_hash_key(model.dynamo_key, opts)
93
+ end
94
+
95
+ def find_all_by_model_paginated(model, opts = {})
96
+ find_all_by_model(model, pagination_opts(opts))
97
+ end
98
+
99
+ def pagination_opts(opts = {})
100
+ { limit: opts[:limit], exclusive_start_key: opts[:exclusive_start_key] }
101
+ end
102
+
103
+ # TODO: Create a batch save method.
104
+ end
105
+
106
+ module InstanceMethods
107
+ def read_attribute_for_serialization(attribute)
108
+ send(attribute)
109
+ end
110
+
111
+ def attribute_hash
112
+ attrs = self.class.attributes.attributes
113
+ attr_keys = attrs.keys
114
+ hash = {}
115
+
116
+ attr_keys.each do |key|
117
+ hash[key] = attrs[key].type_cast(send(key))
118
+ end
119
+ hash
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,7 @@
1
+ require 'dynamo-record/record/version'
2
+
3
+ module DynamoRecord
4
+ module Record
5
+ require 'dynamo-record/record/railtie' if defined? Rails
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ require 'dynamo-record/record'
2
+ require 'rails'
3
+ module DynamoRecord
4
+ module Record
5
+ class Railtie < Rails::Railtie
6
+ railtie_name :dynamo_record
7
+
8
+ rake_tasks do
9
+ load "tasks/dynamo.rake"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ module DynamoRecord
2
+ module Record
3
+ VERSION = '0.1.0'.freeze
4
+ end
5
+ end
@@ -0,0 +1,36 @@
1
+ module DynamoRecord
2
+ class TableMigration
3
+ def self.migrate(model)
4
+ migration = Aws::Record::TableMigration.new(model)
5
+ begin
6
+ migration.client.describe_table(table_name: model.table_name)
7
+ return :exists
8
+ rescue Aws::DynamoDB::Errors::ResourceNotFoundException
9
+ yield migration
10
+ migration.wait_until_available
11
+ return :migrated
12
+ end
13
+ end
14
+
15
+ def self.migrate_updates(model)
16
+ migration = Aws::Record::TableMigration.new(model)
17
+ yield migration
18
+ migration.wait_until_available
19
+ :updated
20
+ end
21
+
22
+ def self.add_stream(model)
23
+ migrate_updates(model) do |migration|
24
+ migration.update!(
25
+ stream_specification: {
26
+ stream_enabled: true,
27
+ stream_view_type: 'NEW_IMAGE'
28
+ }
29
+ )
30
+ end
31
+ rescue Aws::DynamoDB::Errors::ValidationException => e
32
+ return e.message if e.message == 'Table already has an enabled stream'
33
+ raise e
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,16 @@
1
+ module DynamoRecord
2
+ module TaskHelpers
3
+ class Cleanup
4
+ def self.run
5
+ raise 'Task not available on production' if Rails.env.production?
6
+ Dir[Rails.root.join('app/models/*.rb').to_s].each do |filename|
7
+ klass = File.basename(filename, '.rb').camelize.constantize
8
+ if klass.included_modules.include? DynamoRecord::Model
9
+ puts "Deleting all items in table: #{klass}"
10
+ klass.scan.each(&:delete!)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,17 @@
1
+ module DynamoRecord
2
+ module TaskHelpers
3
+ class DropAllTables
4
+ def self.run(override = false)
5
+ raise 'Task not available on production' if Rails.env.production?
6
+ env = Rails.env
7
+ dynamodb = Aws::DynamoDB::Client.new
8
+ tables = dynamodb.list_tables
9
+ tables.table_names.map do |t|
10
+ next unless t.include?(env) || override
11
+ dt = dynamodb.delete_table(table_name: t)
12
+ dt ? "Deleted: #{t}" : "Delete failed: #{t}"
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ module DynamoRecord
2
+ module TaskHelpers
3
+ class DropTable
4
+ def self.run(table_name)
5
+ dynamodb = Aws::DynamoDB::Client.new
6
+ resp = dynamodb.delete_table(table_name: table_name)
7
+ "Deleted: #{resp.table_description.table_name}"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ module DynamoRecord
2
+ module TaskHelpers
3
+ class ListTables
4
+ def self.run
5
+ dynamodb = Aws::DynamoDB::Client.new
6
+ tables = dynamodb.list_tables
7
+ tables.table_names.select do |tn|
8
+ tn.starts_with?(Rails.configuration.dynamo['prefix'])
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,53 @@
1
+ module DynamoRecord
2
+ module TaskHelpers
3
+ class MigrationRunner
4
+ def self.run(path='db/dynamo_migrate')
5
+ constants = []
6
+ filename_regexp = /\A([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/
7
+
8
+ # Sorts the files located in `db/dynamo_migrate` to ensure order is preserved
9
+ Dir[Rails.root.join("#{path}/*.rb")].sort.each do |f|
10
+ raise "Non-numeric prefix: #{f}" if File.basename(f).scan(filename_regexp).first.nil?
11
+ require f
12
+
13
+ # finds the constant that was added on the require statement above
14
+ migration_sym = (DynamoMigrate.constants - constants).first
15
+ migration = DynamoMigrate.const_get(migration_sym)
16
+ constants.push migration_sym
17
+
18
+ # starts the migration
19
+ yield "Migrating: #{migration}"
20
+
21
+ begin
22
+ status = up(migration)
23
+ yield status if status
24
+
25
+ status = update(migration)
26
+ yield status if status
27
+ rescue => e
28
+ yield "Migration failed: #{e}"
29
+ end
30
+ end
31
+ end
32
+
33
+ def self.up(migration)
34
+ return unless migration.respond_to? :up
35
+ case migration.up
36
+ when :exists
37
+ 'Table already exists'
38
+ when :migrated
39
+ 'Migration successful'
40
+ else
41
+ raise 'Migration failed'
42
+ end
43
+ end
44
+
45
+ def self.update(migration)
46
+ return unless migration.respond_to? :update
47
+ status = migration.update
48
+ return 'Migration successful' if status == :updated
49
+ status
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,86 @@
1
+ module DynamoRecord
2
+ module TaskHelpers
3
+ class Scale
4
+ attr_reader :model, :attribute_selector, :new_throughput
5
+ attr_reader :migration, :existing_throughput, :model_name
6
+
7
+ def initialize(model_name, attribute_selector, new_throughput)
8
+ @model_name = model_name
9
+ @attribute_selector = attribute_selector
10
+ @new_throughput = new_throughput
11
+ end
12
+
13
+ def run
14
+ return description if [model_name, attribute_selector, new_throughput].any?(&:nil?)
15
+
16
+ @model = model_name.constantize
17
+ @migration = Aws::Record::TableMigration.new(model)
18
+ @existing_throughput = model.provisioned_throughput
19
+
20
+ update_throughput
21
+ success_message
22
+ end
23
+
24
+ private
25
+
26
+ def success_message
27
+ "Successfully updated #{model.table_name} throughput to #{update_instructions[:provisioned_throughput]}"
28
+ end
29
+
30
+ def update_throughput
31
+ raise_attribute_error if !read? && !write?
32
+
33
+ migration.update!(update_instructions)
34
+ end
35
+
36
+ def update_instructions
37
+ {
38
+ provisioned_throughput: {
39
+ write_capacity_units: (write? && new_throughput) || existing_write,
40
+ read_capacity_units: (read? && new_throughput) || existing_read
41
+ }
42
+ }
43
+ end
44
+
45
+ def existing_write
46
+ existing_throughput[:write_capacity_units]
47
+ end
48
+
49
+ def existing_read
50
+ existing_throughput[:read_capacity_units]
51
+ end
52
+
53
+ def both?
54
+ attribute_selector.to_sym == :both
55
+ end
56
+
57
+ def read?
58
+ both? || attribute_selector.to_sym == :read
59
+ end
60
+
61
+ def write?
62
+ both? || attribute_selector.to_sym == :write
63
+ end
64
+
65
+ def raise_attribute_error
66
+ raise ArgumentError, 'You didn\'t provide an appropriate attribute selection. We accept [:both, :read, :write]'
67
+ end
68
+
69
+ def description
70
+ <<~DESCRIPTION
71
+ --------------------------------------------------------------------------------
72
+ Here's some usage information:
73
+ Scale a dynamo table. Requires three inputs.
74
+ - ModelName
75
+ ruby class of the model
76
+ - attribute
77
+ valid values include "both", "read" and "write"
78
+ - new_throughput
79
+ numerical value for the new read/write capacity units
80
+ Example: `rake dynamo:scale[MySuperDynamoModel,both,50]`
81
+ --------------------------------------------------------------------------------
82
+ DESCRIPTION
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,7 @@
1
+ require 'active_model'
2
+ class ModelExistenceValidator < ActiveModel::EachValidator
3
+ def validate_each(record, attribute, value)
4
+ return if options[:model].exists? value
5
+ record.errors[attribute] << (options[:message] || "#{attribute}:#{value} is not a valid #{options[:model]}")
6
+ end
7
+ end
@@ -0,0 +1,38 @@
1
+ namespace :dynamo do
2
+ desc 'Run dynamo migrations'
3
+ task migrate: :environment do
4
+ DynamoRecord::TaskHelpers::MigrationRunner.run { |msg| puts msg }
5
+ end
6
+
7
+ desc 'Drops all dynamo tables and re-runs migrations'
8
+ task :reset do
9
+ Rake::Task['dynamo:drop_all'].invoke
10
+ Rake::Task['dynamo:migrate'].invoke
11
+ end
12
+
13
+ desc 'Drop all dynamo tables'
14
+ task :drop_all, [:override] => :environment do |_t, args|
15
+ puts DynamoRecord::TaskHelpers::DropAllTables.run args[:override]
16
+ end
17
+
18
+ desc 'Drop a specified dynamo table'
19
+ task :drop, [:table_name] => :environment do |_t, args|
20
+ puts DynamoRecord::TaskHelpers::DropTable.run args[:table_name]
21
+ end
22
+
23
+ desc 'List all tables with dynamo prefix'
24
+ task list_tables: :environment do
25
+ puts DynamoRecord::TaskHelpers::ListTables.run
26
+ end
27
+
28
+ desc 'Delete all records in all DyanmoDB tables.'
29
+ task cleanup: :environment do
30
+ DynamoRecord::TaskHelpers::Cleanup.run
31
+ puts 'Finished deleting all records in all DynamoDB tables.'
32
+ end
33
+
34
+ desc 'Scale a dynamo table read/write capacity to the new capacity value provided'
35
+ task :scale, [:model_name, :attribute, :new_throughput] => :environment do |_t, args|
36
+ puts DynamoRecord::TaskHelpers::Scale.new(args[:model_name], args[:attribute], args[:new_throughput].to_i).run
37
+ end
38
+ end
metadata ADDED
@@ -0,0 +1,207 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dynamo-record
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Davis McClellan
8
+ - Ryan Taylor
9
+ - Bryan Petty
10
+ - Michael Brewer-Davis
11
+ - Marc Phillips
12
+ - Augusto Callejas
13
+ autorequire:
14
+ bindir: exe
15
+ cert_chain: []
16
+ date: 2017-05-02 00:00:00.000000000 Z
17
+ dependencies:
18
+ - !ruby/object:Gem::Dependency
19
+ name: aws-record
20
+ requirement: !ruby/object:Gem::Requirement
21
+ requirements:
22
+ - - "~>"
23
+ - !ruby/object:Gem::Version
24
+ version: '1.1'
25
+ type: :runtime
26
+ prerelease: false
27
+ version_requirements: !ruby/object:Gem::Requirement
28
+ requirements:
29
+ - - "~>"
30
+ - !ruby/object:Gem::Version
31
+ version: '1.1'
32
+ - !ruby/object:Gem::Dependency
33
+ name: rails
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - "~>"
37
+ - !ruby/object:Gem::Version
38
+ version: '4.2'
39
+ type: :runtime
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - "~>"
44
+ - !ruby/object:Gem::Version
45
+ version: '4.2'
46
+ - !ruby/object:Gem::Dependency
47
+ name: bundler
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - "~>"
51
+ - !ruby/object:Gem::Version
52
+ version: '1.14'
53
+ type: :development
54
+ prerelease: false
55
+ version_requirements: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '1.14'
60
+ - !ruby/object:Gem::Dependency
61
+ name: byebug
62
+ requirement: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '9.0'
67
+ type: :development
68
+ prerelease: false
69
+ version_requirements: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '9.0'
74
+ - !ruby/object:Gem::Dependency
75
+ name: combustion
76
+ requirement: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: 0.6.0
81
+ type: :development
82
+ prerelease: false
83
+ version_requirements: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - "~>"
86
+ - !ruby/object:Gem::Version
87
+ version: 0.6.0
88
+ - !ruby/object:Gem::Dependency
89
+ name: rake
90
+ requirement: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - "~>"
93
+ - !ruby/object:Gem::Version
94
+ version: '10.0'
95
+ type: :development
96
+ prerelease: false
97
+ version_requirements: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - "~>"
100
+ - !ruby/object:Gem::Version
101
+ version: '10.0'
102
+ - !ruby/object:Gem::Dependency
103
+ name: rspec
104
+ requirement: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - "~>"
107
+ - !ruby/object:Gem::Version
108
+ version: '3.0'
109
+ type: :development
110
+ prerelease: false
111
+ version_requirements: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - "~>"
114
+ - !ruby/object:Gem::Version
115
+ version: '3.0'
116
+ - !ruby/object:Gem::Dependency
117
+ name: simplecov
118
+ requirement: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - "~>"
121
+ - !ruby/object:Gem::Version
122
+ version: '0.12'
123
+ type: :development
124
+ prerelease: false
125
+ version_requirements: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - "~>"
128
+ - !ruby/object:Gem::Version
129
+ version: '0.12'
130
+ - !ruby/object:Gem::Dependency
131
+ name: webmock
132
+ requirement: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - "~>"
135
+ - !ruby/object:Gem::Version
136
+ version: '2.1'
137
+ type: :development
138
+ prerelease: false
139
+ version_requirements: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - "~>"
142
+ - !ruby/object:Gem::Version
143
+ version: '2.1'
144
+ description: A set of extensions simplifying database operations in aws-record
145
+ email:
146
+ - dmcclellan@instructure.com
147
+ - rtaylor@instructure.com
148
+ - bpetty@instructure.com
149
+ - mbd@instructure.com
150
+ - mphillips@instructure.com
151
+ - acallejas@instructure.com
152
+ executables: []
153
+ extensions: []
154
+ extra_rdoc_files: []
155
+ files:
156
+ - ".gitignore"
157
+ - ".rspec"
158
+ - ".rubocop.yml"
159
+ - ".travis.yml"
160
+ - Dockerfile.test
161
+ - Gemfile
162
+ - Gemfile.lock
163
+ - README.md
164
+ - build.sh
165
+ - docker-compose.override.yml
166
+ - docker-compose.yml
167
+ - dynamo-record.gemspec
168
+ - lib/dynamo-record.rb
169
+ - lib/dynamo-record/marshalers.rb
170
+ - lib/dynamo-record/model.rb
171
+ - lib/dynamo-record/record.rb
172
+ - lib/dynamo-record/record/railtie.rb
173
+ - lib/dynamo-record/record/version.rb
174
+ - lib/dynamo-record/table_migration.rb
175
+ - lib/dynamo-record/task_helpers/cleanup.rb
176
+ - lib/dynamo-record/task_helpers/drop_all_tables.rb
177
+ - lib/dynamo-record/task_helpers/drop_table.rb
178
+ - lib/dynamo-record/task_helpers/list_tables.rb
179
+ - lib/dynamo-record/task_helpers/migration_runner.rb
180
+ - lib/dynamo-record/task_helpers/scale.rb
181
+ - lib/model_existence_validator.rb
182
+ - lib/tasks/dynamo.rake
183
+ homepage: https://instructure.com
184
+ licenses:
185
+ - MIT
186
+ metadata: {}
187
+ post_install_message:
188
+ rdoc_options: []
189
+ require_paths:
190
+ - lib
191
+ required_ruby_version: !ruby/object:Gem::Requirement
192
+ requirements:
193
+ - - ">="
194
+ - !ruby/object:Gem::Version
195
+ version: '0'
196
+ required_rubygems_version: !ruby/object:Gem::Requirement
197
+ requirements:
198
+ - - ">="
199
+ - !ruby/object:Gem::Version
200
+ version: '0'
201
+ requirements: []
202
+ rubyforge_project:
203
+ rubygems_version: 2.5.1
204
+ signing_key:
205
+ specification_version: 4
206
+ summary: Extensions for working with dynamo via aws-record
207
+ test_files: []