dynamoid-edge 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +21 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +20 -0
- data/README.markdown +377 -0
- data/Rakefile +67 -0
- data/dynamoid-edge.gemspec +74 -0
- data/lib/dynamoid/adapter.rb +181 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v2.rb +761 -0
- data/lib/dynamoid/associations/association.rb +105 -0
- data/lib/dynamoid/associations/belongs_to.rb +44 -0
- data/lib/dynamoid/associations/has_and_belongs_to_many.rb +40 -0
- data/lib/dynamoid/associations/has_many.rb +39 -0
- data/lib/dynamoid/associations/has_one.rb +39 -0
- data/lib/dynamoid/associations/many_association.rb +191 -0
- data/lib/dynamoid/associations/single_association.rb +69 -0
- data/lib/dynamoid/associations.rb +106 -0
- data/lib/dynamoid/components.rb +37 -0
- data/lib/dynamoid/config/options.rb +78 -0
- data/lib/dynamoid/config.rb +54 -0
- data/lib/dynamoid/criteria/chain.rb +212 -0
- data/lib/dynamoid/criteria.rb +29 -0
- data/lib/dynamoid/dirty.rb +47 -0
- data/lib/dynamoid/document.rb +201 -0
- data/lib/dynamoid/errors.rb +63 -0
- data/lib/dynamoid/fields.rb +156 -0
- data/lib/dynamoid/finders.rb +197 -0
- data/lib/dynamoid/identity_map.rb +92 -0
- data/lib/dynamoid/middleware/identity_map.rb +16 -0
- data/lib/dynamoid/persistence.rb +324 -0
- data/lib/dynamoid/validations.rb +36 -0
- data/lib/dynamoid.rb +50 -0
- metadata +226 -0
@@ -0,0 +1,156 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module Dynamoid #:nodoc:
|
3
|
+
# All fields on a Dynamoid::Document must be explicitly defined -- if you have fields in the database that are not
|
4
|
+
# specified with field, then they will be ignored.
|
5
|
+
module Fields
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
PERMITTED_KEY_TYPES = [
|
9
|
+
:number,
|
10
|
+
:integer,
|
11
|
+
:string,
|
12
|
+
:datetime
|
13
|
+
]
|
14
|
+
|
15
|
+
# Initialize the attributes we know the class has, in addition to our magic attributes: id, created_at, and updated_at.
|
16
|
+
included do
|
17
|
+
class_attribute :attributes
|
18
|
+
class_attribute :range_key
|
19
|
+
|
20
|
+
self.attributes = {}
|
21
|
+
field :created_at, :datetime
|
22
|
+
field :updated_at, :datetime
|
23
|
+
|
24
|
+
field :id #Default primary key
|
25
|
+
end
|
26
|
+
|
27
|
+
module ClassMethods
|
28
|
+
|
29
|
+
# Specify a field for a document.
|
30
|
+
#
|
31
|
+
# Its type determines how it is coerced when read in and out of the datastore.
|
32
|
+
# You can specify :integer, :number, :set, :array, :datetime, and :serialized,
|
33
|
+
# or specify a class that defines a serialization strategy.
|
34
|
+
#
|
35
|
+
# If you specify a class for field type, Dynamoid will serialize using
|
36
|
+
# `dynamoid_dump` or `dump` methods, and load using `dynamoid_load` or `load` methods.
|
37
|
+
#
|
38
|
+
# Default field type is :string.
|
39
|
+
#
|
40
|
+
# @param [Symbol] name the name of the field
|
41
|
+
# @param [Symbol] type the type of the field (refer to method description for details)
|
42
|
+
# @param [Hash] options any additional options for the field
|
43
|
+
#
|
44
|
+
# @since 0.2.0
|
45
|
+
def field(name, type = :string, options = {})
|
46
|
+
named = name.to_s
|
47
|
+
if type == :float
|
48
|
+
Dynamoid.logger.warn("Field type :float, which you declared for '#{name}', is deprecated in favor of :number.")
|
49
|
+
type = :number
|
50
|
+
end
|
51
|
+
self.attributes = attributes.merge(name => {:type => type}.merge(options))
|
52
|
+
|
53
|
+
define_method(named) { read_attribute(named) }
|
54
|
+
define_method("#{named}?") { !read_attribute(named).nil? }
|
55
|
+
define_method("#{named}=") {|value| write_attribute(named, value) }
|
56
|
+
end
|
57
|
+
|
58
|
+
def range(name, type = :string)
|
59
|
+
field(name, type)
|
60
|
+
self.range_key = name
|
61
|
+
end
|
62
|
+
|
63
|
+
def table(options)
|
64
|
+
#a default 'id' column is created when Dynamoid::Document is included
|
65
|
+
unless(attributes.has_key? hash_key)
|
66
|
+
remove_field :id
|
67
|
+
field(hash_key)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def remove_field(field)
|
72
|
+
field = field.to_sym
|
73
|
+
attributes.delete(field) or raise "No such field"
|
74
|
+
remove_method field
|
75
|
+
remove_method :"#{field}="
|
76
|
+
remove_method :"#{field}?"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# You can access the attributes of an object directly on its attributes method, which is by default an empty hash.
|
81
|
+
attr_accessor :attributes
|
82
|
+
alias :raw_attributes :attributes
|
83
|
+
|
84
|
+
# Write an attribute on the object. Also marks the previous value as dirty.
|
85
|
+
#
|
86
|
+
# @param [Symbol] name the name of the field
|
87
|
+
# @param [Object] value the value to assign to that field
|
88
|
+
#
|
89
|
+
# @since 0.2.0
|
90
|
+
def write_attribute(name, value)
|
91
|
+
if (size = value.to_s.size) > MAX_ITEM_SIZE
|
92
|
+
Dynamoid.logger.warn "DynamoDB can't store items larger than #{MAX_ITEM_SIZE} and the #{name} field has a length of #{size}."
|
93
|
+
end
|
94
|
+
|
95
|
+
if association = @associations[name]
|
96
|
+
association.reset
|
97
|
+
end
|
98
|
+
|
99
|
+
attributes[name.to_sym] = value
|
100
|
+
end
|
101
|
+
alias :[]= :write_attribute
|
102
|
+
|
103
|
+
# Read an attribute from an object.
|
104
|
+
#
|
105
|
+
# @param [Symbol] name the name of the field
|
106
|
+
#
|
107
|
+
# @since 0.2.0
|
108
|
+
def read_attribute(name)
|
109
|
+
attributes[name.to_sym]
|
110
|
+
end
|
111
|
+
alias :[] :read_attribute
|
112
|
+
|
113
|
+
# Updates multiple attibutes at once, saving the object once the updates are complete.
|
114
|
+
#
|
115
|
+
# @param [Hash] attributes a hash of attributes to update
|
116
|
+
#
|
117
|
+
# @since 0.2.0
|
118
|
+
def update_attributes(attributes)
|
119
|
+
attributes.each {|attribute, value| self.write_attribute(attribute, value)} unless attributes.nil? || attributes.empty?
|
120
|
+
save
|
121
|
+
end
|
122
|
+
|
123
|
+
# Update a single attribute, saving the object afterwards.
|
124
|
+
#
|
125
|
+
# @param [Symbol] attribute the attribute to update
|
126
|
+
# @param [Object] value the value to assign it
|
127
|
+
#
|
128
|
+
# @since 0.2.0
|
129
|
+
def update_attribute(attribute, value)
|
130
|
+
write_attribute(attribute, value)
|
131
|
+
save
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
# Automatically called during the created callback to set the created_at time.
|
137
|
+
#
|
138
|
+
# @since 0.2.0
|
139
|
+
def set_created_at
|
140
|
+
self.created_at = DateTime.now
|
141
|
+
end
|
142
|
+
|
143
|
+
# Automatically called during the save callback to set the updated_at time.
|
144
|
+
#
|
145
|
+
# @since 0.2.0
|
146
|
+
def set_updated_at
|
147
|
+
self.updated_at = DateTime.now
|
148
|
+
end
|
149
|
+
|
150
|
+
def set_type
|
151
|
+
self.type ||= self.class.to_s if self.class.attributes[:type]
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module Dynamoid
|
3
|
+
|
4
|
+
# This module defines the finder methods that hang off the document at the
|
5
|
+
# class level, like find, find_by_id, and the method_missing style finders.
|
6
|
+
module Finders
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
RANGE_MAP = {
|
10
|
+
'gt' => :range_greater_than,
|
11
|
+
'lt' => :range_less_than,
|
12
|
+
'gte' => :range_gte,
|
13
|
+
'lte' => :range_lte,
|
14
|
+
'begins_with' => :range_begins_with,
|
15
|
+
'between' => :range_between,
|
16
|
+
'eq' => :range_eq
|
17
|
+
}
|
18
|
+
|
19
|
+
module ClassMethods
|
20
|
+
|
21
|
+
# Find one or many objects, specified by one id or an array of ids.
|
22
|
+
#
|
23
|
+
# @param [Array/String] *id an array of ids or one single id
|
24
|
+
#
|
25
|
+
# @return [Dynamoid::Document] one object or an array of objects, depending on whether the input was an array or not
|
26
|
+
#
|
27
|
+
# @since 0.2.0
|
28
|
+
def find(*ids)
|
29
|
+
options = if ids.last.is_a? Hash
|
30
|
+
ids.slice!(-1)
|
31
|
+
else
|
32
|
+
{}
|
33
|
+
end
|
34
|
+
expects_array = ids.first.kind_of?(Array)
|
35
|
+
|
36
|
+
ids = Array(ids.flatten.uniq)
|
37
|
+
if ids.count == 1
|
38
|
+
result = self.find_by_id(ids.first, options)
|
39
|
+
expects_array ? Array(result) : result
|
40
|
+
else
|
41
|
+
find_all(ids)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Return objects found by the given array of ids, either hash keys, or hash/range key combinations using BatchGet.
|
46
|
+
# Returns empty array if no results found.
|
47
|
+
#
|
48
|
+
# @param [Array<ID>] ids
|
49
|
+
# @param [Hash] options: Passed to the underlying query.
|
50
|
+
#
|
51
|
+
# @example
|
52
|
+
# find all the user with hash key
|
53
|
+
# User.find_all(['1', '2', '3'])
|
54
|
+
#
|
55
|
+
# find all the tweets using hash key and range key with consistent read
|
56
|
+
# Tweet.find_all([['1', 'red'], ['1', 'green']], :consistent_read => true)
|
57
|
+
def find_all(ids, options = {})
|
58
|
+
items = Dynamoid.adapter.read(self.table_name, ids, options)
|
59
|
+
items ? items[self.table_name].map{|i| from_database(i)} : []
|
60
|
+
end
|
61
|
+
|
62
|
+
# Find one object directly by id.
|
63
|
+
#
|
64
|
+
# @param [String] id the id of the object to find
|
65
|
+
#
|
66
|
+
# @return [Dynamoid::Document] the found object, or nil if nothing was found
|
67
|
+
#
|
68
|
+
# @since 0.2.0
|
69
|
+
def find_by_id(id, options = {})
|
70
|
+
if item = Dynamoid.adapter.read(self.table_name, id, options)
|
71
|
+
from_database(item)
|
72
|
+
else
|
73
|
+
nil
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Find one object directly by hash and range keys
|
78
|
+
#
|
79
|
+
# @param [String] hash_key of the object to find
|
80
|
+
# @param [String/Number] range_key of the object to find
|
81
|
+
#
|
82
|
+
def find_by_composite_key(hash_key, range_key, options = {})
|
83
|
+
find_by_id(hash_key, options.merge({:range_key => range_key}))
|
84
|
+
end
|
85
|
+
|
86
|
+
# Find all objects by hash and range keys.
|
87
|
+
#
|
88
|
+
# @example find all ChamberTypes whose level is greater than 1
|
89
|
+
# class ChamberType
|
90
|
+
# include Dynamoid::Document
|
91
|
+
# field :chamber_type, :string
|
92
|
+
# range :level, :integer
|
93
|
+
# table :key => :chamber_type
|
94
|
+
# end
|
95
|
+
# ChamberType.find_all_by_composite_key('DustVault', range_greater_than: 1)
|
96
|
+
#
|
97
|
+
# @param [String] hash_key of the objects to find
|
98
|
+
# @param [Hash] options the options for the range key
|
99
|
+
# @option options [Range] :range_value find the range key within this range
|
100
|
+
# @option options [Number] :range_greater_than find range keys greater than this
|
101
|
+
# @option options [Number] :range_less_than find range keys less than this
|
102
|
+
# @option options [Number] :range_gte find range keys greater than or equal to this
|
103
|
+
# @option options [Number] :range_lte find range keys less than or equal to this
|
104
|
+
#
|
105
|
+
# @return [Array] an array of all matching items
|
106
|
+
#
|
107
|
+
def find_all_by_composite_key(hash_key, options = {})
|
108
|
+
Dynamoid.adapter.query(self.table_name, options.merge({hash_value: hash_key})).collect do |item|
|
109
|
+
from_database(item)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Find all objects by using local secondary or global secondary index
|
114
|
+
#
|
115
|
+
# @example
|
116
|
+
# class User
|
117
|
+
# include Dynamoid::Document
|
118
|
+
# field :email, :string
|
119
|
+
# field :age, :integer
|
120
|
+
# field :gender, :string
|
121
|
+
# field :rank :number
|
122
|
+
# table :key => :email
|
123
|
+
# global_secondary_index :hash_key => :age, :range_key => :gender
|
124
|
+
# end
|
125
|
+
# User.find_all_by_secondary_index(:age => 5, :range => {"rank.lte" => 10})
|
126
|
+
#
|
127
|
+
# @param [Hash] eg: {:age => 5}
|
128
|
+
# @param [Hash] eg: {"rank.lte" => 10}
|
129
|
+
# @param [Hash] options - @TODO support more options in future such as
|
130
|
+
# query filter, projected keys etc
|
131
|
+
# @return [Array] an array of all matching items
|
132
|
+
def find_all_by_secondary_index(hash, options = {})
|
133
|
+
range = options[:range] || {}
|
134
|
+
hash_key_field, hash_key_value = hash.first
|
135
|
+
range_key_field, range_key_value = range.first
|
136
|
+
range_op_mapped = nil
|
137
|
+
|
138
|
+
if range_key_field
|
139
|
+
range_key_field = range_key_field.to_s
|
140
|
+
range_key_op = "eq"
|
141
|
+
if range_key_field.include?(".")
|
142
|
+
range_key_field, range_key_op = range_key_field.split(".", 2)
|
143
|
+
end
|
144
|
+
range_op_mapped = RANGE_MAP.fetch(range_key_op)
|
145
|
+
end
|
146
|
+
|
147
|
+
# Find the index
|
148
|
+
index = self.find_index(hash_key_field, range_key_field)
|
149
|
+
raise Dynamoid::Errors::MissingIndex if index.nil?
|
150
|
+
|
151
|
+
# query
|
152
|
+
opts = {
|
153
|
+
:hash_key => hash_key_field.to_s,
|
154
|
+
:hash_value => hash_key_value,
|
155
|
+
:index_name => index.name,
|
156
|
+
}
|
157
|
+
if range_key_field
|
158
|
+
opts[:range_key] = range_key_field
|
159
|
+
opts[range_op_mapped] = range_key_value
|
160
|
+
end
|
161
|
+
Dynamoid.adapter.query(self.table_name, opts).map do |item|
|
162
|
+
from_database(item)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# Find using exciting method_missing finders attributes. Uses criteria chains under the hood to accomplish this neatness.
|
167
|
+
#
|
168
|
+
# @example find a user by a first name
|
169
|
+
# User.find_by_first_name('Josh')
|
170
|
+
#
|
171
|
+
# @example find all users by first and last name
|
172
|
+
# User.find_all_by_first_name_and_last_name('Josh', 'Symonds')
|
173
|
+
#
|
174
|
+
# @return [Dynamoid::Document/Array] the found object, or an array of found objects if all was somewhere in the method
|
175
|
+
#
|
176
|
+
# @since 0.2.0
|
177
|
+
def method_missing(method, *args)
|
178
|
+
if method =~ /find/
|
179
|
+
finder = method.to_s.split('_by_').first
|
180
|
+
attributes = method.to_s.split('_by_').last.split('_and_')
|
181
|
+
|
182
|
+
chain = Dynamoid::Criteria::Chain.new(self)
|
183
|
+
chain.query = Hash.new.tap {|h| attributes.each_with_index {|attr, index| h[attr.to_sym] = args[index]}}
|
184
|
+
|
185
|
+
if finder =~ /all/
|
186
|
+
return chain.all
|
187
|
+
else
|
188
|
+
return chain.first
|
189
|
+
end
|
190
|
+
else
|
191
|
+
super
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module Dynamoid
|
2
|
+
module IdentityMap
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
def self.clear
|
6
|
+
Dynamoid.included_models.each { |m| m.identity_map.clear }
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def identity_map
|
11
|
+
@identity_map ||= {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def from_database(attrs = {})
|
15
|
+
return super if identity_map_off?
|
16
|
+
|
17
|
+
key = identity_map_key(attrs)
|
18
|
+
document = identity_map[key]
|
19
|
+
|
20
|
+
if document.nil?
|
21
|
+
document = super
|
22
|
+
identity_map[key] = document
|
23
|
+
else
|
24
|
+
document.load(attrs)
|
25
|
+
end
|
26
|
+
|
27
|
+
document
|
28
|
+
end
|
29
|
+
|
30
|
+
def find_by_id(id, options = {})
|
31
|
+
return super if identity_map_off?
|
32
|
+
|
33
|
+
key = id.to_s
|
34
|
+
|
35
|
+
if range_key = options[:range_key]
|
36
|
+
key += "::#{range_key}"
|
37
|
+
end
|
38
|
+
|
39
|
+
if identity_map[key]
|
40
|
+
identity_map[key]
|
41
|
+
else
|
42
|
+
super
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def identity_map_key(attrs)
|
47
|
+
key = attrs[hash_key].to_s
|
48
|
+
if range_key
|
49
|
+
key += "::#{attrs[range_key]}"
|
50
|
+
end
|
51
|
+
key
|
52
|
+
end
|
53
|
+
|
54
|
+
def identity_map_on?
|
55
|
+
Dynamoid::Config.identity_map
|
56
|
+
end
|
57
|
+
|
58
|
+
def identity_map_off?
|
59
|
+
!identity_map_on?
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def identity_map
|
64
|
+
self.class.identity_map
|
65
|
+
end
|
66
|
+
|
67
|
+
def save(*args)
|
68
|
+
return super if self.class.identity_map_off?
|
69
|
+
|
70
|
+
if result = super
|
71
|
+
identity_map[identity_map_key] = self
|
72
|
+
end
|
73
|
+
result
|
74
|
+
end
|
75
|
+
|
76
|
+
def delete
|
77
|
+
return super if self.class.identity_map_off?
|
78
|
+
|
79
|
+
identity_map.delete(identity_map_key)
|
80
|
+
super
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
def identity_map_key
|
85
|
+
key = hash_key.to_s
|
86
|
+
if self.class.range_key
|
87
|
+
key += "::#{range_value}"
|
88
|
+
end
|
89
|
+
key
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|