dynomite 1.0.5

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,15 @@
1
+ module Dynomite::Log
2
+ def self.included(base)
3
+ base.extend(ClassMethods)
4
+ end
5
+
6
+ def log(msg)
7
+ self.class.log(msg)
8
+ end
9
+
10
+ module ClassMethods
11
+ def log(msg)
12
+ Dynomite.logger.info(msg)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,27 @@
1
+ module Dynomite
2
+ class Migration
3
+ autoload :Dsl, "dynomite/migration/dsl"
4
+ autoload :Generator, "dynomite/migration/generator"
5
+ autoload :Executor, "dynomite/migration/executor"
6
+
7
+ def up
8
+ puts "Should defined an up method for your migration: #{self.class.name}"
9
+ end
10
+
11
+ def create_table(table_name, &block)
12
+ execute_with_dsl_params(table_name, :create_table, &block)
13
+ end
14
+
15
+ def update_table(table_name, &block)
16
+ execute_with_dsl_params(table_name, :update_table, &block)
17
+ end
18
+
19
+ private
20
+ def execute_with_dsl_params(table_name, method_name, &block)
21
+ dsl = Dsl.new(method_name, table_name, &block)
22
+ params = dsl.params
23
+ executor = Executor.new(table_name, method_name, params)
24
+ executor.run
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,86 @@
1
+ # Common methods to the *SecondaryIndex classes that handle gsi and lsi methods
2
+ # as well a the Dsl class that handles create_table and update_table methods.
3
+ class Dynomite::Migration::Dsl
4
+ module Common
5
+ # http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Types/KeySchemaElement.html
6
+ # partition_key is required
7
+ def partition_key(identifier)
8
+ @partition_key_identifier = identifier # for later use. useful for conventional_index_name
9
+ adjust_schema_and_attributes(identifier, "hash")
10
+ end
11
+
12
+ # sort_key is optional
13
+ def sort_key(identifier)
14
+ @sort_key_identifier = identifier # for later use. useful for conventional_index_name
15
+ adjust_schema_and_attributes(identifier, "range")
16
+ end
17
+
18
+ # Parameters:
19
+ # identifier: "id:string" or "id"
20
+ # key_type: "hash" or "range"
21
+ #
22
+ # Adjusts the parameters for create_table to add the
23
+ # partition_key and sort_key
24
+ def adjust_schema_and_attributes(identifier, key_type)
25
+ name, attribute_type = identifier.split(':')
26
+ attribute_type = "string" if attribute_type.nil?
27
+
28
+ partition_key = {
29
+ attribute_name: name,
30
+ key_type: key_type.upcase
31
+ }
32
+ @key_schema << partition_key
33
+
34
+ attribute_definition = {
35
+ attribute_name: name,
36
+ attribute_type: Dynomite::ATTRIBUTE_TYPES[attribute_type]
37
+ }
38
+ @attribute_definitions << attribute_definition
39
+ end
40
+
41
+ # t.provisioned_throughput(5) # both
42
+ # t.provisioned_throughput(:read, 5)
43
+ # t.provisioned_throughput(:write, 5)
44
+ # t.provisioned_throughput(:both, 5)
45
+ def provisioned_throughput(*params)
46
+ case params.size
47
+ when 0 # reader method
48
+ return @provisioned_throughput # early return
49
+ when 1
50
+ # @provisioned_throughput_set_called useful for update_table
51
+ # only provide a provisioned_throughput settings if explicitly called for update_table
52
+ @provisioned_throughput_set_called = true
53
+ arg = params[0]
54
+ if arg.is_a?(Hash)
55
+ # Case:
56
+ # provisioned_throughput(
57
+ # read_capacity_units: 10,
58
+ # write_capacity_units: 10
59
+ # )
60
+ @provisioned_throughput = arg # set directly
61
+ return # early return
62
+ else # assume parameter is an Integer
63
+ # Case: provisioned_throughput(10)
64
+ capacity_type = :both
65
+ capacity_units = arg
66
+ end
67
+ when 2
68
+ @provisioned_throughput_set_called = true
69
+ # Case: provisioned_throughput(:read, 5)
70
+ capacity_type, capacity_units = params
71
+ end
72
+
73
+ map = {
74
+ read: :read_capacity_units,
75
+ write: :write_capacity_units,
76
+ }
77
+
78
+ if capacity_type == :both
79
+ @provisioned_throughput[map[:read]] = capacity_units
80
+ @provisioned_throughput[map[:write]] = capacity_units
81
+ else
82
+ @provisioned_throughput[capacity_type] = capacity_units
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,172 @@
1
+ class Dynomite::Migration
2
+ class Dsl
3
+ autoload :Common, "dynomite/migration/common"
4
+ autoload :BaseSecondaryIndex, "dynomite/migration/dsl/base_secondary_index"
5
+ autoload :LocalSecondaryIndex, "dynomite/migration/dsl/local_secondary_index"
6
+ autoload :GlobalSecondaryIndex, "dynomite/migration/dsl/global_secondary_index"
7
+
8
+ include Dynomite::DbConfig
9
+ include Common
10
+
11
+ attr_accessor :key_schema, :attribute_definitions
12
+ attr_accessor :table_name
13
+ def initialize(method_name, table_name, &block)
14
+ @method_name = method_name
15
+ @table_name = table_name
16
+ @block = block
17
+
18
+ # Dsl fills in atttributes in as methods are called within the block.
19
+ # Attributes for both create_table and updated_table:
20
+ @attribute_definitions = []
21
+ @provisioned_throughput = {
22
+ read_capacity_units: 5,
23
+ write_capacity_units: 5
24
+ }
25
+
26
+ # Attributes for create_table only:
27
+ @key_schema = []
28
+
29
+ # Attributes for update_table only:
30
+ @gsi_indexes = []
31
+ @lsi_indexes = []
32
+ end
33
+
34
+ # t.gsi(:create) do |i|
35
+ # i.partition_key = "category:string"
36
+ # i.sort_key = "created_at:string" # optional
37
+ # end
38
+ def gsi(action, index_name=nil, &block)
39
+ gsi_index = GlobalSecondaryIndex.new(action, index_name, &block)
40
+ @gsi_indexes << gsi_index # store @gsi_index for the parent Dsl to use
41
+ end
42
+ alias_method :global_secondary_index, :gsi
43
+
44
+ # t.lsi(:create) do |i|
45
+ # i.partition_key = "category:string"
46
+ # i.sort_key = "created_at:string" # optional
47
+ # end
48
+ def lsi(action=:create, index_name=nil, &block)
49
+ # dont need action create but have it to keep the lsi and gsi method consistent
50
+ lsi_index = LocalSecondaryIndex.new(index_name, &block)
51
+ @lsi_indexes << lsi_index # store @lsi_index for the parent Dsl to use
52
+ end
53
+ alias_method :local_secondary_index, :gsi
54
+
55
+ def evaluate
56
+ return if @evaluated
57
+ @block.call(self) if @block
58
+ @evaluated = true
59
+ end
60
+
61
+ # http://docs.aws.amazon.com/sdk-for-ruby/v3/developer-guide/dynamo-example-create-table.html
62
+ # build the params up from dsl in memory and provides params to the
63
+ # executor
64
+ def params
65
+ evaluate # lazy evaluation: wait until as long as possible before evaluating code block
66
+
67
+ # Not using send because think its clearer in this case
68
+ case @method_name
69
+ when :create_table
70
+ params_create_table
71
+ when :update_table
72
+ params_update_table
73
+ end
74
+ end
75
+
76
+ def params_create_table
77
+ params = {
78
+ table_name: namespaced_table_name,
79
+ key_schema: @key_schema,
80
+ attribute_definitions: @attribute_definitions,
81
+ provisioned_throughput: @provisioned_throughput
82
+ }
83
+
84
+ params[:local_secondary_index_creates] = local_secondary_index_creates unless @lsi_indexes.empty?
85
+ params
86
+ end
87
+
88
+ # Goes thorugh all the lsi_indexes that have been built up in memory.
89
+ # Find the lsi object that creates an index and then grab the
90
+ # attribute_definitions from it.
91
+ def lsi_create_attribute_definitions
92
+ lsi = @lsi_indexes.first # DynamoDB only supports adding one index at a time anyway. The reason @lsi_indexes is an Array is because we're sharing the same class code for LSI and GSI
93
+ if lsi
94
+ lsi.evaluate # force early evaluate since we need the params to
95
+ # add: gsi_attribute_definitions + lsi_attrs
96
+ lsi_attrs = lsi.attribute_definitions
97
+ end
98
+ all_attrs = if lsi_attrs
99
+ @attribute_definitions + lsi_attrs
100
+ else
101
+ @attribute_definitions
102
+ end
103
+ all_attrs.uniq
104
+ end
105
+
106
+ # maps each lsi to the hash structure expected by dynamodb update_table
107
+ # under the global_secondary_index_updates key:
108
+ #
109
+ # { create: {...} }
110
+ # { update: {...} }
111
+ # { delete: {...} }
112
+ def lsi_secondary_index_creates
113
+ @lsi_indexes.map do |lsi|
114
+ { lsi.action => lsi.params }
115
+ end
116
+ end
117
+
118
+ def params_update_table
119
+ params = {
120
+ table_name: namespaced_table_name,
121
+ attribute_definitions: gsi_create_attribute_definitions,
122
+ # update table take values only some values for the "parent" table
123
+ # no key_schema, update_table does not handle key_schema for the "parent" table
124
+ }
125
+ # only set "parent" table provisioned_throughput if user actually invoked
126
+ # it in the dsl
127
+ params[:provisioned_throughput] = @provisioned_throughput if @provisioned_throughput_set_called
128
+ params[:global_secondary_index_updates] = global_secondary_index_updates
129
+ params
130
+ end
131
+
132
+ # Goes thorugh all the gsi_indexes that have been built up in memory.
133
+ # Find the gsi object that creates an index and then grab the
134
+ # attribute_definitions from it.
135
+ def gsi_create_attribute_definitions
136
+ gsi = @gsi_indexes.find { |gsi| gsi.action == :create }
137
+ if gsi
138
+ gsi.evaluate # force early evaluate since we need the params to
139
+ # add: gsi_attribute_definitions + gsi_attrs
140
+ gsi_attrs = gsi.attribute_definitions
141
+ end
142
+ all_attrs = if gsi_attrs
143
+ gsi_attribute_definitions + gsi_attrs
144
+ else
145
+ gsi_attribute_definitions
146
+ end
147
+ all_attrs.uniq
148
+ end
149
+
150
+ # maps each gsi to the hash structure expected by dynamodb update_table
151
+ # under the global_secondary_index_updates key:
152
+ #
153
+ # { create: {...} }
154
+ # { update: {...} }
155
+ # { delete: {...} }
156
+ def global_secondary_index_updates
157
+ @gsi_indexes.map do |gsi|
158
+ { gsi.action => gsi.params }
159
+ end
160
+ end
161
+
162
+ # >> resp = Post.db.describe_table(table_name: "demo-dev-posts")
163
+ # >> resp.table.attribute_definitions.map(&:to_h)
164
+ # => [{:attribute_name=>"id", :attribute_type=>"S"}]
165
+ def gsi_attribute_definitions
166
+ return @gsi_attribute_definitions if @gsi_attribute_definitions
167
+
168
+ resp = db.describe_table(table_name: namespaced_table_name)
169
+ @gsi_attribute_definitions = resp.table.attribute_definitions.map(&:to_h)
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,72 @@
1
+ # Base class for LocalSecondaryIndex and GlobalSecondaryIndex
2
+ class Dynomite::Migration::Dsl
3
+ class BaseSecondaryIndex
4
+ include Common
5
+
6
+ attr_accessor :action, :key_schema, :attribute_definitions
7
+ attr_accessor :index_name
8
+ def initialize(action, index_name=nil, &block)
9
+ @action = action.to_sym
10
+ # for gsi action can be: :create, :update, :delete
11
+ # for lsi action is always: :create
12
+ @index_name = index_name
13
+ @block = block
14
+
15
+ # Dsl fills these atttributes in as methods are called within
16
+ # the block
17
+ @key_schema = []
18
+ @attribute_definitions = []
19
+ # default provisioned_throughput
20
+ @provisioned_throughput = {
21
+ read_capacity_units: 5,
22
+ write_capacity_units: 5
23
+ }
24
+ end
25
+
26
+ def index_name
27
+ @index_name || conventional_index_name
28
+ end
29
+
30
+ def conventional_index_name
31
+ # @partition_key_identifier and @sort_key_identifier are set as immediately
32
+ # when the partition_key and sort_key methods are called in the dsl block.
33
+ # Usually look like this:
34
+ #
35
+ # @partition_key_identifier: post_id:string
36
+ # @sort_key_identifier: updated_at:string
37
+ #
38
+ # We strip the :string portion in case it is provided
39
+ #
40
+ partition_key = @partition_key_identifier.split(':').first
41
+ sort_key = @sort_key_identifier.split(':').first if @sort_key_identifier
42
+ [partition_key, sort_key, "index"].compact.join('-')
43
+ end
44
+
45
+ def evaluate
46
+ return if @evaluated
47
+ @block.call(self) if @block
48
+ @evaluated = true
49
+ end
50
+
51
+ def params
52
+ evaluate # lazy evaluation: wait until as long as possible before evaluating code block
53
+
54
+ params = { index_name: index_name } # required for all actions
55
+
56
+ if @action == :create
57
+ params[:key_schema] = @key_schema # required for create action
58
+ # hardcode to ALL for now
59
+ params[:projection] = { # required
60
+ projection_type: "ALL", # accepts ALL, KEYS_ONLY, INCLUDE
61
+ # non_key_attributes: ["NonKeyAttributeName"],
62
+ }
63
+ end
64
+
65
+ if [:create, :update].include?(@action)
66
+ params[:provisioned_throughput] = @provisioned_throughput
67
+ end
68
+
69
+ params
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,4 @@
1
+ class Dynomite::Migration::Dsl
2
+ class GlobalSecondaryIndex < BaseSecondaryIndex
3
+ end
4
+ end
@@ -0,0 +1,8 @@
1
+ class Dynomite::Migration::Dsl
2
+ class LocalSecondaryIndex < BaseSecondaryIndex
3
+ def initialize(index_name=nil, &block)
4
+ # Can only create local secondary index when creating a table
5
+ super(:create, index_name, &block)
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,30 @@
1
+ class Dynomite::Migration
2
+ class Executor
3
+ include Dynomite::DbConfig
4
+
5
+ # Examples:
6
+ # Executor.new(:create_table, params) or
7
+ # Executor.new(:update_table, params)
8
+ #
9
+ # The params are generated frmo the dsl.params
10
+ attr_accessor :table_name
11
+ def initialize(table_name, method_name, params)
12
+ @table_name = table_name
13
+ @method_name = method_name # create_table or update_table
14
+ @params = params
15
+ end
16
+
17
+ def run
18
+ begin
19
+ # Examples:
20
+ # result = db.create_table(@params)
21
+ # result = db.update_table(@params)
22
+ result = db.send(@method_name, @params)
23
+
24
+ puts "DynamoDB Table: #{@table_name} Status: #{result.table_description.table_status}"
25
+ rescue Aws::DynamoDB::Errors::ServiceError => error
26
+ puts "Unable to #{@method_name.to_s.gsub('_',' ')}: #{error.message}"
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,68 @@
1
+ require "active_support/core_ext/string"
2
+
3
+ class Dynomite::Migration
4
+ # jets dynamodb:generate posts --partition-key id:string
5
+ class Generator
6
+ include Dynomite::DbConfig
7
+
8
+ attr_reader :migration_name, :table_name
9
+ def initialize(migration_name, options)
10
+ @migration_name = migration_name
11
+ @options = options
12
+ end
13
+
14
+ def generate
15
+ puts "Generating migration for #{@table_name}" unless @options[:quiet]
16
+ return if @options[:noop]
17
+ create_migration
18
+ end
19
+
20
+ def create_migration
21
+ FileUtils.mkdir_p(File.dirname(migration_path))
22
+ IO.write(migration_path, migration_code)
23
+ puts "Migration file created: #{migration_path}. \nTo run:"
24
+ puts " jets dynamodb:migrate #{migration_path}"
25
+ end
26
+
27
+ def migration_code
28
+ path = File.expand_path("../templates/#{table_action}.rb", __FILE__)
29
+ result = Dynomite::Erb.result(path,
30
+ migration_class_name: migration_class_name,
31
+ table_name: table_name,
32
+ partition_key: @options[:partition_key],
33
+ sort_key: @options[:sort_key],
34
+ provisioned_throughput: @options[:provisioned_throughput] || 5,
35
+ )
36
+ end
37
+
38
+ def table_action
39
+ @options[:table_action] || conventional_table_action
40
+ end
41
+
42
+ def conventional_table_action
43
+ @migration_name.include?("update") ? "update_table" : "create_table"
44
+ end
45
+
46
+ def table_name
47
+ @options[:table_name] || conventional_table_name
48
+ end
49
+
50
+ # create_posts => posts
51
+ # update_posts => posts
52
+ def conventional_table_name
53
+ @migration_name.sub(/^(create|update)_/, '')
54
+ end
55
+
56
+ def migration_class_name
57
+ "#{@migration_name}_migration".classify # doesnt include timestamp
58
+ end
59
+
60
+ def migration_path
61
+ "#{Dynomite.app_root}dynamodb/migrate/#{timestamp}-#{@migration_name}_migration.rb"
62
+ end
63
+
64
+ def timestamp
65
+ @timestamp ||= Time.now.strftime("%Y%m%d%H%M%S")
66
+ end
67
+ end
68
+ end