dynamodb_model 0.2.0 → 0.3.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.
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