dynamo-record 0.1.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.
@@ -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: []