dynamoid-edge 1.1.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 +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
|