dynamodb_framework 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.
data/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # DynamodbFramework
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/dynamodb_framework`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'dynamodb_framework'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install dynamodb_framework
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/dynamodb_framework. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
36
+
37
+
38
+ ## License
39
+
40
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
41
+
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "dynamodb_framework"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'dynamodb_framework/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "dynamodb_framework"
8
+ spec.version = DynamodbFramework::VERSION
9
+ spec.authors = ["vaughanbrittonsage"]
10
+ spec.email = ["vaughanbritton@gmail.com"]
11
+
12
+ spec.summary = 'A lightweight framework to provide managers for working with aws dynamodb (incuding local version).'
13
+ spec.description = 'A lightweight framework to provide managers for working with aws dynamodb (incuding local version).'
14
+ spec.homepage = "https://github.com/vaughanbrittonsage/dynamodb_framework"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.11"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+
25
+ spec.add_dependency('json')
26
+ spec.add_dependency('aws-sdk-core')
27
+
28
+ end
@@ -0,0 +1,20 @@
1
+ class DynamoDbAttributesBuilder
2
+ attr_reader :attributes
3
+
4
+ def initialize
5
+ @attributes = []
6
+ end
7
+
8
+ def add(name, type)
9
+ type_symbol = :S
10
+ if type == 'number' || type == :N
11
+ type_symbol = :N
12
+ elsif type == 'binary' || type == :B
13
+ type_symbol = :B
14
+ elsif type == 'bool' || type == 'boolean' || type == :BOOL
15
+ type_symbol = :BOOL
16
+ end
17
+ @attributes.push({ :name => name, :type => type_symbol })
18
+ end
19
+
20
+ end
@@ -0,0 +1,75 @@
1
+ class DynamoDbMigrationManager
2
+
3
+ attr_reader :dynamodb_table_manager
4
+ attr_reader :dynamodb_repository
5
+
6
+ def initialize
7
+ @dynamodb_repository = DynamoDbRepository.new
8
+ @dynamodb_table_manager = DynamoDbTableManager.new
9
+ end
10
+
11
+ def connect
12
+
13
+ migration_table_name = 'dynamodb_migrations'
14
+
15
+ #check if migration table exists
16
+ if !@dynamodb_table_manager.exists?(migration_table_name)
17
+ #migration table not found so create it
18
+ builder = DynamoDbAttributesBuilder.new
19
+ builder.add(:timestamp, :S)
20
+ @dynamodb_table_manager.create(migration_table_name, builder.attributes, :timestamp)
21
+ end
22
+
23
+ #set the table name for the repository
24
+ @dynamodb_repository.table_name = migration_table_name
25
+
26
+ end
27
+
28
+ def get_last_executed_script
29
+ scripts = @dynamodb_repository.all()
30
+ if scripts.length > 0
31
+ return scripts.sort { |a,b| b['timestamp'] <=> a['timestamp'] }[0]['timestamp']
32
+ end
33
+
34
+ return nil
35
+ end
36
+
37
+ def apply
38
+
39
+ last_executed_script = get_last_executed_script()
40
+
41
+ scripts = []
42
+ MigrationScript.descendants.each do |ms|
43
+ script = ms.new
44
+ scripts.push(script)
45
+ end
46
+
47
+ scripts.sort { |a,b| a.timestamp <=> b.timestamp }.each do |script|
48
+ if last_executed_script == nil || script.timestamp > last_executed_script
49
+ script.apply
50
+ @dynamodb_repository.put({ :timestamp => script.timestamp })
51
+ end
52
+ end
53
+
54
+ end
55
+
56
+ def rollback
57
+
58
+ last_executed_script = get_last_executed_script()
59
+
60
+ scripts = []
61
+ MigrationScript.descendants.each do |ms|
62
+ script = ms.new
63
+ scripts.push(script)
64
+ end
65
+
66
+ scripts.sort { |a,b| a.timestamp <=> b.timestamp }.each do |script|
67
+ if script.timestamp == last_executed_script
68
+ script.undo
69
+ @dynamodb_repository.delete({ :timestamp => script.timestamp })
70
+ return
71
+ end
72
+ end
73
+ end
74
+
75
+ end
@@ -0,0 +1,16 @@
1
+ class MigrationScript
2
+ attr_accessor :timestamp
3
+
4
+ def apply
5
+ raise 'Not implemented.'
6
+ end
7
+
8
+ def undo
9
+ raise 'Not implemented.'
10
+ end
11
+
12
+ def self.descendants
13
+ ObjectSpace.each_object(Class).select { |klass| klass < self }
14
+ end
15
+
16
+ end
@@ -0,0 +1,185 @@
1
+ class DynamoDbRepository
2
+
3
+ attr_reader :dynamodb
4
+ attr_accessor :table_name
5
+
6
+ def initialize
7
+ @dynamodb = DynamoDbStore.new
8
+ end
9
+
10
+ def put(item)
11
+
12
+ json_string = item.to_json
13
+ json_obj = JSON.load(json_string)
14
+
15
+ params =
16
+ {
17
+ table_name: @table_name,
18
+ item: json_obj
19
+ }
20
+
21
+ dynamodb.client.put_item(params)
22
+
23
+ end
24
+
25
+ def delete(key)
26
+
27
+ params =
28
+ {
29
+ table_name: @table_name,
30
+ key: key
31
+ }
32
+
33
+ dynamodb.client.delete_item(params)
34
+
35
+ end
36
+
37
+ def get_by_key(hash_key, hash_value, range_key = nil, range_value = nil)
38
+
39
+ key = {}
40
+ key[hash_key] = hash_value
41
+ if(range_key != nil)
42
+ key[range_key] = range_value
43
+ end
44
+
45
+ params = {
46
+ table_name: table_name,
47
+ key: key
48
+ }
49
+
50
+ result = dynamodb.client.get_item(params)
51
+ return map(result.item)
52
+
53
+ end
54
+
55
+ def all
56
+
57
+ result = dynamodb.client.scan({
58
+ :table_name => table_name
59
+ })
60
+
61
+ output = []
62
+ result.items.each do |item|
63
+ output.push(map(item))
64
+ end
65
+
66
+ return output
67
+
68
+ end
69
+
70
+ def scan(expression, expression_params, limit = nil, count = false)
71
+
72
+ params = {
73
+ :table_name => table_name
74
+ }
75
+
76
+ if expression != nil
77
+ params[:filter_expression] = expression
78
+ end
79
+
80
+ if expression_params != nil
81
+
82
+ params[:expression_attribute_names] = {}
83
+ params[:expression_attribute_values] = {}
84
+
85
+ expression_params.each do |key, value|
86
+ if key.starts_with?('#')
87
+ params[:expression_attribute_names][key] = value
88
+ elsif key.starts_with?(':')
89
+ params[:expression_attribute_values][key] = value
90
+ end
91
+ end
92
+
93
+ end
94
+
95
+ if limit != nil
96
+ params[:limit] = limit
97
+ end
98
+
99
+ if count
100
+ params[:select] = 'COUNT'
101
+ else
102
+ params[:select] = 'ALL_ATTRIBUTES'
103
+ end
104
+
105
+ result = dynamodb.client.scan(params)
106
+
107
+ if count
108
+ return result.count
109
+ else
110
+ output = []
111
+ result.items.each do |item|
112
+ output.push(map(item))
113
+ end
114
+
115
+ return output
116
+ end
117
+
118
+ end
119
+
120
+ def query(hash_key_name, hash_key_value, range_key_name = nil, range_key_value = nil, expression = nil, expression_params = nil, index_name = nil, limit = nil, count = false)
121
+
122
+ params = {
123
+ table_name: table_name
124
+ }
125
+
126
+ if expression != nil
127
+ params[:filter_expression] = expression
128
+ end
129
+
130
+ if index_name != nil
131
+ params[:index_name] = index_name
132
+ end
133
+
134
+ if range_key_name != nil
135
+ params[:key_condition_expression] = '#hash_key = :hash_key and #range_key = :range_key'
136
+ params[:expression_attribute_names] = { '#hash_key' => hash_key_name, '#range_key' => range_key_name }
137
+ params[:expression_attribute_values] = { ':hash_key' => hash_key_value, ':range_key' => range_key_value }
138
+ else
139
+ params[:key_condition_expression] = '#hash_key = :hash_key'
140
+ params[:expression_attribute_names] = { '#hash_key' => hash_key_name }
141
+ params[:expression_attribute_values] = { ':hash_key' => hash_key_value }
142
+ end
143
+
144
+ if expression_params != nil
145
+
146
+ expression_params.each do |key, value|
147
+ if key.starts_with?('#')
148
+ params[:expression_attribute_names][key] = value
149
+ elsif key.starts_with?(':')
150
+ params[:expression_attribute_values][key] = value
151
+ end
152
+ end
153
+
154
+ end
155
+
156
+ if limit != nil
157
+ params[:limit] = limit
158
+ end
159
+
160
+ if count
161
+ params[:select] = 'COUNT'
162
+ else
163
+ params[:select] = 'ALL_ATTRIBUTES'
164
+ end
165
+
166
+ result = dynamodb.client.query(params)
167
+
168
+ if count
169
+ return result.count
170
+ else
171
+ output = []
172
+ result.items.each do |item|
173
+ output.push(map(item))
174
+ end
175
+
176
+ return output
177
+ end
178
+
179
+ end
180
+
181
+ def map(item)
182
+ return item
183
+ end
184
+
185
+ end
@@ -0,0 +1,16 @@
1
+ class DynamoDbStore
2
+ attr_reader :client
3
+
4
+ def initialize
5
+ region = ENV['DYNAMODB_REGION']
6
+ endpoint = ENV['DYNAMODB_ENDPOINT']
7
+ secret = ENV['DYNAMODB_SECRET']
8
+ key = ENV['DYNAMODB_ACCESS_KEY']
9
+ if endpoint != nil
10
+ @client = Aws::DynamoDB::Client.new(region: region, endpoint: endpoint, secret_access_key: secret, access_key_id: key)
11
+ else
12
+ @client = Aws::DynamoDB::Client.new(region: region, secret_access_key: secret, access_key_id: key)
13
+ end
14
+ end
15
+
16
+ end
@@ -0,0 +1,283 @@
1
+ class DynamoDbTableManager
2
+
3
+ attr_reader :dynamodb
4
+
5
+ def initialize
6
+ @dynamodb = DynamoDbStore.new
7
+ end
8
+
9
+ def exists?(table_name)
10
+
11
+ exists = true
12
+
13
+ begin
14
+ dynamodb.client.describe_table(:table_name => table_name)
15
+ rescue Aws::DynamoDB::Errors::ResourceNotFoundException
16
+ exists = false
17
+ end
18
+
19
+ return exists
20
+
21
+ end
22
+
23
+ def has_index?(table_name, index_name)
24
+ exists = true
25
+
26
+ begin
27
+ result = dynamodb.client.describe_table(:table_name => table_name)
28
+
29
+ if result.table[:global_secondary_indexes] == nil
30
+ return false
31
+ end
32
+
33
+ if result.table[:global_secondary_indexes].select { |i| i[:index_name] == index_name }.length > 0
34
+ exists = true
35
+ else
36
+ exists = false
37
+ end
38
+ rescue Aws::DynamoDB::Errors::ResourceNotFoundException
39
+ exists = false
40
+ end
41
+
42
+ return exists
43
+ end
44
+
45
+ def update_throughput(table_name, read_capacity, write_capacity)
46
+
47
+ if !exists?(table_name)
48
+ raise "table: #{table_name}, does not exist."
49
+ end
50
+
51
+ table = {
52
+ :table_name => table_name,
53
+ :provisioned_throughput => {
54
+ :read_capacity_units => read_capacity,
55
+ :write_capacity_units => write_capacity
56
+ }
57
+ }
58
+
59
+ dynamodb.client.update_table(table)
60
+
61
+ # wait for table to be updated
62
+ puts "waiting for table: [#{table_name}] to be updated..."
63
+ wait_until_active(table_name)
64
+ puts "table: [#{table_name}] updated!"
65
+
66
+ end
67
+
68
+ def add_index(table_name, attributes, global_index)
69
+
70
+ attribute_definitions = []
71
+
72
+ attributes.each do |a|
73
+ attribute_definitions.push({ :attribute_name => a[:name], :attribute_type => a[:type] })
74
+ end
75
+
76
+ table = {
77
+ :table_name => table_name,
78
+ :attribute_definitions => attribute_definitions,
79
+ :global_secondary_index_updates => [
80
+ :create => global_index
81
+ ]
82
+ }
83
+
84
+ dynamodb.client.update_table(table)
85
+
86
+ # wait for table to be updated
87
+ puts "Adding global index: #{global_index[:index_name]}..."
88
+ wait_until_index_active(table_name, global_index[:index_name])
89
+ puts "Index added!"
90
+ end
91
+
92
+ def update_index_throughput(table_name, index_name, read_capacity, write_capacity)
93
+ table = {
94
+ :table_name => table_name,
95
+ :global_secondary_index_updates => [
96
+ :update => {
97
+ :index_name => index_name,
98
+ :provisioned_throughput => {
99
+ :read_capacity_units => read_capacity,
100
+ :write_capacity_units => write_capacity
101
+ }
102
+ }
103
+ ]
104
+ }
105
+
106
+ dynamodb.client.update_table(table)
107
+
108
+ # wait for table to be updated
109
+ puts "Updating throughput for global index: #{index_name}..."
110
+ puts "waiting for table: [#{table_name}] to be updated..."
111
+ wait_until_active(table_name)
112
+ puts "table: [#{table_name}] updated!"
113
+ end
114
+
115
+ def drop_index(table_name, index_name)
116
+ table = {
117
+ :table_name => table_name,
118
+ :global_secondary_index_updates => [
119
+ :delete => {
120
+ :index_name => index_name
121
+ }
122
+ ]
123
+ }
124
+
125
+ dynamodb.client.update_table(table)
126
+
127
+ # wait for table to be updated
128
+ puts "Deleting global index: #{index_name}..."
129
+ wait_until_index_dropped(table_name, index_name)
130
+ puts "Index: [#{index_name}] dropped!"
131
+ end
132
+
133
+ def get_status(table_name)
134
+ result = dynamodb.client.describe_table(:table_name => table_name)
135
+ return result.table[:table_status]
136
+ end
137
+
138
+ def get_index_status(table_name, index_name)
139
+ result = dynamodb.client.describe_table(:table_name => table_name)
140
+
141
+ if result.table[:global_secondary_indexes] == nil
142
+ return nil
143
+ end
144
+
145
+ index = result.table[:global_secondary_indexes].select { |i| i[:index_name] == index_name }
146
+
147
+ if index.length > 0
148
+ return index[0][:index_status]
149
+ end
150
+
151
+ return nil
152
+ end
153
+
154
+ def wait_until_active(table_name)
155
+
156
+ end_time = Time.now + 5.minutes
157
+ while Time.now < end_time do
158
+
159
+ status = get_status(table_name)
160
+
161
+ if status == 'ACTIVE'
162
+ return
163
+ end
164
+
165
+ sleep(5.seconds)
166
+ end
167
+
168
+ raise "Timeout occured while waiting for table: #{table_name}, to become active."
169
+
170
+ end
171
+
172
+ def wait_until_index_active(table_name, index_name)
173
+
174
+ end_time = Time.now + 5.minutes
175
+ while Time.now < end_time do
176
+
177
+ status = get_index_status(table_name, index_name)
178
+
179
+ if status == 'ACTIVE'
180
+ return
181
+ end
182
+
183
+ sleep(5.seconds)
184
+ end
185
+
186
+ raise "Timeout occured while waiting for table: #{table_name}, index: #{index_name}, to become active."
187
+
188
+ end
189
+
190
+ def wait_until_index_dropped(table_name, index_name)
191
+
192
+ end_time = Time.now + 5.minutes
193
+ while Time.now < end_time do
194
+
195
+ status = get_index_status(table_name, index_name)
196
+
197
+ if status == nil
198
+ return
199
+ end
200
+
201
+ sleep(5.seconds)
202
+ end
203
+
204
+ raise "Timeout occured while waiting for table: #{table_name}, index: #{index_name}, to be dropped."
205
+
206
+ end
207
+
208
+ def create(table_name, attributes, partition_key, range_key = nil, read_capacity = 20, write_capacity = 10, global_indexes = nil)
209
+
210
+ if exists?(table_name)
211
+ return
212
+ end
213
+
214
+ attribute_definitions = []
215
+
216
+ attributes.each do |a|
217
+ attribute_definitions.push({ :attribute_name => a[:name], :attribute_type => a[:type] })
218
+ end
219
+
220
+ key_schema = []
221
+ key_schema.push({ :attribute_name => partition_key, :key_type => :HASH })
222
+ if range_key != nil
223
+ key_schema.push({ :attribute_name => range_key, :key_type => :RANGE })
224
+ end
225
+
226
+ table = {
227
+ :table_name => table_name,
228
+ :attribute_definitions => attribute_definitions,
229
+ :key_schema => key_schema,
230
+ :provisioned_throughput => {
231
+ :read_capacity_units => read_capacity,
232
+ :write_capacity_units => write_capacity
233
+ }
234
+ }
235
+
236
+ if global_indexes != nil
237
+ table[:global_secondary_indexes] = global_indexes
238
+ end
239
+
240
+ dynamodb.client.create_table(table)
241
+
242
+ # wait for table to be created
243
+ puts "waiting for table: [#{table_name}] to be created..."
244
+ dynamodb.client.wait_until(:table_exists, table_name: table_name)
245
+ puts "table: [#{table_name}] created!"
246
+ end
247
+
248
+ def create_global_index(name, partition_key, range_key = nil, read_capacity = 20, write_capacity = 10)
249
+
250
+ key_schema = []
251
+
252
+ key_schema.push({ :attribute_name => partition_key, :key_type => :HASH })
253
+ if range_key != nil
254
+ key_schema.push({ :attribute_name => range_key, :key_type => :RANGE })
255
+ end
256
+
257
+ index = {
258
+ :index_name => name,
259
+ :key_schema => key_schema,
260
+ :projection => {
261
+ :projection_type => :ALL
262
+ },
263
+ :provisioned_throughput => {
264
+ :read_capacity_units => read_capacity,
265
+ :write_capacity_units => write_capacity,
266
+ }
267
+ }
268
+
269
+ return index
270
+ end
271
+
272
+ def drop(table_name)
273
+
274
+ if !exists?(table_name)
275
+ return
276
+ end
277
+
278
+ puts "dropping table: [#{table_name}] ..."
279
+ dynamodb.client.delete_table({ table_name: table_name })
280
+ puts "table: [#{table_name}] dropped!"
281
+ end
282
+
283
+ end
@@ -0,0 +1,3 @@
1
+ module DynamodbFramework
2
+ VERSION = "0.1.0"
3
+ end