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.
- checksums.yaml +4 -4
- data/.gitignore +17 -2
- data/CHANGELOG.md +21 -0
- data/Gemfile +1 -5
- data/LICENSE.txt +22 -0
- data/README.md +6 -190
- data/Rakefile +13 -1
- data/dynomite.gemspec +9 -2
- data/exe/dynomite +14 -0
- data/lib/dynomite/associations/association.rb +126 -0
- data/lib/dynomite/associations/belongs_to.rb +35 -0
- data/lib/dynomite/associations/has_and_belongs_to_many.rb +19 -0
- data/lib/dynomite/associations/has_many.rb +19 -0
- data/lib/dynomite/associations/has_one.rb +19 -0
- data/lib/dynomite/associations/many_association.rb +257 -0
- data/lib/dynomite/associations/single_association.rb +157 -0
- data/lib/dynomite/associations.rb +248 -0
- data/lib/dynomite/autoloader.rb +25 -0
- data/lib/dynomite/cli.rb +48 -0
- data/lib/dynomite/client.rb +118 -0
- data/lib/dynomite/command.rb +89 -0
- data/lib/dynomite/completer/script.rb +6 -0
- data/lib/dynomite/completer/script.sh +10 -0
- data/lib/dynomite/completer.rb +159 -0
- data/lib/dynomite/config.rb +39 -0
- data/lib/dynomite/core.rb +18 -19
- data/lib/dynomite/engine.rb +15 -0
- data/lib/dynomite/erb.rb +5 -3
- data/lib/dynomite/error.rb +12 -0
- data/lib/dynomite/help/completion.md +20 -0
- data/lib/dynomite/help/completion_script.md +3 -0
- data/lib/dynomite/help/migrate.md +3 -0
- data/lib/dynomite/help.rb +9 -0
- data/lib/dynomite/install.rb +4 -0
- data/lib/dynomite/item/abstract.rb +15 -0
- data/lib/dynomite/item/components.rb +33 -0
- data/lib/dynomite/item/dsl.rb +101 -0
- data/lib/dynomite/item/id.rb +41 -0
- data/lib/dynomite/item/indexes/finder.rb +58 -0
- data/lib/dynomite/item/indexes/index.rb +21 -0
- data/lib/dynomite/item/indexes/primary_index.rb +18 -0
- data/lib/dynomite/item/indexes.rb +25 -0
- data/lib/dynomite/item/locking.rb +53 -0
- data/lib/dynomite/item/magic_fields.rb +66 -0
- data/lib/dynomite/item/primary_key.rb +85 -0
- data/lib/dynomite/item/query/delegates.rb +28 -0
- data/lib/dynomite/item/query/params/base.rb +42 -0
- data/lib/dynomite/item/query/params/expression_attribute.rb +79 -0
- data/lib/dynomite/item/query/params/filter.rb +41 -0
- data/lib/dynomite/item/query/params/function/attribute_exists.rb +21 -0
- data/lib/dynomite/item/query/params/function/attribute_type.rb +30 -0
- data/lib/dynomite/item/query/params/function/base.rb +33 -0
- data/lib/dynomite/item/query/params/function/begins_with.rb +32 -0
- data/lib/dynomite/item/query/params/function/contains.rb +7 -0
- data/lib/dynomite/item/query/params/function/size_fn.rb +37 -0
- data/lib/dynomite/item/query/params/helpers.rb +94 -0
- data/lib/dynomite/item/query/params/key_condition.rb +34 -0
- data/lib/dynomite/item/query/params.rb +115 -0
- data/lib/dynomite/item/query/partiql/executer.rb +72 -0
- data/lib/dynomite/item/query/partiql.rb +67 -0
- data/lib/dynomite/item/query/relation/chain.rb +125 -0
- data/lib/dynomite/item/query/relation/comparision_expression.rb +21 -0
- data/lib/dynomite/item/query/relation/comparision_map.rb +19 -0
- data/lib/dynomite/item/query/relation/delete.rb +38 -0
- data/lib/dynomite/item/query/relation/ids.rb +21 -0
- data/lib/dynomite/item/query/relation/math.rb +19 -0
- data/lib/dynomite/item/query/relation/where_field.rb +32 -0
- data/lib/dynomite/item/query/relation/where_group.rb +78 -0
- data/lib/dynomite/item/query/relation.rb +127 -0
- data/lib/dynomite/item/query.rb +7 -0
- data/lib/dynomite/item/read/find.rb +196 -0
- data/lib/dynomite/item/read/find_with_event.rb +42 -0
- data/lib/dynomite/item/read.rb +90 -0
- data/lib/dynomite/item/sti.rb +43 -0
- data/lib/dynomite/item/table_namespace.rb +43 -0
- data/lib/dynomite/item/typecaster.rb +106 -0
- data/lib/dynomite/item/waiter_methods.rb +18 -0
- data/lib/dynomite/item/write/base.rb +15 -0
- data/lib/dynomite/item/write/delete_item.rb +14 -0
- data/lib/dynomite/item/write/put_item.rb +99 -0
- data/lib/dynomite/item/write/update_item.rb +73 -0
- data/lib/dynomite/item/write.rb +204 -0
- data/lib/dynomite/item.rb +113 -286
- data/lib/dynomite/migration/dsl/accessor.rb +19 -0
- data/lib/dynomite/migration/dsl/index/base.rb +42 -0
- data/lib/dynomite/migration/dsl/index/gsi.rb +59 -0
- data/lib/dynomite/migration/dsl/index/lsi.rb +27 -0
- data/lib/dynomite/migration/dsl/index.rb +72 -0
- data/lib/dynomite/migration/dsl/primary_key.rb +62 -0
- data/lib/dynomite/migration/dsl/provisioned_throughput.rb +38 -0
- data/lib/dynomite/migration/dsl.rb +89 -142
- data/lib/dynomite/migration/file_info.rb +28 -0
- data/lib/dynomite/migration/generator.rb +30 -16
- data/lib/dynomite/migration/helpers.rb +7 -0
- data/lib/dynomite/migration/internal/migrate/create_schema_migrations.rb +17 -0
- data/lib/dynomite/migration/internal/models/schema_migration.rb +6 -0
- data/lib/dynomite/migration/runner.rb +179 -0
- data/lib/dynomite/migration/templates/create_table.rb +7 -23
- data/lib/dynomite/migration/templates/delete_table.rb +7 -0
- data/lib/dynomite/migration/templates/update_table.rb +3 -18
- data/lib/dynomite/migration.rb +53 -10
- data/lib/dynomite/reserved_words.rb +13 -3
- data/lib/dynomite/seed.rb +13 -0
- data/lib/dynomite/types.rb +22 -0
- data/lib/dynomite/version.rb +1 -1
- data/lib/dynomite/waiter.rb +40 -0
- data/lib/dynomite.rb +11 -17
- data/lib/generators/application_item/application_item_generator.rb +30 -0
- data/lib/generators/application_item/templates/application_item.rb.tt +4 -0
- data/lib/jets/commands/dynamodb_command.rb +29 -0
- data/lib/jets/commands/help/generate.md +33 -0
- data/lib/jets/commands/help/migrate.md +3 -0
- metadata +201 -17
- data/docs/migrations/long-example.rb +0 -127
- data/docs/migrations/short-example.rb +0 -40
- data/lib/dynomite/db_config.rb +0 -121
- data/lib/dynomite/errors.rb +0 -15
- data/lib/dynomite/log.rb +0 -15
- data/lib/dynomite/migration/common.rb +0 -86
- data/lib/dynomite/migration/dsl/base_secondary_index.rb +0 -73
- data/lib/dynomite/migration/dsl/global_secondary_index.rb +0 -4
- data/lib/dynomite/migration/dsl/local_secondary_index.rb +0 -8
- data/lib/dynomite/migration/executor.rb +0 -38
data/lib/dynomite/item.rb
CHANGED
|
@@ -1,334 +1,161 @@
|
|
|
1
|
-
require "
|
|
2
|
-
require "aws-sdk-dynamodb"
|
|
1
|
+
require "active_model"
|
|
3
2
|
require "digest"
|
|
4
3
|
require "yaml"
|
|
5
4
|
|
|
6
|
-
|
|
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 =
|
|
15
|
-
# post
|
|
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
|
-
#
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
#
|
|
52
|
-
def
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
#
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
61
|
+
# Longer hand methods for completeness. Internally encourage shorter attrs.
|
|
62
|
+
alias_method :attributes=, :attrs=
|
|
63
|
+
alias_method :attributes, :attrs
|
|
96
64
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
#
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
|
283
|
-
|
|
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
|
|
292
|
-
|
|
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
|
-
|
|
297
|
-
|
|
119
|
+
# For render json: item
|
|
120
|
+
def as_json(options={})
|
|
121
|
+
@attrs
|
|
298
122
|
end
|
|
299
123
|
|
|
300
|
-
|
|
301
|
-
|
|
124
|
+
# Required for ActiveModel
|
|
125
|
+
def persisted?
|
|
126
|
+
!new_record?
|
|
302
127
|
end
|
|
303
128
|
|
|
304
|
-
def
|
|
305
|
-
|
|
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
|
-
#
|
|
309
|
-
#
|
|
310
|
-
#
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|