dynamodb_model 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: afe935d182837391daa3ca740b280d338f6bc458
4
- data.tar.gz: 536563d9d5e036a46dfdcc3a1a9f47f6df24eff9
3
+ metadata.gz: 4e369807797f7eeb8eef8476828b0760a85e8076
4
+ data.tar.gz: 23f014fb766cda2c085bbc932ab9961946155150
5
5
  SHA512:
6
- metadata.gz: 6a7281afdba6068ec75af043d66c3490be35ed8beb39af7c0d60e13196bed14003a9c9142db616ca575e848fe7391467dcb70ba68a656ab5f74b5c28c205a118
7
- data.tar.gz: c56348d9fda4a5ac732251e48f8924b50970488db1cfdb0a6090eb301a6084ec94bdce07f4f8e60168a4c538f7e335801cd05928f3fe693afe8afe5da0f78617
6
+ metadata.gz: aa48e2bf21ff32e98640a3264f35c2b9e77ae51d1e8638c912404041fc68b1da336b24c3005dc703457ff013eec49fea9234ea516ac1473d611b90dc5f59cd06
7
+ data.tar.gz: eaebb86f390ce9e2035af5e50f71f3235c5459284ee4cf540425272f0d511b826b568ad925da367fa9b47c2749e950bac56692901f805a3247cb31cc9e43818e
data/CHANGELOG.md ADDED
@@ -0,0 +1,8 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ This project *tries* to adhere to [Semantic Versioning](http://semver.org/), even before v1.0.
5
+
6
+ ## [0.3.0]
7
+ - DSL methods now available: create_table, update_table
8
+ - Also can add GSI indexes within update_table with: i.gsi
@@ -0,0 +1,72 @@
1
+ class CreateCommentsMigration < DynamodbModel::Migration
2
+ def up
3
+ create_table :comments do |t|
4
+ t.partition_key "post_id:string" # required
5
+ t.sort_key "created_at:string" # optional
6
+ t.provisioned_throughput(5) # sets both read and write, defaults to 5 when not set
7
+ end
8
+ end
9
+ end
10
+
11
+ class UpdateCommentsMigration < DynamodbModel::Migration
12
+ def up
13
+ update_table :comments do |t|
14
+
15
+ # t.global_secondary_index do
16
+ # t.gsi(METHOD, INDEX_NAME) do
17
+
18
+ # You normally create an index like so:
19
+ #
20
+ # t.gsi(:create) do |i|
21
+ # i.partition_key = "post_id:string" # partition_key is required
22
+ # i.sort_key = "updated_at:string" # sort_key is optional
23
+ # end
24
+ #
25
+ # The index name will be inferred from the partition_key and sort_key when
26
+ # not explicitly set. Examples:
27
+ #
28
+ # index_name = "#{partition_key}-#{sort_key}-index"
29
+ # index_name = "post_id-index" # no sort key
30
+ # index_name = "post_id-updated_at-index" # has sort key
31
+ #
32
+ # The inference allows you to not have to worry about the index
33
+ # naming scheme. You can still set the index_name explicitly like so:
34
+ #
35
+ # t.gsi(:create, "post_id-updated_at-index") do |i|
36
+ # i.partition_key = "post_id:string" # partition_key is required
37
+ # i.sort_key = "updated_at:string" # sort_key is optional
38
+ # end
39
+ #
40
+ t.gsi(:create) do |i|
41
+ i.partition_key "post_id:string"
42
+ i.sort_key "updated_at:string" # optional
43
+
44
+ # translates to
45
+ # i.key_schema({...})
46
+ # also makes sure that the schema_keys are added to the attributes_definitions
47
+
48
+ # t.projected_attributes(:all) # default if not called
49
+ # t.projected_attributes(:keys_only) # other ways to call
50
+ # t.projected_attributes([:id, :body, :tags, :updated_at])
51
+ # translates to:
52
+ # Valid Values: ALL | KEYS_ONLY | INCLUDE
53
+ # t.projection(
54
+ # projection_type: :all, # defaults to all
55
+ # )
56
+ # t.projection(
57
+ # projection_type: :include, # defaults to all
58
+ # non_key_attributes: [:id, :body, :tags, :updated_at], # defaults to all
59
+ # )
60
+
61
+ i.provisioned_throughput(10)
62
+ end
63
+
64
+ t.gsi(:update, "category-index") do |i|
65
+ i.provisioned_throughput(10)
66
+ end
67
+
68
+ t.gsi(:delete, "category-index")
69
+ end
70
+ end
71
+ end
72
+
@@ -0,0 +1,29 @@
1
+ class CreateCommentsMigration < DynamodbModel::Migration
2
+ def up
3
+ create_table :comments do |t|
4
+ t.partition_key "post_id:string" # required
5
+ t.sort_key "created_at:string" # optional
6
+ t.provisioned_throughput(5) # sets both read and write, defaults to 5 when not set
7
+ end
8
+ end
9
+ end
10
+
11
+ class UpdateCommentsMigration < DynamodbModel::Migration
12
+ def up
13
+ update_table :comments do |t|
14
+ t.gsi(:create) do |i|
15
+ i.partition_key "post_id:string"
16
+ i.sort_key "updated_at:string" # optional
17
+
18
+ i.provisioned_throughput(10)
19
+ end
20
+
21
+ t.gsi(:update, "update-me-index") do |i|
22
+ i.provisioned_throughput(10)
23
+ end
24
+
25
+ t.gsi(:delete, "delete-me-index")
26
+ end
27
+ end
28
+ end
29
+
@@ -12,6 +12,11 @@ module DynamodbModel::DbConfig
12
12
  self.class.db
13
13
  end
14
14
 
15
+ # NOTE: Class including DynamodbModel::DbConfig is required to have table_name method defined
16
+ def namespaced_table_name
17
+ [self.class.table_namespace, table_name].reject {|s| s.nil? || s.empty?}.join('-')
18
+ end
19
+
15
20
  module ClassMethods
16
21
  @@db = nil
17
22
  def db
@@ -30,21 +35,71 @@ module DynamodbModel::DbConfig
30
35
  end
31
36
 
32
37
  def db_config
38
+ return @db_config if @db_config
39
+
33
40
  if defined?(Jets)
34
- YAML.load_file("#{Jets.root}config/dynamodb.yml")[Jets.env] || {}
41
+ config_path = "#{Jets.root}config/dynamodb.yml"
42
+ env = Jets.env
35
43
  else
36
44
  config_path = ENV['DYNAMODB_MODEL_CONFIG'] || "./config/dynamodb.yml"
37
45
  env = ENV['DYNAMODB_MODEL_ENV'] || "development"
38
- YAML.load_file(config_path)[env] || {}
46
+ end
47
+
48
+ config = YAML.load(erb_result(config_path))
49
+ @db_config ||= config[env] || {}
50
+ end
51
+
52
+ def table_namespace(*args)
53
+ case args.size
54
+ when 0
55
+ get_table_namespace
56
+ when 1
57
+ set_table_namespace(args[0])
39
58
  end
40
59
  end
41
60
 
42
- @table_namespace = nil
43
- def table_namespace
44
- return @table_namespace if @table_namespace
61
+ def get_table_namespace
62
+ return @table_namespace if defined?(@table_namespace)
45
63
 
46
64
  config = db_config
47
65
  @table_namespace = config['table_namespace']
48
66
  end
67
+
68
+ def set_table_namespace(value)
69
+ @table_namespace = value
70
+ end
71
+
72
+ def erb_result(path)
73
+ template = IO.read(path)
74
+ begin
75
+ ERB.new(template, nil, "-").result(binding)
76
+ rescue Exception => e
77
+ puts e
78
+ puts e.backtrace if ENV['DEBUG']
79
+
80
+ # how to know where ERB stopped? - https://www.ruby-forum.com/topic/182051
81
+ # syntax errors have the (erb):xxx info in e.message
82
+ # undefined variables have (erb):xxx info in e.backtrac
83
+ error_info = e.message.split("\n").grep(/\(erb\)/)[0]
84
+ error_info ||= e.backtrace.grep(/\(erb\)/)[0]
85
+ raise unless error_info # unable to find the (erb):xxx: error line
86
+ line = error_info.split(':')[1].to_i
87
+ puts "Error evaluating ERB template on line #{line.to_s.colorize(:red)} of: #{path.sub(/^\.\//, '').colorize(:green)}"
88
+
89
+ template_lines = template.split("\n")
90
+ context = 5 # lines of context
91
+ top, bottom = [line-context-1, 0].max, line+context-1
92
+ spacing = template_lines.size.to_s.size
93
+ template_lines[top..bottom].each_with_index do |line_content, index|
94
+ line_number = top+index+1
95
+ if line_number == line
96
+ printf("%#{spacing}d %s\n".colorize(:red), line_number, line_content)
97
+ else
98
+ printf("%#{spacing}d %s\n", line_number, line_content)
99
+ end
100
+ end
101
+ exit 1 unless ENV['TEST']
102
+ end
103
+ end
49
104
  end
50
105
  end
@@ -8,25 +8,25 @@ require "yaml"
8
8
  #
9
9
  # Examples:
10
10
  #
11
- # post = Post.new
11
+ # post = MyModel.new
12
12
  # post = post.replace(title: "test title")
13
13
  #
14
14
  # post.attrs[:id] now contain a generaetd unique partition_key id.
15
15
  # Usually the partition_key is 'id'. You can set your own unique id also:
16
16
  #
17
- # post = Post.new(id: "myid", title: "my title")
17
+ # post = MyModel.new(id: "myid", title: "my title")
18
18
  # post.replace
19
19
  #
20
20
  # Note that the replace method replaces the entire item, so you
21
21
  # need to merge the attributes if you want to keep the other attributes.
22
22
  #
23
- # post = Post.find("myid")
23
+ # post = MyModel.find("myid")
24
24
  # post.attrs = post.attrs.deep_merge("desc": "my desc") # keeps title field
25
25
  # post.replace
26
26
  #
27
27
  # The convenience `attrs` method performs a deep_merge:
28
28
  #
29
- # post = Post.find("myid")
29
+ # post = MyModel.find("myid")
30
30
  # post.attrs("desc": "my desc") # <= does a deep_merge
31
31
  # post.replace
32
32
  #
@@ -100,10 +100,16 @@ module DynamodbModel
100
100
  @attributes
101
101
  end
102
102
 
103
- # AWS Docs examples: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GettingStarted.Ruby.04.html
104
- # Usage:
103
+ # Adds very little wrapper logic to scan.
104
+ #
105
+ # * Automatically add table_name to options for convenience.
106
+ # * Decorates return value. Returns Array of [MyModel.new] instead of the
107
+ # dynamodb client response.
108
+ #
109
+ # Other than that, usage is same was using the dynamodb client scan method
110
+ # directly. Example:
105
111
  #
106
- # Post.scan(
112
+ # MyModel.scan(
107
113
  # expression_attribute_names: {"#updated_at"=>"updated_at"},
108
114
  # filter_expression: "#updated_at between :start_time and :end_time",
109
115
  # expression_attribute_values: {
@@ -112,20 +118,78 @@ module DynamodbModel
112
118
  # }
113
119
  # )
114
120
  #
115
- # TODO: pretty lame interface, improve it somehow. Maybe:
116
- #
117
- # Post.scan(filter: "updated_at between :start_time and :end_time")
118
- #
119
- # which automatically maps the structure.
121
+ # AWS Docs examples: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GettingStarted.Ruby.04.html
120
122
  def self.scan(params={})
121
123
  puts("Should not use scan for production. It's slow and expensive. You should create either a LSI or GSI and use query the index instead.")
124
+ params = { table_name: table_name }.merge(params)
125
+ resp = db.scan(params)
126
+ resp.items.map {|i| self.new(i) }
127
+ end
122
128
 
123
- defaults = {
124
- table_name: table_name
129
+ # Adds very little wrapper logic to query.
130
+ #
131
+ # * Automatically add table_name to options for convenience.
132
+ # * Decorates return value. Returns Array of [MyModel.new] instead of the
133
+ # dynamodb client response.
134
+ #
135
+ # Other than that, usage is same was using the dynamodb client query method
136
+ # directly. Example:
137
+ #
138
+ # MyModel.query(
139
+ # index_name: 'category-index',
140
+ # expression_attribute_names: { "#category_name" => "category" },
141
+ # expression_attribute_values: { ":category_value" => "Entertainment" },
142
+ # key_condition_expression: "#category_name = :category_value",
143
+ # )
144
+ #
145
+ # AWS Docs examples: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GettingStarted.Ruby.04.html
146
+ def self.query(params={})
147
+ params = { table_name: table_name }.merge(params)
148
+ resp = db.query(params)
149
+ resp.items.map {|i| self.new(i) }
150
+ end
151
+
152
+ # Translates simple query searches:
153
+ #
154
+ # Post.where({category: "Drama"}, index_name: "category-index")
155
+ #
156
+ # translates to
157
+ #
158
+ # resp = db.query(
159
+ # table_name: "proj-dev-post",
160
+ # index_name: 'category-index',
161
+ # expression_attribute_names: { "#category_name" => "category" },
162
+ # expression_attribute_values: { ":category_value" => category },
163
+ # key_condition_expression: "#category_name = :category_value",
164
+ # )
165
+ #
166
+ # TODO: Implement nicer where syntax with index_name as a chained method.
167
+ #
168
+ # Post.where({category: "Drama"}, {index_name: "category-index"})
169
+ # VS
170
+ # Post.where(category: "Drama").index_name("category-index")
171
+ def self.where(attributes, options={})
172
+ raise "attributes.size == 1 only supported for now" if attributes.size != 1
173
+
174
+ attr_name = attributes.keys.first
175
+ attr_value = attributes[attr_name]
176
+
177
+ # params = {
178
+ # expression_attribute_names: { "#category_name" => "category" },
179
+ # expression_attribute_values: { ":category_value" => "Entertainment" },
180
+ # key_condition_expression: "#category_name = :category_value",
181
+ # }
182
+ name_key, value_key = "##{attr_name}_name", ":#{attr_name}_value"
183
+ params = {
184
+ expression_attribute_names: { name_key => attr_name },
185
+ expression_attribute_values: { value_key => attr_value },
186
+ key_condition_expression: "#{name_key} = #{value_key}",
125
187
  }
126
- params = defaults.merge(params)
127
- resp = db.scan(params)
128
- resp.items.map {|i| Post.new(i) }
188
+ # Allow direct access to override params passed to dynamodb query options.
189
+ # This is is how index_name is passed:
190
+ params = params.merge(options)
191
+
192
+ query(params)
129
193
  end
130
194
 
131
195
  def self.replace(attrs)
@@ -156,7 +220,7 @@ module DynamodbModel
156
220
  key: {partition_key => id}
157
221
  )
158
222
  attributes = resp.item # unwraps the item's attributes
159
- Post.new(attributes) if attributes
223
+ self.new(attributes) if attributes
160
224
  end
161
225
 
162
226
  # Two ways to use the delete method:
@@ -203,9 +267,23 @@ module DynamodbModel
203
267
  end
204
268
  end
205
269
 
206
- def self.table_name
207
- @table_name = self.name.pluralize.underscore
270
+ def self.table_name(*args)
271
+ case args.size
272
+ when 0
273
+ get_table_name
274
+ when 1
275
+ set_table_name(args[0])
276
+ end
277
+ end
278
+
279
+ def self.get_table_name
280
+ @table_name ||= self.name.pluralize.underscore
208
281
  [table_namespace, @table_name].reject {|s| s.nil? || s.empty?}.join('-')
209
282
  end
283
+
284
+ def self.set_table_name(value)
285
+ @table_name = value
286
+ end
287
+
210
288
  end
211
289
  end
@@ -2,15 +2,26 @@ module DynamodbModel
2
2
  class Migration
3
3
  autoload :Dsl, "dynamodb_model/migration/dsl"
4
4
  autoload :Generator, "dynamodb_model/migration/generator"
5
+ autoload :Executor, "dynamodb_model/migration/executor"
5
6
 
6
7
  def up
7
8
  puts "Should defined an up method for your migration: #{self.class.name}"
8
9
  end
9
10
 
10
- def create_table(table_name)
11
- dsl = Dsl.new(table_name)
12
- yield(dsl)
13
- dsl.execute
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
14
25
  end
15
26
  end
16
27
  end
@@ -0,0 +1,84 @@
1
+ class DynamodbModel::Migration::Dsl
2
+ module Common
3
+ # http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Types/KeySchemaElement.html
4
+ # partition_key is required
5
+ def partition_key(identifier)
6
+ @partition_key_identifier = identifier # for later use. useful for conventional_index_name
7
+ adjust_schema_and_attributes(identifier, "hash")
8
+ end
9
+
10
+ # sort_key is optional
11
+ def sort_key(identifier)
12
+ @sort_key_identifier = identifier # for later use. useful for conventional_index_name
13
+ adjust_schema_and_attributes(identifier, "range")
14
+ end
15
+
16
+ # Parameters:
17
+ # identifier: "id:string" or "id"
18
+ # key_type: "hash" or "range"
19
+ #
20
+ # Adjusts the parameters for create_table to add the
21
+ # partition_key and sort_key
22
+ def adjust_schema_and_attributes(identifier, key_type)
23
+ name, attribute_type = identifier.split(':')
24
+ attribute_type = "string" if attribute_type.nil?
25
+
26
+ partition_key = {
27
+ attribute_name: name,
28
+ key_type: key_type.upcase
29
+ }
30
+ @key_schema << partition_key
31
+
32
+ attribute_definition = {
33
+ attribute_name: name,
34
+ attribute_type: ATTRIBUTE_TYPE_MAP[attribute_type]
35
+ }
36
+ @attribute_definitions << attribute_definition
37
+ end
38
+
39
+ # t.provisioned_throughput(5) # both
40
+ # t.provisioned_throughput(:read, 5)
41
+ # t.provisioned_throughput(:write, 5)
42
+ # t.provisioned_throughput(:both, 5)
43
+ def provisioned_throughput(*params)
44
+ case params.size
45
+ when 0 # reader method
46
+ return @provisioned_throughput # early return
47
+ when 1
48
+ # @provisioned_throughput_set_called useful for update_table
49
+ # only provide a provisioned_throughput settings if explicitly called for update_table
50
+ @provisioned_throughput_set_called = true
51
+ arg = params[0]
52
+ if arg.is_a?(Hash)
53
+ # Case:
54
+ # provisioned_throughput(
55
+ # read_capacity_units: 10,
56
+ # write_capacity_units: 10
57
+ # )
58
+ @provisioned_throughput = arg # set directly
59
+ return # early return
60
+ else # assume parameter is an Integer
61
+ # Case: provisioned_throughput(10)
62
+ capacity_type = :both
63
+ capacity_units = arg
64
+ end
65
+ when 2
66
+ @provisioned_throughput_set_called = true
67
+ # Case: provisioned_throughput(:read, 5)
68
+ capacity_type, capacity_units = params
69
+ end
70
+
71
+ map = {
72
+ read: :read_capacity_units,
73
+ write: :write_capacity_units,
74
+ }
75
+
76
+ if capacity_type = :both
77
+ @provisioned_throughput[map[:read]] = capacity_units
78
+ @provisioned_throughput[map[:write]] = capacity_units
79
+ else
80
+ @provisioned_throughput[capacity_type] = capacity_units
81
+ end
82
+ end
83
+ end
84
+ end
@@ -1,6 +1,10 @@
1
1
  class DynamodbModel::Migration
2
2
  class Dsl
3
+ autoload :GlobalSecondaryIndex, "dynamodb_model/migration/dsl/global_secondary_index"
4
+ autoload :Common, "dynamodb_model/migration/common"
5
+
3
6
  include DynamodbModel::DbConfig
7
+ include Common
4
8
 
5
9
  ATTRIBUTE_TYPE_MAP = {
6
10
  'string' => 'S',
@@ -12,100 +16,119 @@ class DynamodbModel::Migration
12
16
  }
13
17
 
14
18
  attr_accessor :key_schema, :attribute_definitions
15
- # db is the dynamodb client
16
- def initialize(table_name)
19
+ attr_accessor :table_name
20
+ def initialize(method_name, table_name, &block)
21
+ @method_name = method_name
17
22
  @table_name = table_name
18
- @key_schema = []
23
+ @block = block
24
+
25
+ # Dsl fills in atttributes in as methods are called within the block.
26
+ # Attributes for both create_table and updated_table:
19
27
  @attribute_definitions = []
20
28
  @provisioned_throughput = {
21
- read_capacity_units: 10,
22
- write_capacity_units: 10
29
+ read_capacity_units: 5,
30
+ write_capacity_units: 5
23
31
  }
24
- end
25
32
 
26
- # http://docs.aws.amazon.com/sdkforruby/api/Aws/DynamoDB/Types/KeySchemaElement.html
27
- # partition_key is required
28
- def partition_key(identifier)
29
- adjust_schema_and_attributes(identifier, "hash")
30
- end
33
+ # Attributes for create_table only:
34
+ @key_schema = []
31
35
 
32
- # sort_key is optional
33
- def sort_key(identifier)
34
- adjust_schema_and_attributes(identifier, "range")
36
+ # Attributes for update_table only:
37
+ @gsi_indexes = []
35
38
  end
36
39
 
37
- # Parameters:
38
- # identifier: "id:string" or "id"
39
- # key_type: "hash" or "range"
40
- #
41
- # Adjusts the parameters for create_table to add the
42
- # partition_key and sort_key
43
- def adjust_schema_and_attributes(identifier, key_type)
44
- name, attribute_type = identifier.split(':')
45
- attribute_type = "string" if attribute_type.nil?
46
-
47
- partition_key = {
48
- attribute_name: name,
49
- key_type: key_type.upcase
50
- }
51
- @key_schema << partition_key
52
-
53
- attribute_definition = {
54
- attribute_name: name,
55
- attribute_type: ATTRIBUTE_TYPE_MAP[attribute_type]
56
- }
57
- @attribute_definitions << attribute_definition
40
+ # t.gsi(:create) do |i|
41
+ # i.partition_key = "category:string"
42
+ # i.sort_key = "created_at:string" # optional
43
+ # end
44
+ def gsi(action, index_name=nil, &block)
45
+ gsi_index = GlobalSecondaryIndex.new(action, index_name, &block)
46
+ @gsi_indexes << gsi_index # store @gsi_index for the parent Dsl to use
58
47
  end
48
+ alias_method :global_secondary_index, :gsi
59
49
 
60
- # t.provisioned_throughput(5) # both
61
- # t.provisioned_throughput(:read, 5)
62
- # t.provisioned_throughput(:write, 5)
63
- # t.provisioned_throughput(:both, 5)
64
- def provisioned_throughput(*params)
65
- case params.size
66
- when 2
67
- capacity_type, capacity_units = params
68
- when 1
69
- arg = params[0]
70
- if arg.is_a?(Hash)
71
- @provisioned_throughput = arg # set directly
72
- return
73
- else # assume parameter is an Integer
74
- capacity_type = :both
75
- capacity_units = arg
76
- end
77
- when 0 # reader method
78
- return @provisioned_throughput
79
- end
50
+ def evaluate
51
+ return if @evaluated
52
+ @block.call(self) if @block
53
+ @evaluated = true
54
+ end
80
55
 
81
- map = {
82
- read: :read_capacity_units,
83
- write: :write_capacity_units,
84
- }
56
+ # http://docs.aws.amazon.com/sdk-for-ruby/v3/developer-guide/dynamo-example-create-table.html
57
+ # build the params up from dsl in memory and provides params to the
58
+ # executor
59
+ def params
60
+ evaluate # lazy evaluation: wait until as long as possible before evaluating code block
85
61
 
86
- if capacity_type = :both
87
- @provisioned_throughput[map[:read]] = capacity_units
88
- @provisioned_throughput[map[:write]] = capacity_units
89
- else
90
- @provisioned_throughput[capacity_type] = capacity_units
62
+ # Not using send because think its clearer in this case
63
+ case @method_name
64
+ when :create_table
65
+ params_create_table
66
+ when :update_table
67
+ params_update_table
91
68
  end
92
69
  end
93
70
 
94
- # http://docs.aws.amazon.com/sdk-for-ruby/v3/developer-guide/dynamo-example-create-table.html
95
- def execute
96
- params = {
97
- table_name: @table_name,
71
+ def params_create_table
72
+ {
73
+ table_name: namespaced_table_name,
98
74
  key_schema: @key_schema,
99
75
  attribute_definitions: @attribute_definitions,
100
76
  provisioned_throughput: @provisioned_throughput
101
77
  }
102
- begin
103
- result = db.create_table(params)
78
+ end
79
+
80
+ def params_update_table
81
+ params = {
82
+ table_name: namespaced_table_name,
83
+ # update table take values only some values for the "parent" table
84
+ attribute_definitions: gsi_create_attribute_definitions, # This is only a partial
85
+ # key_schema: @key_schema, # update_table does not handle key_schema for the "parent" table,
86
+ }
87
+ # only set "parent" table provisioned_throughput if user actually invoked
88
+ # it in the dsl
89
+ params[:provisioned_throughput] = @provisioned_throughput if @provisioned_throughput_set_called
90
+ params[:global_secondary_index_updates] = global_secondary_index_updates
91
+ params
92
+ end
104
93
 
105
- puts "DynamoDB Table: #{@table_name} Status: #{result.table_description.table_status}"
106
- rescue Aws::DynamoDB::Errors::ServiceError => error
107
- puts "Unable to create table: #{error.message}"
94
+ # Goes thorugh all the gsi_indexes that have been built up in memory.
95
+ # Find the gsi object that creates an index and then grab the
96
+ # attribute_definitions from it.
97
+ def gsi_create_attribute_definitions
98
+ gsi = @gsi_indexes.find { |gsi| gsi.action == :create }
99
+ if gsi
100
+ gsi.evaluate # force early evaluate since we need the params to
101
+ # add: current_attribute_definitions + gsi_attrs
102
+ gsi_attrs = gsi.attribute_definitions
108
103
  end
104
+ all_attrs = if gsi_attrs
105
+ current_attribute_definitions + gsi_attrs
106
+ else
107
+ current_attribute_definitions
108
+ end
109
+ all_attrs.uniq
110
+ end
111
+
112
+ # maps each gsi to the hash structure expected by dynamodb update_table
113
+ # under the global_secondary_index_updates key:
114
+ #
115
+ # { create: {...} }
116
+ # { update: {...} }
117
+ # { delete: {...} }
118
+ def global_secondary_index_updates
119
+ @gsi_indexes.map do |gsi|
120
+ { gsi.action => gsi.params }
121
+ end
122
+ end
123
+
124
+ # >> resp = Post.db.describe_table(table_name: "proj-dev-posts")
125
+ # >> resp.table.attribute_definitions.map(&:to_h)
126
+ # => [{:attribute_name=>"id", :attribute_type=>"S"}]
127
+ def current_attribute_definitions
128
+ return @current_attribute_definitions if @current_attribute_definitions
129
+
130
+ resp = db.describe_table(table_name: namespaced_table_name)
131
+ @current_attribute_definitions = resp.table.attribute_definitions.map(&:to_h)
109
132
  end
110
133
  end
111
134
  end
@@ -0,0 +1,71 @@
1
+ class DynamodbModel::Migration::Dsl
2
+ class GlobalSecondaryIndex
3
+ include Common
4
+
5
+ ATTRIBUTE_TYPE_MAP = DynamodbModel::Migration::Dsl::ATTRIBUTE_TYPE_MAP
6
+
7
+ attr_accessor :action, :key_schema, :attribute_definitions
8
+ attr_accessor :index_name
9
+ def initialize(action, index_name=nil, &block)
10
+ @action = action.to_sym # :create, :update, :index
11
+ @index_name = index_name
12
+ @block = block
13
+
14
+ # Dsl fills these atttributes in as methods are called within
15
+ # the block
16
+ @key_schema = []
17
+ @attribute_definitions = []
18
+ # default provisioned_throughput
19
+ @provisioned_throughput = {
20
+ read_capacity_units: 5,
21
+ write_capacity_units: 5
22
+ }
23
+ end
24
+
25
+ def index_name
26
+ @index_name || conventional_index_name
27
+ end
28
+
29
+ def conventional_index_name
30
+ # @partition_key_identifier and @sort_key_identifier are set as immediately
31
+ # when the partition_key and sort_key methods are called in the dsl block.
32
+ # Usually look like this:
33
+ #
34
+ # @partition_key_identifier: post_id:string
35
+ # @sort_key_identifier: updated_at:string
36
+ #
37
+ # We strip the :string portion in case it is provided
38
+ #
39
+ partition_key = @partition_key_identifier.split(':').first
40
+ sort_key = @sort_key_identifier.split(':').first if @sort_key_identifier
41
+ [partition_key, sort_key, "index"].compact.join('-')
42
+ end
43
+
44
+ def evaluate
45
+ return if @evaluated
46
+ @block.call(self) if @block
47
+ @evaluated = true
48
+ end
49
+
50
+ def params
51
+ evaluate # lazy evaluation: wait until as long as possible before evaluating code block
52
+
53
+ params = { index_name: index_name } # required for all actions
54
+
55
+ if @action == :create
56
+ params[:key_schema] = @key_schema # required for create action
57
+ # hardcode to ALL for now
58
+ params[:projection] = { # required
59
+ projection_type: "ALL", # accepts ALL, KEYS_ONLY, INCLUDE
60
+ # non_key_attributes: ["NonKeyAttributeName"],
61
+ }
62
+ end
63
+
64
+ if [:create, :update].include?(@action)
65
+ params[:provisioned_throughput] = @provisioned_throughput
66
+ end
67
+
68
+ params
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,30 @@
1
+ class DynamodbModel::Migration
2
+ class Executor
3
+ include DynamodbModel::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
@@ -12,21 +12,24 @@ class DynamodbModel::Migration
12
12
  end
13
13
 
14
14
  def generate
15
- puts "Generating migration" unless @options[:quiet]
15
+ puts "Generating migration for #{@table_name}" unless @options[:quiet]
16
16
  return if @options[:noop]
17
17
  create_migration
18
18
  end
19
19
 
20
20
  def create_migration
21
- migration_path = "#{DynamodbModel.root}db/migrate/#{migration_file_name}.rb"
21
+ migration_relative_path = "db/migrate/#{migration_file_name}.rb"
22
+ migration_path = "#{DynamodbModel.root}#{migration_relative_path}"
22
23
  dir = File.dirname(migration_path)
23
24
  FileUtils.mkdir_p(dir) unless File.exist?(dir)
24
25
  IO.write(migration_path, migration_code)
26
+ puts "Migration file created: #{migration_relative_path}. To run:"
27
+ puts " jets db migrate #{migration_relative_path}"
25
28
  end
26
29
 
27
30
  def migration_code
28
31
  # @table_name already set
29
- @migration_class_name = migration_file_name.classify
32
+ @migration_class_name = "#{@table_name}_migration".classify
30
33
  @partition_key = @options[:partition_key]
31
34
  @sort_key = @options[:sort_key]
32
35
  @provisioned_throughput = @options[:provisioned_throughput] || 5
@@ -39,7 +42,7 @@ class DynamodbModel::Migration
39
42
  end
40
43
 
41
44
  def timestamp
42
- "timestamp"
45
+ @timestamp ||= Time.now.strftime("%Y%m%d%H%M%S")
43
46
  end
44
47
  end
45
48
  end
@@ -1,3 +1,3 @@
1
1
  module DynamodbModel
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dynamodb_model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tung Nguyen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-11-04 00:00:00.000000000 Z
11
+ date: 2017-11-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -89,17 +89,23 @@ extra_rdoc_files: []
89
89
  files:
90
90
  - ".gitignore"
91
91
  - ".rspec"
92
+ - CHANGELOG.md
92
93
  - Gemfile
93
94
  - README.md
94
95
  - Rakefile
95
96
  - bin/console
96
97
  - bin/setup
98
+ - docs/migrations-long.rb
99
+ - docs/migrations-short.rb
97
100
  - dynamodb_model.gemspec
98
101
  - lib/dynamodb_model.rb
99
102
  - lib/dynamodb_model/db_config.rb
100
103
  - lib/dynamodb_model/item.rb
101
104
  - lib/dynamodb_model/migration.rb
105
+ - lib/dynamodb_model/migration/common.rb
102
106
  - lib/dynamodb_model/migration/dsl.rb
107
+ - lib/dynamodb_model/migration/dsl/global_secondary_index.rb
108
+ - lib/dynamodb_model/migration/executor.rb
103
109
  - lib/dynamodb_model/migration/generator.rb
104
110
  - lib/dynamodb_model/migration/template.rb
105
111
  - lib/dynamodb_model/util.rb
@@ -123,7 +129,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
123
129
  version: '0'
124
130
  requirements: []
125
131
  rubyforge_project:
126
- rubygems_version: 2.6.14
132
+ rubygems_version: 2.4.5
127
133
  signing_key:
128
134
  specification_version: 4
129
135
  summary: ActiveRecord-ish Dynamodb Model