dynomite 1.2.7 → 2.0.1

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.
Files changed (123) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +17 -2
  3. data/CHANGELOG.md +21 -0
  4. data/Gemfile +1 -5
  5. data/LICENSE.txt +22 -0
  6. data/README.md +6 -190
  7. data/Rakefile +13 -1
  8. data/dynomite.gemspec +9 -2
  9. data/exe/dynomite +14 -0
  10. data/lib/dynomite/associations/association.rb +126 -0
  11. data/lib/dynomite/associations/belongs_to.rb +35 -0
  12. data/lib/dynomite/associations/has_and_belongs_to_many.rb +19 -0
  13. data/lib/dynomite/associations/has_many.rb +19 -0
  14. data/lib/dynomite/associations/has_one.rb +19 -0
  15. data/lib/dynomite/associations/many_association.rb +257 -0
  16. data/lib/dynomite/associations/single_association.rb +157 -0
  17. data/lib/dynomite/associations.rb +248 -0
  18. data/lib/dynomite/autoloader.rb +25 -0
  19. data/lib/dynomite/cli.rb +48 -0
  20. data/lib/dynomite/client.rb +118 -0
  21. data/lib/dynomite/command.rb +89 -0
  22. data/lib/dynomite/completer/script.rb +6 -0
  23. data/lib/dynomite/completer/script.sh +10 -0
  24. data/lib/dynomite/completer.rb +159 -0
  25. data/lib/dynomite/config.rb +39 -0
  26. data/lib/dynomite/core.rb +18 -19
  27. data/lib/dynomite/engine.rb +15 -0
  28. data/lib/dynomite/erb.rb +5 -3
  29. data/lib/dynomite/error.rb +12 -0
  30. data/lib/dynomite/help/completion.md +20 -0
  31. data/lib/dynomite/help/completion_script.md +3 -0
  32. data/lib/dynomite/help/migrate.md +3 -0
  33. data/lib/dynomite/help.rb +9 -0
  34. data/lib/dynomite/install.rb +4 -0
  35. data/lib/dynomite/item/abstract.rb +15 -0
  36. data/lib/dynomite/item/components.rb +33 -0
  37. data/lib/dynomite/item/dsl.rb +101 -0
  38. data/lib/dynomite/item/id.rb +41 -0
  39. data/lib/dynomite/item/indexes/finder.rb +58 -0
  40. data/lib/dynomite/item/indexes/index.rb +21 -0
  41. data/lib/dynomite/item/indexes/primary_index.rb +18 -0
  42. data/lib/dynomite/item/indexes.rb +25 -0
  43. data/lib/dynomite/item/locking.rb +53 -0
  44. data/lib/dynomite/item/magic_fields.rb +66 -0
  45. data/lib/dynomite/item/primary_key.rb +85 -0
  46. data/lib/dynomite/item/query/delegates.rb +28 -0
  47. data/lib/dynomite/item/query/params/base.rb +42 -0
  48. data/lib/dynomite/item/query/params/expression_attribute.rb +79 -0
  49. data/lib/dynomite/item/query/params/filter.rb +41 -0
  50. data/lib/dynomite/item/query/params/function/attribute_exists.rb +21 -0
  51. data/lib/dynomite/item/query/params/function/attribute_type.rb +30 -0
  52. data/lib/dynomite/item/query/params/function/base.rb +33 -0
  53. data/lib/dynomite/item/query/params/function/begins_with.rb +32 -0
  54. data/lib/dynomite/item/query/params/function/contains.rb +7 -0
  55. data/lib/dynomite/item/query/params/function/size_fn.rb +37 -0
  56. data/lib/dynomite/item/query/params/helpers.rb +94 -0
  57. data/lib/dynomite/item/query/params/key_condition.rb +34 -0
  58. data/lib/dynomite/item/query/params.rb +115 -0
  59. data/lib/dynomite/item/query/partiql/executer.rb +72 -0
  60. data/lib/dynomite/item/query/partiql.rb +67 -0
  61. data/lib/dynomite/item/query/relation/chain.rb +125 -0
  62. data/lib/dynomite/item/query/relation/comparision_expression.rb +21 -0
  63. data/lib/dynomite/item/query/relation/comparision_map.rb +19 -0
  64. data/lib/dynomite/item/query/relation/delete.rb +38 -0
  65. data/lib/dynomite/item/query/relation/ids.rb +21 -0
  66. data/lib/dynomite/item/query/relation/math.rb +19 -0
  67. data/lib/dynomite/item/query/relation/where_field.rb +32 -0
  68. data/lib/dynomite/item/query/relation/where_group.rb +78 -0
  69. data/lib/dynomite/item/query/relation.rb +127 -0
  70. data/lib/dynomite/item/query.rb +7 -0
  71. data/lib/dynomite/item/read/find.rb +196 -0
  72. data/lib/dynomite/item/read/find_with_event.rb +42 -0
  73. data/lib/dynomite/item/read.rb +90 -0
  74. data/lib/dynomite/item/sti.rb +43 -0
  75. data/lib/dynomite/item/table_namespace.rb +43 -0
  76. data/lib/dynomite/item/typecaster.rb +106 -0
  77. data/lib/dynomite/item/waiter_methods.rb +18 -0
  78. data/lib/dynomite/item/write/base.rb +15 -0
  79. data/lib/dynomite/item/write/delete_item.rb +14 -0
  80. data/lib/dynomite/item/write/put_item.rb +99 -0
  81. data/lib/dynomite/item/write/update_item.rb +73 -0
  82. data/lib/dynomite/item/write.rb +204 -0
  83. data/lib/dynomite/item.rb +113 -286
  84. data/lib/dynomite/migration/dsl/accessor.rb +19 -0
  85. data/lib/dynomite/migration/dsl/index/base.rb +42 -0
  86. data/lib/dynomite/migration/dsl/index/gsi.rb +59 -0
  87. data/lib/dynomite/migration/dsl/index/lsi.rb +27 -0
  88. data/lib/dynomite/migration/dsl/index.rb +72 -0
  89. data/lib/dynomite/migration/dsl/primary_key.rb +62 -0
  90. data/lib/dynomite/migration/dsl/provisioned_throughput.rb +38 -0
  91. data/lib/dynomite/migration/dsl.rb +89 -142
  92. data/lib/dynomite/migration/file_info.rb +28 -0
  93. data/lib/dynomite/migration/generator.rb +30 -16
  94. data/lib/dynomite/migration/helpers.rb +7 -0
  95. data/lib/dynomite/migration/internal/migrate/create_schema_migrations.rb +17 -0
  96. data/lib/dynomite/migration/internal/models/schema_migration.rb +6 -0
  97. data/lib/dynomite/migration/runner.rb +179 -0
  98. data/lib/dynomite/migration/templates/create_table.rb +7 -23
  99. data/lib/dynomite/migration/templates/delete_table.rb +7 -0
  100. data/lib/dynomite/migration/templates/update_table.rb +3 -18
  101. data/lib/dynomite/migration.rb +53 -10
  102. data/lib/dynomite/reserved_words.rb +13 -3
  103. data/lib/dynomite/seed.rb +13 -0
  104. data/lib/dynomite/types.rb +22 -0
  105. data/lib/dynomite/version.rb +1 -1
  106. data/lib/dynomite/waiter.rb +40 -0
  107. data/lib/dynomite.rb +11 -17
  108. data/lib/generators/application_item/application_item_generator.rb +30 -0
  109. data/lib/generators/application_item/templates/application_item.rb.tt +4 -0
  110. data/lib/jets/commands/dynamodb_command.rb +29 -0
  111. data/lib/jets/commands/help/generate.md +33 -0
  112. data/lib/jets/commands/help/migrate.md +3 -0
  113. metadata +201 -17
  114. data/docs/migrations/long-example.rb +0 -127
  115. data/docs/migrations/short-example.rb +0 -40
  116. data/lib/dynomite/db_config.rb +0 -121
  117. data/lib/dynomite/errors.rb +0 -15
  118. data/lib/dynomite/log.rb +0 -15
  119. data/lib/dynomite/migration/common.rb +0 -86
  120. data/lib/dynomite/migration/dsl/base_secondary_index.rb +0 -73
  121. data/lib/dynomite/migration/dsl/global_secondary_index.rb +0 -4
  122. data/lib/dynomite/migration/dsl/local_secondary_index.rb +0 -8
  123. data/lib/dynomite/migration/executor.rb +0 -38
data/lib/dynomite/item.rb CHANGED
@@ -1,334 +1,161 @@
1
- require "active_support/core_ext/hash"
2
- require "aws-sdk-dynamodb"
1
+ require "active_model"
3
2
  require "digest"
4
3
  require "yaml"
5
4
 
6
- require "dynomite/reserved_words"
7
- require "dynomite/query"
8
-
9
- # The modeling is ActiveRecord-ish but not exactly because DynamoDB is a
10
- # different type of database.
5
+ # The model is ActiveModel compatiable even though DynamoDB is a different type of database.
11
6
  #
12
7
  # Examples:
13
8
  #
14
- # post = MyModel.new
15
- # post = post.replace(title: "test title")
16
- #
17
- # post.attrs[:id] now contain a generaetd unique partition_key id.
18
- # Usually the partition_key is 'id'. You can set your own unique id also:
19
- #
20
- # post = MyModel.new(id: "myid", title: "my title")
21
- # post.replace
22
- #
23
- # Note that the replace method replaces the entire item, so you
24
- # need to merge the attributes if you want to keep the other attributes.
25
- #
26
- # post = MyModel.find("myid")
27
- # post.attrs = post.attrs.deep_merge("desc": "my desc") # keeps title field
28
- # post.replace
9
+ # post = Post.new(id: "myid", title: "my title")
10
+ # post.save
29
11
  #
30
- # The convenience `attrs` method performs a deep_merge:
31
- #
32
- # post = MyModel.find("myid")
33
- # post.attrs("desc": "my desc") # <= does a deep_merge
34
- # post.replace
35
- #
36
- # Note, a race condition edge case can exist when several concurrent replace
37
- # calls are happening. This is why the interface is called replace to
38
- # emphasis that possibility.
39
- # TODO: implement post.update with db.update_item in a Ruby-ish way.
12
+ # post.id now contain a generated unique partition_key id.
40
13
  #
41
14
  module Dynomite
42
15
  class Item
43
- include Log
44
- include DbConfig
45
- include Errors
46
-
47
- def initialize(attrs={})
48
- @attrs = attrs
49
- end
50
-
51
- # Defining our own reader so we can do a deep merge if user passes in attrs
52
- def attrs(*args)
53
- case args.size
54
- when 0
55
- ActiveSupport::HashWithIndifferentAccess.new(@attrs)
56
- when 1
57
- attributes = args[0] # Hash
58
- if attributes.empty?
59
- ActiveSupport::HashWithIndifferentAccess.new
60
- else
61
- @attrs = attrs.deep_merge!(attributes)
16
+ class_attribute :fields_map
17
+ self.fields_map = {}
18
+ class_attribute :id_prefix_value
19
+
20
+ include Components
21
+ include Abstract
22
+ abstract!
23
+
24
+ # Must come after include Dynomite::Associations
25
+ def self.inherited(subclass)
26
+ subclass.id_prefix_value = subclass.name.underscore
27
+ # Not direct descendants of Dynomite::Item are abstract
28
+ # IE: SchemaMigration < Dynomite::Item
29
+ subclass.abstract! if subclass.name == "ApplicationItem"
30
+ subclass.class_attribute :fields_map
31
+ subclass.fields_map = {}
32
+ super # Dynomite::Associations.inherited
33
+ end
34
+
35
+ delegate :partition_key_field, :sort_key_field, to: :class
36
+ attr_reader :attrs
37
+ attr_accessor :new_record
38
+ alias_method :new_record?, :new_record
39
+ def initialize(attrs={}, &block)
40
+ run_callbacks(:initialize) do
41
+ @new_record = true
42
+ attrs = attrs.to_hash if attrs.respond_to?(:to_hash) # IE: ActionController::Parameters
43
+ raise ArgumentError, "attrs must be a Hash. attrs is a #{attrs.class}" unless attrs.is_a?(Hash)
44
+ @attrs = ActiveSupport::HashWithIndifferentAccess.new(attrs)
45
+ attrs.each do |k,v|
46
+ send("#{k}=", v) if respond_to?("#{k}=") # so typecasting happens
62
47
  end
63
- end
64
- end
65
-
66
- # Not using method_missing to allow usage of dot notation and assign
67
- # @attrs because it might hide actual missing methods errors.
68
- # DynamoDB attrs can go many levels deep so it makes less make sense to
69
- # use to dot notation.
48
+ @associations = {}
70
49
 
71
- # The method is named replace to clearly indicate that the item is
72
- # fully replaced.
73
- def replace(hash={})
74
- @attrs = @attrs.deep_merge(hash)
75
-
76
- # valid? method comes from ActiveModel::Validations
77
- if respond_to? :valid?
78
- return false unless valid?
50
+ if block
51
+ yield(self)
52
+ end
79
53
  end
80
-
81
- attrs = self.class.replace(@attrs)
82
-
83
- @attrs = attrs # refresh attrs because it now has the id
84
- self
85
54
  end
86
55
 
87
- # Similar to replace, but raises an error on failed validation.
88
- # Works that way only if ActiveModel::Validations are included
89
- def replace!(hash={})
90
- raise ValidationError, "Validation failed: #{errors.full_messages.join(', ')}" unless replace(hash)
56
+ # Keeps the current attrs
57
+ def attrs=(attrs)
58
+ @attrs.deep_merge!(attrs)
91
59
  end
92
60
 
93
- def find(id)
94
- self.class.find(id)
95
- end
61
+ # Longer hand methods for completeness. Internally encourage shorter attrs.
62
+ alias_method :attributes=, :attrs=
63
+ alias_method :attributes, :attrs
96
64
 
97
- def delete
98
- self.class.delete(@attrs[:id]) if @attrs[:id]
99
- end
100
-
101
- def table_name
102
- self.class.table_name
103
- end
104
-
105
- def partition_key
106
- self.class.partition_key
107
- end
108
-
109
- # For render json: item
110
- def as_json(options={})
111
- @attrs
112
- end
113
-
114
- # Longer hand methods for completeness.
115
- # Internallly encourage the shorter attrs method.
116
- def attributes=(attributes)
117
- @attributes = attributes
118
- end
119
-
120
- def attributes
121
- @attributes
122
- end
123
-
124
- # Adds very little wrapper logic to scan.
125
- #
126
- # * Automatically add table_name to options for convenience.
127
- # * Decorates return value. Returns Array of [MyModel.new] instead of the
128
- # dynamodb client response.
129
- #
130
- # Other than that, usage is same was using the dynamodb client scan method
131
- # directly. Example:
132
- #
133
- # MyModel.scan(
134
- # expression_attribute_names: {"#updated_at"=>"updated_at"},
135
- # filter_expression: "#updated_at between :start_time and :end_time",
136
- # expression_attribute_values: {
137
- # ":start_time" => "2010-01-01T00:00:00",
138
- # ":end_time" => "2020-01-01T00:00:00"
139
- # }
140
- # )
65
+ # Because using `define_attribute_methods *names` as part of `add_field` dsl.
66
+ # Found that define_attribute_methods is required for dirty support.
67
+ # This adds missing_attribute method that will look for a method called attribute.
68
+ # send(match.target, match.attr_name, *args, &block)
69
+ # send(:attribute, :my_column)
70
+ # The error message when an attribute is not found is more helpful when this is defined.
141
71
  #
142
- # AWS Docs examples: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GettingStarted.Ruby.04.html
143
- def self.scan(params={})
144
- log("It's recommended to not use scan for production. It can be slow and expensive. You can a LSI or GSI and query the index instead.")
145
- log("Scanning table: #{table_name}")
146
- params = { table_name: table_name }.merge(params)
147
- resp = db.scan(params)
148
- resp.items.map {|i| self.new(i) }
72
+ # It looks confusing that we always raise an error for attribute because fields must
73
+ # be defined to access them through dot notation. This is because users to
74
+ # explicitly define fields and access undeclared fields with hash notation [],
75
+ # read_attribute, or attributes.
76
+ def attribute(name)
77
+ raise NoMethodError, "undefined method '#{name}' for #{self.class}"
149
78
  end
150
79
 
151
- # Adds very little wrapper logic to query.
152
- #
153
- # * Automatically add table_name to options for convenience.
154
- # * Decorates return value. Returns Array of [MyModel.new] instead of the
155
- # dynamodb client response.
156
- #
157
- # Other than that, usage is same was using the dynamodb client query method
158
- # directly. Example:
159
- #
160
- # MyModel.query(
161
- # index_name: 'category-index',
162
- # expression_attribute_names: { "#category_name" => "category" },
163
- # expression_attribute_values: { ":category_value" => "Entertainment" },
164
- # key_condition_expression: "#category_name = :category_value",
165
- # )
166
- #
167
- # AWS Docs examples: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GettingStarted.Ruby.04.html
168
- def self.query(params={})
169
- params = { table_name: table_name }.merge(params)
170
- resp = db.query(params)
171
- resp.items.map {|i| self.new(i) }
172
- end
173
-
174
- # Creates a new chainable ActiveRecord Query-style instance with a certain index_name.
175
- #
176
- # Post.index_name("category-index").where(category: "Drama")
177
- #
178
- def self.index_name(name)
179
- _new_query.index_name(name)
180
- end
181
-
182
- # Translates simple query searches:
183
- #
184
- # Post.index_name("category-index").where(category: "Drama")
185
- #
186
- # translates to
187
- #
188
- # resp = db.query(
189
- # table_name: "demo-dev-post",
190
- # index_name: 'category-index',
191
- # expression_attribute_names: { "#category_name" => "category" },
192
- # expression_attribute_values: { ":category_value" => category },
193
- # key_condition_expression: "#category_name = :category_value",
194
- # )
195
- def self.where(attributes)
196
- _new_query.where(attributes)
80
+ def read_attribute(field)
81
+ @attrs[field.to_sym]
197
82
  end
198
83
 
199
- def self.replace(attrs)
200
- # Automatically adds some attributes:
201
- # partition key unique id
202
- # created_at and updated_at timestamps. Timestamp format from AWS docs: http://amzn.to/2z98Bdc
203
- defaults = {
204
- partition_key => Digest::SHA1.hexdigest([Time.now, rand].join)
205
- }
206
- item = defaults.merge(attrs)
207
- item["created_at"] ||= Time.now.utc.strftime('%Y-%m-%dT%TZ')
208
- item["updated_at"] = Time.now.utc.strftime('%Y-%m-%dT%TZ')
209
-
210
- # put_item full replaces the item
211
- resp = db.put_item(
212
- table_name: table_name,
213
- item: item
214
- )
215
-
216
- # The resp does not contain the attrs. So might as well return
217
- # the original item with the generated partition_key value
218
- item
84
+ # Only updates in memory, does not save to database.
85
+ # Same as ActiveRecord behavior.
86
+ def write_attribute(field, value)
87
+ @attrs[field.to_sym] = value
219
88
  end
220
89
 
221
- def self.find(id)
222
- params =
223
- case id
224
- when String
225
- { partition_key => id }
226
- when Hash
227
- id
228
- end
229
-
230
- resp = db.get_item(
231
- table_name: table_name,
232
- key: params
233
- )
234
- attributes = resp.item # unwraps the item's attributes
235
- self.new(attributes) if attributes
90
+ def update_attribute(field, value)
91
+ write_attribute(field, value)
92
+ update(@attrs, {validate: false})
93
+ valid? # ActiveRecord return value behavior
236
94
  end
237
95
 
238
- # Two ways to use the delete method:
239
- #
240
- # 1. Specify the key as a String. In this case the key will is the partition_key
241
- # set on the model.
242
- # MyModel.delete("728e7b5df40b93c3ea6407da8ac3e520e00d7351")
243
- #
244
- # 2. Specify the key as a Hash, you can arbitrarily specific the key structure this way
245
- # MyModel.delete("728e7b5df40b93c3ea6407da8ac3e520e00d7351")
246
- #
247
- # options is provided in case you want to specific condition_expression or
248
- # expression_attribute_values.
249
- def self.delete(key_object, options={})
250
- if key_object.is_a?(String)
251
- key = {
252
- partition_key => key_object
253
- }
254
- else # it should be a Hash
255
- key = key_object
256
- end
257
-
258
- params = {
259
- table_name: table_name,
260
- key: key
261
- }
262
- # In case you want to specify condition_expression or expression_attribute_values
263
- params = params.merge(options)
264
-
265
- resp = db.delete_item(params)
96
+ def delete_attribute(field)
97
+ @attrs.delete(field.to_sym)
98
+ update(@attrs, {validate: false})
99
+ valid? # ActiveRecord does not have a delete_attribute. Follow update_attribute behavior.
266
100
  end
101
+ alias :remove_attribute :delete_attribute
267
102
 
268
- # When called with an argument we'll set the internal @partition_key value
269
- # When called without an argument just retun it.
270
- # class Comment < Dynomite::Item
271
- # partition_key "post_id"
272
- # end
273
- def self.partition_key(*args)
274
- case args.size
275
- when 0
276
- @partition_key || "id" # defaults to id
277
- when 1
278
- @partition_key = args[0].to_s
103
+ def update_attribute_presence(field, value)
104
+ if value.present?
105
+ update_attribute(field, value)
106
+ else # nil or empty string or empty array
107
+ delete_attribute(field)
279
108
  end
280
109
  end
281
110
 
282
- def self.table_name(*args)
283
- case args.size
284
- when 0
285
- get_table_name
286
- when 1
287
- set_table_name(args[0])
288
- end
111
+ def [](field)
112
+ read_attribute(field)
289
113
  end
290
114
 
291
- def self.get_table_name
292
- @table_name ||= self.name.pluralize.gsub('::','-').underscore.dasherize
293
- [table_namespace, @table_name].reject {|s| s.nil? || s.empty?}.join('-')
115
+ def []=(field, value)
116
+ write_attribute(field, value)
294
117
  end
295
118
 
296
- def self.set_table_name(value)
297
- @table_name = value
119
+ # For render json: item
120
+ def as_json(options={})
121
+ @attrs
298
122
  end
299
123
 
300
- def self.table
301
- Aws::DynamoDB::Table.new(name: table_name, client: db)
124
+ # Required for ActiveModel
125
+ def persisted?
126
+ !new_record?
302
127
  end
303
128
 
304
- def self.count
305
- table.item_count
129
+ def reload
130
+ if persisted?
131
+ id = @attrs[partition_key_field]
132
+ item = if sort_key_field
133
+ find(partition_key_field => id, sort_key_field => @attrs[sort_key_field])
134
+ else
135
+ find(id) # item has different object_id
136
+ end
137
+ @attrs = item.attrs # replace current loaded attributes
138
+ end
139
+ self
306
140
  end
307
141
 
308
- # Defines column. Defined column can be accessed by getter and setter methods of the same
309
- # name (e.g. [model.my_column]). Attributes with undefined columns can be accessed by
310
- # [model.attrs] method.
311
- def self.column(*names)
312
- names.each(&method(:add_column))
142
+ # p1 = Product.first
143
+ # p2 = Product.first
144
+ # p1 == p2 # => true
145
+ #
146
+ # p1 = Product.first
147
+ # products = Product.all
148
+ # products.include?(p1) # => true
149
+ def ==(other)
150
+ self.class == other.class && self.attrs == other.attrs
313
151
  end
314
152
 
315
- # @see Item.column
316
- def self.add_column(name)
317
- if Dynomite::RESERVED_WORDS.include?(name)
318
- raise ReservedWordError, "'#{name}' is a reserved word"
319
- end
320
-
321
- define_method(name) do
322
- @attrs[name.to_s]
153
+ def to_param
154
+ if id
155
+ id
156
+ else
157
+ raise "Need to define a id field for to_param"
323
158
  end
324
-
325
- define_method("#{name}=") do |value|
326
- @attrs[name.to_s] = value
327
- end
328
- end
329
-
330
- def self._new_query
331
- Dynomite::Query.new(self, {})
332
159
  end
333
160
  end
334
161
  end
@@ -0,0 +1,19 @@
1
+ class Dynomite::Migration::Dsl
2
+ module Accessor
3
+ def dsl_accessor(*names)
4
+ names.each do |name|
5
+ define_dsl_accessor(name)
6
+ end
7
+ end
8
+
9
+ def define_dsl_accessor(name)
10
+ define_method(name) do |*args|
11
+ if args.empty?
12
+ instance_variable_get("@#{name}")
13
+ else
14
+ instance_variable_set("@#{name}", args.first)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,42 @@
1
+ module Dynomite::Migration::Dsl::Index
2
+ class Base
3
+ include Dynomite::Types
4
+
5
+ def partition_key_attribute_name
6
+ get_attribute_name(:partition_key)
7
+ end
8
+
9
+ def sort_key_attribute_name
10
+ get_attribute_name(:sort_key)
11
+ end
12
+
13
+ def get_attribute_name(field=:partition_key)
14
+ value = instance_variable_get("@#{field}") # IE: @partition_key
15
+ value.to_s.split(':').first if value
16
+ end
17
+
18
+ def partition_key_attribute_type
19
+ get_attribute_type(:partition_key)
20
+ end
21
+
22
+ def sort_key_attribute_type
23
+ get_attribute_type(:sort_key)
24
+ end
25
+
26
+ def get_attribute_type(field=:partition_key)
27
+ value = instance_variable_get("@#{field}") # IE: @partition_key
28
+ name, type = value.to_s.split(':')
29
+ type ||= "string"
30
+ type_map(type)
31
+ end
32
+
33
+ def conventional_index_name
34
+ # DynamoDB requires index names to be at least 3 characters long, otherwise:
35
+ # Error: Unable to : 1 validation error detected: Value 'id' at 'globalSecondaryIndexes.1.member.indexName' failed to satisfy constraint: Member must have length greater than or equal to 3
36
+ # The id valid is too short sadly.
37
+ # Adding -index to the end of the index name is a safe way to ensure that.
38
+ # Annoying that the index_name("id-index") is going to be a little longer.
39
+ [partition_key_attribute_name, sort_key_attribute_name, 'index'].compact.join('-')
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,59 @@
1
+ module Dynomite::Migration::Dsl::Index
2
+ class Gsi < Base
3
+ attr_reader :action, :attrs
4
+ def initialize(action, attrs)
5
+ @action = action
6
+ @params = attrs.dup
7
+ # Delete the special DSL keys. Keep the rest and pass through to AWS create_table or update_table
8
+ @partition_key = @params.delete(:partition_key) || @params.delete(:hash_key) # required for create, optional for update (only index needed)
9
+ @sort_key = @params.delete(:sort_key) || @params.delete(:range_key) # optional for create
10
+ end
11
+
12
+ def params
13
+ send("params_#{@action}")
14
+ end
15
+
16
+ # https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/DynamoDB/Client.html#create_table-instance_method
17
+ def params_create
18
+ @params[:key_schema] ||= []
19
+ # HASH - add to beginning of array
20
+ @params[:key_schema].unshift(attribute_name: partition_key_attribute_name, key_type: "HASH") unless @partition_key.blank?
21
+ # RANGE - add to end of array
22
+ @params[:key_schema] << {attribute_name: sort_key_attribute_name, key_type: "RANGE"} unless @sort_key.blank?
23
+ @params[:index_name] ||= conventional_index_name
24
+ @params[:projection] ||= {projection_type: "ALL"}
25
+ @params[:provisioned_throughput] = normalize_provisioned_throughput(@params[:provisioned_throughput]) if @params[:provisioned_throughput]
26
+ @params
27
+ end
28
+
29
+ # https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/DynamoDB/Client.html#update_table-instance_method
30
+ def params_update
31
+ @params[:index_name] ||= conventional_index_name
32
+ @params[:provisioned_throughput] = normalize_provisioned_throughput(@params[:provisioned_throughput]) if @params[:provisioned_throughput]
33
+ @params
34
+ end
35
+
36
+ def params_delete
37
+ @params[:index_name] ||= conventional_index_name
38
+ @params
39
+ end
40
+
41
+ def attribute_definitions
42
+ definitions = []
43
+ definitions << {attribute_name: partition_key_attribute_name, attribute_type: partition_key_attribute_type} unless @partition_key.blank?
44
+ definitions << {attribute_name: sort_key_attribute_name, attribute_type: sort_key_attribute_type} unless @sort_key.blank?
45
+ definitions
46
+ end
47
+
48
+ def normalize_provisioned_throughput(value)
49
+ if value.is_a?(Hash)
50
+ value
51
+ else
52
+ {
53
+ read_capacity_units: value,
54
+ write_capacity_units: value,
55
+ }
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,27 @@
1
+ module Dynomite::Migration::Dsl::Index
2
+ class Lsi < Base
3
+ attr_reader :attrs
4
+ def initialize(partition_key_name, attrs)
5
+ @partition_key_name = partition_key_name
6
+ @params = attrs.dup
7
+ # Delete the special DSL keys. Keep the rest and pass through to AWS create_table or update_table
8
+ @sort_key = @params.delete(:sort_key) || @params.delete(:range_key) # require for LSI index since it must be a composite key
9
+ end
10
+
11
+ def params
12
+ @params[:key_schema] ||= []
13
+ @params[:key_schema] << {attribute_name: @partition_key_name, key_type: "HASH"}
14
+ @params[:key_schema] << {attribute_name: sort_key_attribute_name, key_type: "RANGE"}
15
+ @params[:index_name] ||= conventional_index_name
16
+ @params[:projection] ||= {projection_type: "ALL"}
17
+ @params
18
+ end
19
+
20
+ def attribute_definitions
21
+ definitions = []
22
+ definitions << {attribute_name: partition_key_attribute_name, attribute_type: partition_key_attribute_type} unless @partition_key.blank?
23
+ definitions << {attribute_name: sort_key_attribute_name, attribute_type: sort_key_attribute_type} unless @sort_key.blank?
24
+ definitions
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,72 @@
1
+ class Dynomite::Migration::Dsl
2
+ module Index
3
+ def add_gsi(attrs={})
4
+ attrs = normalize_index_attrs(attrs, :partition_key)
5
+ @gsi_indexes << Gsi.new(:create, attrs) # store in @gsi_indexes for the parent Dsl to use
6
+ end
7
+ alias create_gsi add_gsi
8
+
9
+ def update_gsi(attrs={})
10
+ attrs = normalize_index_attrs(attrs, :partition_key)
11
+ @gsi_indexes << Gsi.new(:update, attrs) # store in @gsi_indexes for the parent Dsl to use
12
+ end
13
+
14
+ def remove_gsi(attrs={})
15
+ attrs = normalize_index_attrs(attrs, :partition_key)
16
+ @gsi_indexes << Gsi.new(:delete, attrs) # store in @gsi_indexes for the parent Dsl to use
17
+ end
18
+ alias delete_gsi remove_gsi
19
+
20
+ def add_lsi(attrs={})
21
+ # dont need create with lsi indexes, they are created as part of create_table
22
+ partition_key_name = @partition_key_identifier.split(':').first
23
+ attrs = normalize_index_attrs(attrs, :sort_key)
24
+ @lsi_indexes << Lsi.new(partition_key_name, attrs) # store in @lsi_indexes for the parent Dsl to use
25
+ end
26
+ alias create_lsi add_lsi
27
+
28
+ def normalize_index_attrs(attrs, type=:partition_key)
29
+ if attrs.is_a?(String) || attrs.is_a?(Symbol)
30
+ {type => attrs.to_s}
31
+ else
32
+ attrs
33
+ end
34
+ end
35
+
36
+ # maps each lsi to the hash structure expected by dynamodb update_table
37
+ # under the lsi_secondary_index_creates key:
38
+ #
39
+ # { create: {...} }
40
+ # { update: {...} }
41
+ # { delete: {...} }
42
+ def lsi_secondary_index_creates
43
+ @lsi_indexes.map do |lsi|
44
+ lsi.params
45
+ end
46
+ end
47
+
48
+ # maps each lsi to the hash structure expected by dynamodb update_table
49
+ # under the gsi_secondary_index_creates key:
50
+ #
51
+ # { create: {...} }
52
+ # { update: {...} }
53
+ # { delete: {...} }
54
+ def gsi_secondary_index_creates
55
+ @gsi_indexes.map do |gsi|
56
+ gsi.params
57
+ end
58
+ end
59
+
60
+ # maps each gsi to the hash structure expected by dynamodb update_table
61
+ # under the global_secondary_index_updates key:
62
+ #
63
+ # { create: {...} }
64
+ # { update: {...} }
65
+ # { delete: {...} }
66
+ def global_secondary_index_updates
67
+ @gsi_indexes.map do |gsi|
68
+ { gsi.action => gsi.params }
69
+ end
70
+ end
71
+ end
72
+ end