dynomite 1.2.7 → 2.0.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 +4 -4
- data/.gitignore +17 -2
- data/CHANGELOG.md +18 -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 +45 -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 +178 -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 +12 -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
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
class Dynomite::Item
|
|
2
|
+
module Write
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
# Not using method_missing to allow usage of dot notation and assign
|
|
6
|
+
# @attrs because it might hide actual missing methods errors.
|
|
7
|
+
# DynamoDB attrs can go many levels deep so it makes less make sense to
|
|
8
|
+
# use to dot notation.
|
|
9
|
+
|
|
10
|
+
def save(options={})
|
|
11
|
+
options.reverse_merge!(validate: true)
|
|
12
|
+
return self if options[:validate] && !valid?
|
|
13
|
+
|
|
14
|
+
action = new_record? ? :create : :update
|
|
15
|
+
run_callbacks(:save) do
|
|
16
|
+
run_callbacks(action) do
|
|
17
|
+
if action == :create
|
|
18
|
+
PutItem.call(self, options)
|
|
19
|
+
else # :update
|
|
20
|
+
call_update_strategy(options)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Similar to save, but raises an error on failed validation.
|
|
27
|
+
def save!(options={})
|
|
28
|
+
raise_error_if_invalid
|
|
29
|
+
save(options)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# post.update(title: "test", body: "body")
|
|
33
|
+
# post.update({title: "test", body: "body"}, {validate: false})
|
|
34
|
+
def update(attrs={}, options={})
|
|
35
|
+
self.attrs.merge!(attrs)
|
|
36
|
+
options.reverse_merge!(validate: true)
|
|
37
|
+
return false if options[:validate] && !valid?
|
|
38
|
+
|
|
39
|
+
run_callbacks(:save) do
|
|
40
|
+
run_callbacks(:update) do
|
|
41
|
+
call_update_strategy(options)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def call_update_strategy(options)
|
|
47
|
+
if Dynomite.config.update_strategy == :update_item
|
|
48
|
+
# Note: fields assigned directly with brackets are not tracked as changed
|
|
49
|
+
# IE: post[:title] = "test"
|
|
50
|
+
UpdateItem.call(self, options)
|
|
51
|
+
else # default
|
|
52
|
+
PutItem.call(self, options)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Similar to update, but raises an error on failed validation.
|
|
57
|
+
def update!(attrs={}, options={})
|
|
58
|
+
raise_error_if_invalid
|
|
59
|
+
update(attrs, options)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# When you add an item, the primary key attributes are the only required attributes.
|
|
63
|
+
# https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/DynamoDB/Client.html#put_item-instance_method
|
|
64
|
+
def put(options={})
|
|
65
|
+
found_primary_keys = self.attrs.keys.map(&:to_s) & primary_key_fields
|
|
66
|
+
unless primary_key_fields.sort == found_primary_keys
|
|
67
|
+
raise Dynomite::Error::InvalidPut.new("Invalid put. The primary key fields #{primary_key_fields} must be present in the attrs #{attrs}")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
options.reverse_merge!(validate: true)
|
|
71
|
+
return self if options[:validate] && !valid? # return self so can grab errors in invalid. save does the same thing
|
|
72
|
+
|
|
73
|
+
# Run callbacks for put so id is also set
|
|
74
|
+
run_callbacks(:save) do
|
|
75
|
+
run_callbacks(:update) do
|
|
76
|
+
PutItem.call(self, options.merge(put: true))
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
alias replace put
|
|
81
|
+
|
|
82
|
+
def put!(options={})
|
|
83
|
+
raise_error_if_invalid
|
|
84
|
+
put(options)
|
|
85
|
+
end
|
|
86
|
+
alias replace! put!
|
|
87
|
+
|
|
88
|
+
def destroy(options={})
|
|
89
|
+
run_callbacks(:destroy) do
|
|
90
|
+
DeleteItem.call(self, options)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def delete(options={})
|
|
95
|
+
DeleteItem.call(self, options)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
attr_reader :_touching
|
|
99
|
+
def touch(*names, **options)
|
|
100
|
+
if new_record?
|
|
101
|
+
raise Dynomite::Error, 'cannot touch on a new item'
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
time_to_assign = options.delete(:time) || Time.now
|
|
105
|
+
|
|
106
|
+
self.updated_at = time_to_assign
|
|
107
|
+
names.each do |name|
|
|
108
|
+
attrs.send("#{name}=", time_to_assign)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
@_touching = true
|
|
112
|
+
run_callbacks :touch do
|
|
113
|
+
UpdateItem.call(self, options)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
self
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Examples:
|
|
120
|
+
# user.increment(:likes)
|
|
121
|
+
# user.increment(:likes, 2)
|
|
122
|
+
def increment(attribute, by = 1)
|
|
123
|
+
self[attribute] ||= 0
|
|
124
|
+
self[attribute] += by
|
|
125
|
+
self
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Increment counter. Validations and callbacks are skipped.
|
|
129
|
+
#
|
|
130
|
+
# Examples:
|
|
131
|
+
#
|
|
132
|
+
# user.increment!(:likes)
|
|
133
|
+
# user.increment!(:likes, 2)
|
|
134
|
+
# user.increment!(:likes, touch: true)
|
|
135
|
+
# user.increment!(:likes, touch: :created_at)
|
|
136
|
+
# user.increment!(:likes, touch: [:viewed_at, :created_at])
|
|
137
|
+
#
|
|
138
|
+
def increment!(attribute, by = 1, touch: nil)
|
|
139
|
+
increment(attribute, by)
|
|
140
|
+
|
|
141
|
+
now = Time.now
|
|
142
|
+
attrs = Array(touch).inject({}) do |attrs, field|
|
|
143
|
+
attrs.merge!(field => now)
|
|
144
|
+
end if touch
|
|
145
|
+
|
|
146
|
+
run_callbacks :touch do
|
|
147
|
+
UpdateItem.new(self).save_changes(attrs: attrs, count_changes: { attribute => by })
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
self
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Example:
|
|
154
|
+
#
|
|
155
|
+
# user = User.first
|
|
156
|
+
# user.banned? # => false
|
|
157
|
+
# user.toggle(:banned)
|
|
158
|
+
# user.banned? # => true
|
|
159
|
+
#
|
|
160
|
+
def toggle(attribute)
|
|
161
|
+
self[attribute] = !public_send("#{attribute}?")
|
|
162
|
+
self
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def toggle!(attribute)
|
|
166
|
+
toggle(attribute).update_attribute(attribute, self[attribute])
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def raise_error_if_invalid
|
|
170
|
+
raise Dynomite::Error::Validation, "Validation failed: #{errors.full_messages.join(', ')}" unless valid?
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
class_methods do
|
|
174
|
+
def put(attrs={}, &block)
|
|
175
|
+
new(attrs, &block).put
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def put!(attrs={}, &block)
|
|
179
|
+
new(attrs, &block).put!
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def create(attrs={}, &block)
|
|
183
|
+
new(attrs, &block).save
|
|
184
|
+
end
|
|
185
|
+
alias create_with create
|
|
186
|
+
|
|
187
|
+
def create!(attrs={}, &block)
|
|
188
|
+
new(attrs, &block).save!
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def find_or_create_by(attrs={})
|
|
192
|
+
find_by(attrs) || create(attrs)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def find_or_create_by!(attrs={})
|
|
196
|
+
find_by(attrs) || create!(attrs)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def find_or_initialize_by(attrs)
|
|
200
|
+
find_by(attrs) || new(attrs)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
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
|