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