dynamodb_framework 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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