synamoid 1.2.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 +7 -0
- data/.document +5 -0
- data/.gitignore +67 -0
- data/.rspec +2 -0
- data/.travis.yml +15 -0
- data/CHANGELOG.md +48 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +20 -0
- data/README.md +443 -0
- data/Rakefile +64 -0
- data/dynamoid.gemspec +53 -0
- data/lib/dynamoid.rb +53 -0
- data/lib/dynamoid/adapter.rb +190 -0
- data/lib/dynamoid/adapter_plugin/aws_sdk_v2.rb +892 -0
- data/lib/dynamoid/associations.rb +106 -0
- data/lib/dynamoid/associations/association.rb +116 -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 +193 -0
- data/lib/dynamoid/associations/single_association.rb +69 -0
- data/lib/dynamoid/components.rb +37 -0
- data/lib/dynamoid/config.rb +58 -0
- data/lib/dynamoid/config/options.rb +78 -0
- data/lib/dynamoid/criteria.rb +29 -0
- data/lib/dynamoid/criteria/chain.rb +214 -0
- data/lib/dynamoid/dirty.rb +47 -0
- data/lib/dynamoid/document.rb +201 -0
- data/lib/dynamoid/errors.rb +66 -0
- data/lib/dynamoid/fields.rb +164 -0
- data/lib/dynamoid/finders.rb +199 -0
- data/lib/dynamoid/identity_map.rb +92 -0
- data/lib/dynamoid/indexes.rb +273 -0
- data/lib/dynamoid/middleware/identity_map.rb +16 -0
- data/lib/dynamoid/persistence.rb +359 -0
- data/lib/dynamoid/validations.rb +63 -0
- data/lib/dynamoid/version.rb +3 -0
- metadata +266 -0
@@ -0,0 +1,47 @@
|
|
1
|
+
module Dynamoid
|
2
|
+
module Dirty
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
include ActiveModel::Dirty
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
def from_database(*)
|
8
|
+
super.tap { |d| d.changed_attributes.clear }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def save(*)
|
13
|
+
clear_changes { super }
|
14
|
+
end
|
15
|
+
|
16
|
+
def update!(*)
|
17
|
+
ret = super
|
18
|
+
clear_changes #update! completely reloads all fields on the class, so any extant changes are wiped out
|
19
|
+
ret
|
20
|
+
end
|
21
|
+
|
22
|
+
def reload
|
23
|
+
super.tap { clear_changes }
|
24
|
+
end
|
25
|
+
|
26
|
+
def clear_changes
|
27
|
+
previous = changes
|
28
|
+
(block_given? ? yield : true).tap do |result|
|
29
|
+
unless result == false #failed validation; nil is OK.
|
30
|
+
@previously_changed = previous
|
31
|
+
changed_attributes.clear
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def write_attribute(name, value)
|
37
|
+
attribute_will_change!(name) unless self.read_attribute(name) == value
|
38
|
+
super
|
39
|
+
end
|
40
|
+
|
41
|
+
protected
|
42
|
+
|
43
|
+
def attribute_method?(attr)
|
44
|
+
super || self.class.attributes.has_key?(attr.to_sym)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,201 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module Dynamoid #:nodoc:
|
3
|
+
|
4
|
+
# This is the base module for all domain objects that need to be persisted to
|
5
|
+
# the database as documents.
|
6
|
+
module Document
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
include Dynamoid::Components
|
9
|
+
|
10
|
+
included do
|
11
|
+
class_attribute :options, :read_only_attributes, :base_class
|
12
|
+
self.options = {}
|
13
|
+
self.read_only_attributes = []
|
14
|
+
self.base_class = self
|
15
|
+
|
16
|
+
Dynamoid.included_models << self
|
17
|
+
end
|
18
|
+
|
19
|
+
module ClassMethods
|
20
|
+
# Set up table options, including naming it whatever you want, setting the id key, and manually overriding read and
|
21
|
+
# write capacity.
|
22
|
+
#
|
23
|
+
# @param [Hash] options options to pass for this table
|
24
|
+
# @option options [Symbol] :name the name for the table; this still gets namespaced
|
25
|
+
# @option options [Symbol] :id id column for the table
|
26
|
+
# @option options [Integer] :read_capacity set the read capacity for the table; does not work on existing tables
|
27
|
+
# @option options [Integer] :write_capacity set the write capacity for the table; does not work on existing tables
|
28
|
+
#
|
29
|
+
# @since 0.4.0
|
30
|
+
def table(options = {})
|
31
|
+
self.options = options
|
32
|
+
super if defined? super
|
33
|
+
end
|
34
|
+
|
35
|
+
def attr_readonly(*read_only_attributes)
|
36
|
+
self.read_only_attributes.concat read_only_attributes.map(&:to_s)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns the read_capacity for this table.
|
40
|
+
#
|
41
|
+
# @since 0.4.0
|
42
|
+
def read_capacity
|
43
|
+
options[:read_capacity] || Dynamoid::Config.read_capacity
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns the write_capacity for this table.
|
47
|
+
#
|
48
|
+
# @since 0.4.0
|
49
|
+
def write_capacity
|
50
|
+
options[:write_capacity] || Dynamoid::Config.write_capacity
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns the id field for this class.
|
54
|
+
#
|
55
|
+
# @since 0.4.0
|
56
|
+
def hash_key
|
57
|
+
options[:key] || :id
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns the number of items for this class.
|
61
|
+
#
|
62
|
+
# @since 0.6.1
|
63
|
+
def count
|
64
|
+
Dynamoid.adapter.count(table_name)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Initialize a new object and immediately save it to the database.
|
68
|
+
#
|
69
|
+
# @param [Hash] attrs Attributes with which to create the object.
|
70
|
+
#
|
71
|
+
# @return [Dynamoid::Document] the saved document
|
72
|
+
#
|
73
|
+
# @since 0.2.0
|
74
|
+
def create(attrs = {})
|
75
|
+
build(attrs).tap(&:save)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Initialize a new object and immediately save it to the database. Raise an exception if persistence failed.
|
79
|
+
#
|
80
|
+
# @param [Hash] attrs Attributes with which to create the object.
|
81
|
+
#
|
82
|
+
# @return [Dynamoid::Document] the saved document
|
83
|
+
#
|
84
|
+
# @since 0.2.0
|
85
|
+
def create!(attrs = {})
|
86
|
+
build(attrs).tap(&:save!)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Initialize a new object.
|
90
|
+
#
|
91
|
+
# @param [Hash] attrs Attributes with which to create the object.
|
92
|
+
#
|
93
|
+
# @return [Dynamoid::Document] the new document
|
94
|
+
#
|
95
|
+
# @since 0.2.0
|
96
|
+
def build(attrs = {})
|
97
|
+
attrs[:type] ? attrs[:type].constantize.new(attrs) : new(attrs)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Does this object exist?
|
101
|
+
#
|
102
|
+
# @param [Mixed] id_or_conditions the id of the object or a hash with the options to filter from.
|
103
|
+
#
|
104
|
+
# @return [Boolean] true/false
|
105
|
+
#
|
106
|
+
# @since 0.2.0
|
107
|
+
def exists?(id_or_conditions = {})
|
108
|
+
case id_or_conditions
|
109
|
+
when Hash then ! where(id_or_conditions).all.empty?
|
110
|
+
else !! find(id_or_conditions)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Initialize a new object.
|
116
|
+
#
|
117
|
+
# @param [Hash] attrs Attributes with which to create the object.
|
118
|
+
#
|
119
|
+
# @return [Dynamoid::Document] the new document
|
120
|
+
#
|
121
|
+
# @since 0.2.0
|
122
|
+
def initialize(attrs = {})
|
123
|
+
run_callbacks :initialize do
|
124
|
+
@new_record = true
|
125
|
+
@attributes ||= {}
|
126
|
+
@associations ||= {}
|
127
|
+
|
128
|
+
load(attrs)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def load(attrs)
|
133
|
+
self.class.undump(attrs).each do |key, value|
|
134
|
+
send("#{key}=", value) if self.respond_to?("#{key}=")
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# An object is equal to another object if their ids are equal.
|
139
|
+
#
|
140
|
+
# @since 0.2.0
|
141
|
+
def ==(other)
|
142
|
+
if self.class.identity_map_on?
|
143
|
+
super
|
144
|
+
else
|
145
|
+
return false if other.nil?
|
146
|
+
other.is_a?(Dynamoid::Document) && self.hash_key == other.hash_key && self.range_value == other.range_value
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def eql?(other)
|
151
|
+
self == other
|
152
|
+
end
|
153
|
+
|
154
|
+
def hash
|
155
|
+
hash_key.hash ^ range_value.hash
|
156
|
+
end
|
157
|
+
|
158
|
+
# Reload an object from the database -- if you suspect the object has changed in the datastore and you need those
|
159
|
+
# changes to be reflected immediately, you would call this method. This is a consistent read.
|
160
|
+
#
|
161
|
+
# @return [Dynamoid::Document] the document this method was called on
|
162
|
+
#
|
163
|
+
# @since 0.2.0
|
164
|
+
def reload
|
165
|
+
range_key_value = range_value ? dumped_range_value : nil
|
166
|
+
self.attributes = self.class.find(hash_key, :range_key => range_key_value, :consistent_read => true).attributes
|
167
|
+
@associations.values.each(&:reset)
|
168
|
+
self
|
169
|
+
end
|
170
|
+
|
171
|
+
# Return an object's hash key, regardless of what it might be called to the object.
|
172
|
+
#
|
173
|
+
# @since 0.4.0
|
174
|
+
def hash_key
|
175
|
+
self.send(self.class.hash_key)
|
176
|
+
end
|
177
|
+
|
178
|
+
# Assign an object's hash key, regardless of what it might be called to the object.
|
179
|
+
#
|
180
|
+
# @since 0.4.0
|
181
|
+
def hash_key=(value)
|
182
|
+
self.send("#{self.class.hash_key}=", value)
|
183
|
+
end
|
184
|
+
|
185
|
+
def range_value
|
186
|
+
if range_key = self.class.range_key
|
187
|
+
self.send(range_key)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def range_value=(value)
|
192
|
+
self.send("#{self.class.range_key}=", value)
|
193
|
+
end
|
194
|
+
|
195
|
+
private
|
196
|
+
|
197
|
+
def dumped_range_value
|
198
|
+
dump_field(range_value, self.class.attributes[self.class.range_key])
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module Dynamoid
|
3
|
+
|
4
|
+
# All the errors specific to Dynamoid. The goal is to mimic ActiveRecord.
|
5
|
+
module Errors
|
6
|
+
|
7
|
+
# Generic Dynamoid error
|
8
|
+
class Error < StandardError; end
|
9
|
+
|
10
|
+
class MissingRangeKey < Error; end
|
11
|
+
|
12
|
+
class MissingIndex < Error; end
|
13
|
+
|
14
|
+
# InvalidIndex is raised when an invalid index is specified, for example if
|
15
|
+
# specified key attribute(s) or projected attributes do not exist.
|
16
|
+
class InvalidIndex < Error
|
17
|
+
def initialize(item)
|
18
|
+
if (item.is_a? String)
|
19
|
+
super(item)
|
20
|
+
else
|
21
|
+
super("Validation failed: #{item.errors.full_messages.join(", ")}")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# This class is intended to be private to Dynamoid.
|
27
|
+
class ConditionalCheckFailedException < Error
|
28
|
+
attr_reader :inner_exception
|
29
|
+
|
30
|
+
def initialize(inner)
|
31
|
+
super
|
32
|
+
@inner_exception = inner
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class RecordNotUnique < ConditionalCheckFailedException
|
37
|
+
attr_reader :original_exception
|
38
|
+
|
39
|
+
def initialize(original_exception, record)
|
40
|
+
super("Attempted to write record #{record} when its key already exists")
|
41
|
+
@original_exception = original_exception
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class StaleObjectError < ConditionalCheckFailedException
|
46
|
+
attr_reader :record, :attempted_action
|
47
|
+
|
48
|
+
def initialize(record, attempted_action)
|
49
|
+
super("Attempted to #{attempted_action} a stale object #{record}")
|
50
|
+
@record = record
|
51
|
+
@attempted_action = attempted_action
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class DocumentNotValid < Error
|
56
|
+
attr_reader :document
|
57
|
+
|
58
|
+
def initialize(document)
|
59
|
+
super("Validation failed: #{document.errors.full_messages.join(", ")}")
|
60
|
+
@document = document
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class InvalidQuery < Error; end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,164 @@
|
|
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}?") do
|
55
|
+
value = read_attribute(named)
|
56
|
+
case value
|
57
|
+
when true then true
|
58
|
+
when false, nil then false
|
59
|
+
else
|
60
|
+
!value.nil?
|
61
|
+
end
|
62
|
+
end
|
63
|
+
define_method("#{named}=") {|value| write_attribute(named, value) }
|
64
|
+
end
|
65
|
+
|
66
|
+
def range(name, type = :string)
|
67
|
+
field(name, type)
|
68
|
+
self.range_key = name
|
69
|
+
end
|
70
|
+
|
71
|
+
def table(options)
|
72
|
+
#a default 'id' column is created when Dynamoid::Document is included
|
73
|
+
unless(attributes.has_key? hash_key)
|
74
|
+
remove_field :id
|
75
|
+
field(hash_key)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def remove_field(field)
|
80
|
+
field = field.to_sym
|
81
|
+
attributes.delete(field) or raise "No such field"
|
82
|
+
remove_method field
|
83
|
+
remove_method :"#{field}="
|
84
|
+
remove_method :"#{field}?"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# You can access the attributes of an object directly on its attributes method, which is by default an empty hash.
|
89
|
+
attr_accessor :attributes
|
90
|
+
alias :raw_attributes :attributes
|
91
|
+
|
92
|
+
# Write an attribute on the object. Also marks the previous value as dirty.
|
93
|
+
#
|
94
|
+
# @param [Symbol] name the name of the field
|
95
|
+
# @param [Object] value the value to assign to that field
|
96
|
+
#
|
97
|
+
# @since 0.2.0
|
98
|
+
def write_attribute(name, value)
|
99
|
+
if (size = value.to_s.size) > MAX_ITEM_SIZE
|
100
|
+
Dynamoid.logger.warn "DynamoDB can't store items larger than #{MAX_ITEM_SIZE} and the #{name} field has a length of #{size}."
|
101
|
+
end
|
102
|
+
|
103
|
+
if association = @associations[name]
|
104
|
+
association.reset
|
105
|
+
end
|
106
|
+
|
107
|
+
attributes[name.to_sym] = value
|
108
|
+
end
|
109
|
+
alias :[]= :write_attribute
|
110
|
+
|
111
|
+
# Read an attribute from an object.
|
112
|
+
#
|
113
|
+
# @param [Symbol] name the name of the field
|
114
|
+
#
|
115
|
+
# @since 0.2.0
|
116
|
+
def read_attribute(name)
|
117
|
+
attributes[name.to_sym]
|
118
|
+
end
|
119
|
+
alias :[] :read_attribute
|
120
|
+
|
121
|
+
# Updates multiple attibutes at once, saving the object once the updates are complete.
|
122
|
+
#
|
123
|
+
# @param [Hash] attributes a hash of attributes to update
|
124
|
+
#
|
125
|
+
# @since 0.2.0
|
126
|
+
def update_attributes(attributes)
|
127
|
+
attributes.each {|attribute, value| self.write_attribute(attribute, value)} unless attributes.nil? || attributes.empty?
|
128
|
+
save
|
129
|
+
end
|
130
|
+
|
131
|
+
# Update a single attribute, saving the object afterwards.
|
132
|
+
#
|
133
|
+
# @param [Symbol] attribute the attribute to update
|
134
|
+
# @param [Object] value the value to assign it
|
135
|
+
#
|
136
|
+
# @since 0.2.0
|
137
|
+
def update_attribute(attribute, value)
|
138
|
+
write_attribute(attribute, value)
|
139
|
+
save
|
140
|
+
end
|
141
|
+
|
142
|
+
private
|
143
|
+
|
144
|
+
# Automatically called during the created callback to set the created_at time.
|
145
|
+
#
|
146
|
+
# @since 0.2.0
|
147
|
+
def set_created_at
|
148
|
+
self.created_at = DateTime.now if Dynamoid::Config.timestamps
|
149
|
+
end
|
150
|
+
|
151
|
+
# Automatically called during the save callback to set the updated_at time.
|
152
|
+
#
|
153
|
+
# @since 0.2.0
|
154
|
+
def set_updated_at
|
155
|
+
self.updated_at = DateTime.now if Dynamoid::Config.timestamps
|
156
|
+
end
|
157
|
+
|
158
|
+
def set_type
|
159
|
+
self.type ||= self.class.to_s if self.class.attributes[:type]
|
160
|
+
end
|
161
|
+
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|