dynomite 1.2.7 → 2.0.1

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